| 1 |
import React, { useMemo, useState } from 'react' |
| 2 |
import classnames from 'classnames' |
| 3 |
import { fetchQueryParam } from '../../../utils/urls' |
| 4 |
import { ColumnHeadings } from './ColumnHeadings' |
| 5 |
import { TableRows } from './TableRows' |
| 6 |
import { TableNavigation } from './TableNavigation' |
| 7 |
import type { ColumnHeadingsProps } from './ColumnHeadings' |
| 8 |
import type { Key, ReactNode } from 'react' |
| 9 |
|
| 10 |
export interface ListTableColumn<T> { |
| 11 |
id: Key |
| 12 |
title?: ReactNode |
| 13 |
render: (item: T) => ReactNode |
| 14 |
isHidden?: boolean |
| 15 |
isPrimary?: boolean |
| 16 |
isHeading?: boolean |
| 17 |
sortedValue?: (item: T) => Key |
| 18 |
defaultSortDirection?: ListTableSortDirection |
| 19 |
} |
| 20 |
|
| 21 |
export type ListTableSortDirection = 'asc' | 'desc' |
| 22 |
|
| 23 |
export interface ListTableAction<A extends string> { |
| 24 |
key: A |
| 25 |
label: string |
| 26 |
group?: string |
| 27 |
} |
| 28 |
|
| 29 |
export interface ListTableNavProps<K extends Key, A extends string> { |
| 30 |
actions?: ListTableAction<A>[] |
| 31 |
doAction?: (action: A, selected: Set<K>) => Promise<void> |
| 32 |
disabled?: boolean |
| 33 |
extraTableNav?: (which: 'top' | 'bottom') => ReactNode |
| 34 |
endTableNav?: (which: 'top' | 'bottom') => ReactNode |
| 35 |
} |
| 36 |
|
| 37 |
export interface ListTableRowsProps<T, K extends Key> { |
| 38 |
getKey: (item: T) => K |
| 39 |
columns: ListTableColumn<T>[] |
| 40 |
noItems?: ReactNode |
| 41 |
rowClassName?: (item: T) => string |
| 42 |
} |
| 43 |
|
| 44 |
export interface ListTablePaginationProps { |
| 45 |
totalPages?: number |
| 46 |
pageSearchParam?: string |
| 47 |
} |
| 48 |
|
| 49 |
export interface ListTableBorderProps { |
| 50 |
fixed?: boolean |
| 51 |
striped?: boolean |
| 52 |
className?: string |
| 53 |
} |
| 54 |
|
| 55 |
export const sortTableItems = <T, >( |
| 56 |
items: T[], |
| 57 |
sortColumn: ListTableColumn<T> | undefined, |
| 58 |
sortDirection: ListTableSortDirection |
| 59 |
): T[] => |
| 60 |
items.toSorted((itemA, itemB) => { |
| 61 |
const valueA = sortColumn?.sortedValue?.(itemA) |
| 62 |
const valueB = sortColumn?.sortedValue?.(itemB) |
| 63 |
|
| 64 |
if (valueA === undefined || valueB === undefined) { |
| 65 |
return 0 |
| 66 |
} |
| 67 |
|
| 68 |
if (valueA < valueB) { |
| 69 |
return 'asc' === sortDirection ? -1 : 1 |
| 70 |
} |
| 71 |
|
| 72 |
if (valueA > valueB) { |
| 73 |
return 'asc' === sortDirection ? 1 : -1 |
| 74 |
} |
| 75 |
|
| 76 |
return 0 |
| 77 |
}) |
| 78 |
|
| 79 |
const pageItems = <T, >( |
| 80 |
items: T[], |
| 81 |
{ currentPage, totalPages }: { currentPage: number; totalPages?: number } |
| 82 |
): T[] => { |
| 83 |
if (totalPages) { |
| 84 |
const itemsPerPage = Math.ceil(items.length / totalPages) |
| 85 |
const start = (currentPage - 1) * itemsPerPage |
| 86 |
const end = start + itemsPerPage |
| 87 |
return items.slice(start, end) |
| 88 |
} else { |
| 89 |
return items |
| 90 |
} |
| 91 |
} |
| 92 |
|
| 93 |
const getVisibleSelected = <T, K extends Key>( |
| 94 |
visibleItems: T[], |
| 95 |
getKey: (item: T) => K, |
| 96 |
selected: Set<K> |
| 97 |
): Set<K> => |
| 98 |
new Set(visibleItems.map(getKey).filter(key => selected.has(key))) |
| 99 |
|
| 100 |
interface TableBorderProps<T, K extends Key> |
| 101 |
extends ListTableBorderProps, Omit<ColumnHeadingsProps<T, K>, 'which'> { |
| 102 |
children: ReactNode |
| 103 |
} |
| 104 |
|
| 105 |
const TableBorder = <T, K extends Key>({ |
| 106 |
fixed, |
| 107 |
striped, |
| 108 |
children, |
| 109 |
className, |
| 110 |
...tableHeadingsProps |
| 111 |
}: TableBorderProps<T, K>) => ( |
| 112 |
<table className={classnames('wp-list-table widefat', { striped, fixed }, className)}> |
| 113 |
<thead> |
| 114 |
<ColumnHeadings which="head" {...tableHeadingsProps} /> |
| 115 |
</thead> |
| 116 |
<tbody> |
| 117 |
{children} |
| 118 |
</tbody> |
| 119 |
<tfoot> |
| 120 |
<ColumnHeadings which="foot" {...tableHeadingsProps} /> |
| 121 |
</tfoot> |
| 122 |
</table> |
| 123 |
) |
| 124 |
|
| 125 |
export interface ListTableProps<T, K extends Key, A extends string> extends ListTableBorderProps, |
| 126 |
ListTableNavProps<K, A>, |
| 127 |
ListTablePaginationProps, |
| 128 |
ListTableRowsProps<T, K> { |
| 129 |
items: T[] |
| 130 |
beforeTable?: ReactNode |
| 131 |
selectAllControl?: boolean |
| 132 |
} |
| 133 |
|
| 134 |
export const ListTable = <T, K extends Key, A extends string = never>({ |
| 135 |
items, |
| 136 |
getKey, |
| 137 |
totalPages, |
| 138 |
pageSearchParam = 'paged', |
| 139 |
...tableProps |
| 140 |
}: ListTableProps<T, K, A>) => { |
| 141 |
const [sortColumn, setSortColumn] = useState<ListTableColumn<T>>() |
| 142 |
const [currentPage, setCurrentPage] = useState( |
| 143 |
() => pageSearchParam && Number(fetchQueryParam(pageSearchParam)) || 1 |
| 144 |
) |
| 145 |
const [sortDirection, setSortDirection] = useState<ListTableSortDirection>('asc') |
| 146 |
|
| 147 |
const visibleItems: T[] = useMemo( |
| 148 |
() => pageItems(sortTableItems(items, sortColumn, sortDirection), { currentPage, totalPages }), |
| 149 |
[items, sortColumn, sortDirection, currentPage, totalPages]) |
| 150 |
|
| 151 |
return ( |
| 152 |
<PartialDataListTable |
| 153 |
getKey={getKey} |
| 154 |
totalItems={items.length} |
| 155 |
totalPages={totalPages} |
| 156 |
visibleItems={visibleItems} |
| 157 |
pageSearchParam={pageSearchParam} |
| 158 |
{...{ sortColumn, sortDirection, currentPage, setSortColumn, setSortDirection, setCurrentPage }} |
| 159 |
{...tableProps} |
| 160 |
/> |
| 161 |
) |
| 162 |
} |
| 163 |
|
| 164 |
export interface PartialDataListTableProps<T, K extends Key, A extends string> |
| 165 |
extends ListTablePaginationProps, |
| 166 |
ListTableBorderProps, |
| 167 |
ListTableRowsProps<T, K>, |
| 168 |
ListTableNavProps<K, A> { |
| 169 |
sortColumn: ListTableColumn<T> | undefined |
| 170 |
totalItems: number |
| 171 |
currentPage: number |
| 172 |
visibleItems: T[] |
| 173 |
beforeTable?: ReactNode |
| 174 |
selectAllControl?: boolean |
| 175 |
setSortColumn: (column: ListTableColumn<T> | undefined) => void |
| 176 |
sortDirection?: ListTableSortDirection |
| 177 |
setCurrentPage: (page: number) => void |
| 178 |
setSortDirection: (direction: ListTableSortDirection) => void |
| 179 |
} |
| 180 |
|
| 181 |
export const PartialDataListTable = <T, K extends Key, A extends string>({ |
| 182 |
getKey, |
| 183 |
actions, |
| 184 |
columns, |
| 185 |
noItems, |
| 186 |
doAction, |
| 187 |
disabled = false, |
| 188 |
totalItems, |
| 189 |
totalPages, |
| 190 |
currentPage, |
| 191 |
beforeTable, |
| 192 |
endTableNav, |
| 193 |
rowClassName, |
| 194 |
visibleItems, |
| 195 |
sortDirection = 'asc', |
| 196 |
extraTableNav, |
| 197 |
selectAllControl, |
| 198 |
setCurrentPage, |
| 199 |
pageSearchParam, |
| 200 |
...tableBorderProps |
| 201 |
}: PartialDataListTableProps<T, K, A>) => { |
| 202 |
const [selected, setSelected] = useState(() => new Set<K>()) |
| 203 |
|
| 204 |
return ( |
| 205 |
<TableNavigation |
| 206 |
totalItems={totalItems} |
| 207 |
selected={getVisibleSelected(visibleItems, getKey, selected)} |
| 208 |
selectAllKeys={selectAllControl ? visibleItems.map(getKey) : undefined} |
| 209 |
{...{ actions, doAction, extraTableNav, endTableNav, disabled, currentPage, totalPages }} |
| 210 |
{...{ pageSearchParam, setSelected, setCurrentPage }} |
| 211 |
> |
| 212 |
{beforeTable} |
| 213 |
|
| 214 |
<TableBorder |
| 215 |
items={visibleItems} |
| 216 |
{...tableBorderProps} |
| 217 |
{...{ getKey, columns, selected, setSelected, sortDirection }} |
| 218 |
> |
| 219 |
<TableRows |
| 220 |
items={visibleItems} |
| 221 |
{...{ getKey, columns, noItems, rowClassName, selected, setSelected }} |
| 222 |
/> |
| 223 |
</TableBorder> |
| 224 |
</TableNavigation> |
| 225 |
) |
| 226 |
} |
| 227 |
|