Skip to content

Commit bf3f420

Browse files
committed
feat: add pagination, filter & resizable columns to table
1 parent d376aec commit bf3f420

File tree

13 files changed

+594
-14
lines changed

13 files changed

+594
-14
lines changed

src/Common/Hooks/useStateFilters/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface UseStateFiltersReturnType<T>
3131
| 'pageSize'
3232
| 'searchKey'
3333
| 'handleSearch'
34+
| 'isFilterApplied'
3435
> {}
3536

3637
export interface PaginationType<T> extends Pick<UseUrlFiltersReturnType<T>, 'pageSize'> {

src/Common/Hooks/useStateFilters/useStateFilters.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ const useStateFilters = <T = string,>({
116116
changePage,
117117
changePageSize,
118118
offset,
119+
isFilterApplied: !!searchKey,
119120
}
120121
}
121122

src/Common/Hooks/useUrlFilters/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,5 @@ export type UseUrlFiltersReturnType<T, K = unknown> = K & {
8686
* Update the search params with the passed object
8787
*/
8888
updateSearchParams: (paramsToSerialize: Partial<K>, options?: UpdateSearchParamsOptionsType<T, K>) => void
89+
isFilterApplied: boolean
8990
}

src/Common/Hooks/useUrlFilters/useUrlFilters.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ const useUrlFilters = <T = string, K = unknown>({
220220
clearFilters,
221221
...parsedParams,
222222
updateSearchParams,
223+
isFilterApplied: !!Object.keys(parsedParams).length || !!searchKey,
223224
}
224225
}
225226

Lines changed: 266 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,267 @@
1-
const Table = () => {}
1+
import {
2+
ErrorScreenManager,
3+
GenericEmptyState,
4+
GenericFilterEmptyState,
5+
Pagination,
6+
showError,
7+
SortableTableHeaderCell,
8+
useAsync,
9+
} from '@Common/index'
10+
import { Fragment, useEffect, useMemo, useRef, useState } from 'react'
11+
import { Cell, FiltersTypeEnum, InternalTablePropsWithWrappers, RowsType, TableProps } from './types'
12+
import {
13+
getFilterWrapperComponent,
14+
getVisibleColumns,
15+
searchAndSortRows,
16+
setVisibleColumnsToLocalStorage,
17+
} from './utils'
18+
import { SEARCH_SORT_CHANGE_DEBOUNCE_TIME } from './constants'
19+
import UseResizableTableConfigWrapper from './UseResizableTableConfigWrapper'
220

3-
export default Table
21+
const InternalTable = ({
22+
filtersVariant,
23+
filterData,
24+
rows,
25+
getRows,
26+
columns,
27+
ViewWrapper,
28+
resizableConfig,
29+
emptyStateConfig,
30+
additionalProps,
31+
configurableColumns,
32+
id,
33+
}: InternalTablePropsWithWrappers) => {
34+
const [visibleColumns, setVisibleColumns] = useState(getVisibleColumns({ columns, id, configurableColumns }))
35+
36+
const setVisibleColumnsWrapper = (newVisibleColumns: typeof visibleColumns) => {
37+
setVisibleColumns(newVisibleColumns)
38+
setVisibleColumnsToLocalStorage({ id, visibleColumns: newVisibleColumns })
39+
}
40+
41+
useEffect(() => {
42+
getVisibleColumns({ columns, id, configurableColumns })
43+
}, [columns, id, configurableColumns])
44+
45+
const {
46+
sortBy,
47+
sortOrder,
48+
searchKey = '',
49+
handleSorting,
50+
pageSize,
51+
offset,
52+
changePage,
53+
changePageSize,
54+
clearFilters,
55+
isFilterApplied,
56+
} = filterData ?? {}
57+
58+
const {
59+
handleResize,
60+
gridTemplateColumns = visibleColumns
61+
.map((column) => (typeof column.size?.fixed === 'number' ? `${column.size.fixed}px` : '1fr'))
62+
.join(' '),
63+
} = resizableConfig ?? {}
64+
65+
const searchSortTimeoutRef = useRef<number>(-1)
66+
67+
const isColumnVisibleVector = useMemo(() => {
68+
const visibleColumnsSet = new Set(visibleColumns.map(({ label }) => label))
69+
70+
return columns.map((column) => !!visibleColumnsSet.has(column.label))
71+
}, [visibleColumns, columns])
72+
73+
const sortByToColumnIndexMap: Record<string, number> = useMemo(
74+
() =>
75+
visibleColumns.reduce((acc, column, index) => {
76+
acc[column.label] = index
77+
78+
return acc
79+
}, {}),
80+
// NOTE: wrap columns in useMemo
81+
[visibleColumns],
82+
)
83+
84+
const [areFilteredRowsLoading, _filteredRows, filteredRowsError] = useAsync(async () => {
85+
if (rows) {
86+
return new Promise<RowsType>((resolve, reject) => {
87+
const sortByColumnIndex = sortByToColumnIndexMap[sortBy]
88+
89+
if (searchSortTimeoutRef.current !== -1) {
90+
clearTimeout(searchSortTimeoutRef.current)
91+
}
92+
93+
searchSortTimeoutRef.current = setTimeout(() => {
94+
try {
95+
resolve(
96+
searchAndSortRows(
97+
rows,
98+
filterData,
99+
sortByColumnIndex,
100+
visibleColumns[sortByColumnIndex]?.comparator,
101+
),
102+
)
103+
} catch (error) {
104+
console.error('Error while filtering/sorting rows:', error)
105+
showError(error)
106+
reject(error)
107+
}
108+
109+
searchSortTimeoutRef.current = -1
110+
}, SEARCH_SORT_CHANGE_DEBOUNCE_TIME)
111+
})
112+
}
113+
114+
try {
115+
return getRows(filterData)
116+
} catch (error) {
117+
console.error('Error while fetching rows:', error)
118+
showError(error)
119+
throw error
120+
}
121+
}, [searchKey, sortBy, sortOrder, rows, sortByToColumnIndexMap])
122+
123+
const filteredRows = _filteredRows ?? []
124+
125+
const Wrapper = ViewWrapper ?? Fragment
126+
127+
const getTriggerSortingHandler = (newSortBy: string) => () => {
128+
handleSorting(newSortBy)
129+
}
130+
131+
const renderContent = () => {
132+
if (!areFilteredRowsLoading && !filteredRows.length) {
133+
return filtersVariant !== FiltersTypeEnum.NONE && isFilterApplied ? (
134+
<GenericFilterEmptyState handleClearFilters={clearFilters} />
135+
) : (
136+
<GenericEmptyState {...emptyStateConfig.noRowsConfig} />
137+
)
138+
}
139+
140+
if (filteredRowsError) {
141+
return <ErrorScreenManager code={filteredRowsError.code} />
142+
}
143+
144+
return (
145+
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
146+
<div tabIndex={0} className="generic-table flexbox-col dc__overflow-hidden">
147+
<table className="flexbox-col flex-grow-1 w-100 dc__overflow-hidden">
148+
<thead>
149+
<tr className="dc__grid" style={{ gridTemplateColumns }}>
150+
{visibleColumns.map(({ label, isSortable, size, showTippyOnTruncate }) => {
151+
const isResizable = !!size.range
152+
153+
return (
154+
<SortableTableHeaderCell
155+
key={label}
156+
title={label}
157+
isSortable={isSortable}
158+
sortOrder={sortOrder}
159+
isSorted={sortBy === label}
160+
triggerSorting={getTriggerSortingHandler(label)}
161+
showTippyOnTruncate={showTippyOnTruncate}
162+
disabled={areFilteredRowsLoading}
163+
{...(isResizable
164+
? { isResizable, handleResize, id: label }
165+
: { isResizable: false })}
166+
/>
167+
)
168+
})}
169+
</tr>
170+
</thead>
171+
172+
<tbody className="dc__overflow-auto flex-grow-1 flexbox-col">
173+
{(areFilteredRowsLoading ? Array(3) : filteredRows)
174+
.filter((_, index) => isColumnVisibleVector[index])
175+
.map((row) => (
176+
<tr className="dc__grid" style={{ gridTemplateColumns }}>
177+
{row.map(({ data, label, horizontallySticky, render }: Cell) => {
178+
if (areFilteredRowsLoading) {
179+
return <div className="dc__shimmer" />
180+
}
181+
182+
if (render) {
183+
return render(data, filterData)
184+
}
185+
186+
return (
187+
<td
188+
className={
189+
horizontallySticky ? 'dc__position-sticky dc__left-0 dc__zi-1' : ''
190+
}
191+
>
192+
{label}
193+
</td>
194+
)
195+
})}
196+
</tr>
197+
))}
198+
</tbody>
199+
</table>
200+
201+
{!!filteredRows?.length && filteredRows.length > pageSize && (
202+
<Pagination
203+
pageSize={pageSize}
204+
changePage={changePage}
205+
changePageSize={changePageSize}
206+
offset={offset}
207+
rootClassName="dc__border-top flex dc__content-space px-20 py-10"
208+
size={filteredRows.length}
209+
/>
210+
)}
211+
</div>
212+
)
213+
}
214+
215+
return (
216+
<Wrapper
217+
{...{
218+
...filterData,
219+
...additionalProps,
220+
areRowsLoading: areFilteredRowsLoading,
221+
...(configurableColumns
222+
? {
223+
allColumns: columns,
224+
setVisibleColumns: setVisibleColumnsWrapper,
225+
visibleColumns,
226+
}
227+
: {}),
228+
}}
229+
>
230+
{renderContent()}
231+
</Wrapper>
232+
)
233+
}
234+
235+
const TableWithResizableConfigWrapper = (tableProps: InternalTablePropsWithWrappers) => {
236+
const { columns } = tableProps
237+
238+
const isResizable = columns.some(({ size }) => !!size?.range)
239+
240+
return isResizable ? (
241+
<UseResizableTableConfigWrapper columns={columns}>
242+
<InternalTable {...tableProps} />
243+
</UseResizableTableConfigWrapper>
244+
) : (
245+
<InternalTable {...tableProps} />
246+
)
247+
}
248+
249+
const TableWrapper = (tableProps: TableProps) => {
250+
const { filtersVariant, additionalFilterProps } = tableProps
251+
252+
const FilterWrapperComponent = getFilterWrapperComponent(filtersVariant)
253+
const wrapperProps = FilterWrapperComponent === Fragment ? {} : { additionalFilterProps }
254+
255+
return (
256+
<FilterWrapperComponent {...wrapperProps}>
257+
{/* NOTE: filterData will be populated by FilterWrapperComponent */}
258+
<TableWithResizableConfigWrapper
259+
{...(tableProps as InternalTablePropsWithWrappers)}
260+
resizableConfig={null}
261+
filterData={null}
262+
/>
263+
</FilterWrapperComponent>
264+
)
265+
}
266+
267+
export default TableWrapper
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { cloneElement } from 'react'
2+
import { useResizableTableConfig } from '@Common/SortableTableHeaderCell'
3+
import { UseResizableTableConfigWrapperProps } from './types'
4+
5+
const UseResizableTableConfigWrapper = ({ children, columns }: UseResizableTableConfigWrapperProps) => {
6+
const resizableConfig = useResizableTableConfig({
7+
headersConfig: columns.map(({ label, size }) => {
8+
const {
9+
range: { minWidth, maxWidth, startWidth },
10+
} = size
11+
12+
return {
13+
id: label,
14+
minWidth,
15+
width: startWidth,
16+
maxWidth: maxWidth === 'infinite' ? Number.MAX_SAFE_INTEGER : maxWidth,
17+
}
18+
}),
19+
})
20+
21+
return cloneElement(children, { ...children.props, resizableConfig })
22+
}
23+
24+
export default UseResizableTableConfigWrapper
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { cloneElement } from 'react'
2+
import { useStateFilters } from '@Common/Hooks'
3+
import { FiltersTypeEnum, FilterWrapperProps } from './types'
4+
5+
const UseStateFilterWrapper = ({ children, additionalFilterProps }: FilterWrapperProps<FiltersTypeEnum.STATE>) => {
6+
const filterData = useStateFilters<string>(additionalFilterProps)
7+
8+
return cloneElement(children, { ...children.props, filterData })
9+
}
10+
11+
export default UseStateFilterWrapper
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { cloneElement } from 'react'
2+
import { useUrlFilters } from '@Common/Hooks'
3+
import { FiltersTypeEnum, FilterWrapperProps } from './types'
4+
5+
const UseUrlFilterWrapper = ({ children, additionalFilterProps }: FilterWrapperProps<FiltersTypeEnum.URL>) => {
6+
const filterData = useUrlFilters<string, unknown>(additionalFilterProps)
7+
8+
return cloneElement(children, { ...children.props, filterData })
9+
}
10+
11+
export default UseUrlFilterWrapper
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const SEARCH_SORT_CHANGE_DEBOUNCE_TIME = 350 /** in ms */
2+
3+
export const LOCAL_STORAGE_EXISTS = !!(Storage && localStorage)
4+
5+
export const LOCAL_STORAGE_KEY_FOR_VISIBLE_COLUMNS = 'generic-table-configurable-columns'

src/Shared/Components/Table/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
export { default as Table } from './Table.component'
2+
export { FiltersTypeEnum, PaginationEnum } from './types'
3+
export type {
4+
ViewWrapperProps as TableViewWrapperProps,
5+
RowComponentProps as TableRowComponentProps,
6+
Column as TableColumnType,
7+
TableProps,
8+
Cell as TableCellType,
9+
} from './types'

src/Shared/Components/Table/styles.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.table {
1+
.generic-table {
22
table, caption, tbody, tfoot, thead, tr, th, td {
33
margin: 0;
44
padding: 0;

0 commit comments

Comments
 (0)