| 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 |
} |
| 35 |
|
| 36 |
export interface ListTableRowsProps<T, K extends Key> { |
| 37 |
items: T[] |
| 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 |
useQueryVars?: boolean |
| 47 |
} |
| 48 |
|
| 49 |
export interface ListTableBorderProps { |
| 50 |
fixed?: boolean |
| 51 |
striped?: boolean |
| 52 |
className?: string |
| 53 |
} |
| 54 |
|
| 55 |
export type ListTableProps<T, K extends Key, A extends string> = |
| 56 |
ListTableBorderProps & |
| 57 |
ListTableNavProps<K, A> & |
| 58 |
ListTablePaginationProps & |
| 59 |
ListTableRowsProps<T, K> |
| 60 |
|
| 61 |
const sortItems = <T, >( |
| 62 |
items: T[], |
| 63 |
sortColumn: ListTableColumn<T> | undefined, |
| 64 |
sortDirection: ListTableSortDirection |
| 65 |
): T[] => |
| 66 |
items.toSorted((itemA, itemB) => { |
| 67 |
const valueA = sortColumn?.sortedValue?.(itemA) |
| 68 |
const valueB = sortColumn?.sortedValue?.(itemB) |
| 69 |
|
| 70 |
if (valueA === undefined || valueB === undefined) { |
| 71 |
return 0 |
| 72 |
} |
| 73 |
|
| 74 |
if (valueA < valueB) { |
| 75 |
return 'asc' === sortDirection ? -1 : 1 |
| 76 |
} |
| 77 |
|
| 78 |
if (valueA > valueB) { |
| 79 |
return 'asc' === sortDirection ? 1 : -1 |
| 80 |
} |
| 81 |
|
| 82 |
return 0 |
| 83 |
}) |
| 84 |
|
| 85 |
const pageItems = <T, >( |
| 86 |
items: T[], |
| 87 |
{ currentPage, totalPages }: { currentPage: number; totalPages?: number } |
| 88 |
): T[] => { |
| 89 |
if (totalPages) { |
| 90 |
const itemsPerPage = Math.ceil(items.length / totalPages) |
| 91 |
const start = (currentPage - 1) * itemsPerPage |
| 92 |
const end = start + itemsPerPage |
| 93 |
return items.slice(start, end) |
| 94 |
} else { |
| 95 |
return items |
| 96 |
} |
| 97 |
} |
| 98 |
|
| 99 |
const getVisibleSelected = <T, K extends Key>( |
| 100 |
visibleItems: T[], |
| 101 |
getKey: (item: T) => K, |
| 102 |
selected: Set<K> |
| 103 |
): Set<K> => |
| 104 |
new Set(visibleItems.map(getKey).filter(key => selected.has(key))) |
| 105 |
|
| 106 |
interface TableBorderProps<T, K extends Key> extends ListTableBorderProps, Omit<ColumnHeadingsProps<T, K>, 'which'> { |
| 107 |
children: ReactNode |
| 108 |
} |
| 109 |
|
| 110 |
const TableBorder = <T, K extends Key>({ |
| 111 |
fixed, |
| 112 |
striped, |
| 113 |
children, |
| 114 |
className, |
| 115 |
...tableHeadingsProps |
| 116 |
}: TableBorderProps<T, K>) => ( |
| 117 |
<table className={classnames('wp-list-table widefat', { striped, fixed }, className)}> |
| 118 |
<thead> |
| 119 |
<ColumnHeadings which="head" {...tableHeadingsProps} /> |
| 120 |
</thead> |
| 121 |
<tbody> |
| 122 |
{children} |
| 123 |
</tbody> |
| 124 |
<tfoot> |
| 125 |
<ColumnHeadings which="foot" {...tableHeadingsProps} /> |
| 126 |
</tfoot> |
| 127 |
</table> |
| 128 |
) |
| 129 |
|
| 130 |
export const ListTable = <T, K extends Key, A extends string = never>({ |
| 131 |
items, |
| 132 |
getKey, |
| 133 |
columns, |
| 134 |
actions, |
| 135 |
doAction, |
| 136 |
totalPages, |
| 137 |
extraTableNav, |
| 138 |
disabled = false, |
| 139 |
useQueryVars = true, |
| 140 |
fixed, |
| 141 |
striped, |
| 142 |
className, |
| 143 |
...tableRowsProps |
| 144 |
}: ListTableProps<T, K, A>) => { |
| 145 |
const [selected, setSelected] = useState(() => new Set<K>()) |
| 146 |
const [sortColumn, setSortColumn] = useState<ListTableColumn<T>>() |
| 147 |
const [currentPage, setCurrentPage] = useState(() => useQueryVars && Number(fetchQueryParam('paged')) || 1) |
| 148 |
const [sortDirection, setSortDirection] = useState<ListTableSortDirection>('asc') |
| 149 |
|
| 150 |
const visibleItems: T[] = useMemo( |
| 151 |
() => pageItems(sortItems(items, sortColumn, sortDirection), { currentPage, totalPages }), |
| 152 |
[items, sortColumn, sortDirection, currentPage, totalPages]) |
| 153 |
return ( |
| 154 |
<TableNavigation |
| 155 |
totalItems={items.length} |
| 156 |
selected={getVisibleSelected(visibleItems, getKey, selected)} |
| 157 |
{...{ actions, doAction, extraTableNav, disabled, currentPage, totalPages, useQueryVars, setSelected, setCurrentPage }} |
| 158 |
> |
| 159 |
<TableBorder |
| 160 |
items={visibleItems} |
| 161 |
fixed={fixed} |
| 162 |
striped={striped} |
| 163 |
className={className} |
| 164 |
{...{ columns, getKey, selected, sortColumn, sortDirection, setSelected, setSortColumn, setSortDirection }} |
| 165 |
> |
| 166 |
<TableRows |
| 167 |
items={visibleItems} |
| 168 |
{...{ getKey, columns, selected, setSelected }} |
| 169 |
{...tableRowsProps} |
| 170 |
/> |
| 171 |
</TableBorder> |
| 172 |
</TableNavigation> |
| 173 |
) |
| 174 |
} |
| 175 |
|