| 1 |
import {useState} from 'react'; |
| 2 |
import PropTypes from 'prop-types'; |
| 3 |
import classNames from 'classnames'; |
| 4 |
import {LoadingOverlay, Spinner} from '@givewp/components'; |
| 5 |
|
| 6 |
import styles from './style.module.scss'; |
| 7 |
|
| 8 |
import {__} from '@wordpress/i18n'; |
| 9 |
|
| 10 |
const Table = ({title = null, columns = [], data = [], columnFilters = {}, stripped = false, isLoading = false, isSorting, ...rest}) => { |
| 11 |
const [state, setState] = useState({}); |
| 12 |
const [cachedData, setCachedData] = useState([]); |
| 13 |
|
| 14 |
Table.resetSortState = () => { |
| 15 |
setState({}); |
| 16 |
}; |
| 17 |
|
| 18 |
// Clear cache if data is empty |
| 19 |
if (!isLoading && !data.length && cachedData.length) { |
| 20 |
setCachedData([]); |
| 21 |
} else if (data.length && data !== cachedData) { |
| 22 |
// Cache data so we can show that under overlay while new data is fetching |
| 23 |
setCachedData(data); |
| 24 |
} |
| 25 |
|
| 26 |
const visibleColumns = columns.filter((column) => column.visible || column.visible === undefined); |
| 27 |
const allowedColumns = visibleColumns.map((column) => column.key); |
| 28 |
// Get additional row columns added manually |
| 29 |
const additionalColumns = visibleColumns |
| 30 |
.filter((column) => 'append' in column && column.append) |
| 31 |
.map((column) => { |
| 32 |
return { |
| 33 |
[column.key]: column.defaultValue ?? '', |
| 34 |
}; |
| 35 |
}); |
| 36 |
|
| 37 |
// Used when additional columns are added, but they not exist in the result row. |
| 38 |
// So we have to sort result row to match columns order |
| 39 |
const sortResults = (order, object) => { |
| 40 |
const newObject = {}; |
| 41 |
|
| 42 |
order.forEach((key) => { |
| 43 |
newObject[key] = object[key]; |
| 44 |
}); |
| 45 |
return newObject; |
| 46 |
}; |
| 47 |
|
| 48 |
const handleItemSort = (item) => { |
| 49 |
const direction = state[item.label] === 'desc' ? 'asc' : 'desc'; |
| 50 |
|
| 51 |
setState({[item.label]: direction}); |
| 52 |
|
| 53 |
return item.sortCallback(direction); |
| 54 |
}; |
| 55 |
|
| 56 |
const getItemSortDirectionIcon = (item) => { |
| 57 |
if (!state[item.label]) { |
| 58 |
return ( |
| 59 |
<span className={classNames('dashicons dashicons-sort', styles.sortIcons, styles.sortIconUndefined)} /> |
| 60 |
); |
| 61 |
} |
| 62 |
|
| 63 |
const iconClasses = classNames( |
| 64 |
'dashicons', |
| 65 |
styles.sortIcons, |
| 66 |
{'dashicons-arrow-down': state[item.label] === 'desc'}, |
| 67 |
{'dashicons-arrow-up': state[item.label] === 'asc'} |
| 68 |
); |
| 69 |
|
| 70 |
return <span className={iconClasses} />; |
| 71 |
}; |
| 72 |
|
| 73 |
const getHeaderRow = () => { |
| 74 |
return visibleColumns.map((item, index) => { |
| 75 |
const columnStyles = item.styles ? {style: item.styles} : null; |
| 76 |
return ( |
| 77 |
<div className={styles.label} key={index} {...columnStyles}> |
| 78 |
{item.label} |
| 79 |
{item.sort && typeof item.sortCallback === 'function' && ( |
| 80 |
<span onClick={() => handleItemSort(item)}>{getItemSortDirectionIcon(item)}</span> |
| 81 |
)} |
| 82 |
{isLoading && isSorting && state[item.label] !== undefined && ( |
| 83 |
<Spinner size="tiny" style={{marginLeft: 10}} /> |
| 84 |
)} |
| 85 |
</div> |
| 86 |
); |
| 87 |
}); |
| 88 |
}; |
| 89 |
|
| 90 |
const getRows = () => { |
| 91 |
if (!isLoading && data.length === 0) { |
| 92 |
return <div className={styles.noData}>{__('No data', 'give')}</div>; |
| 93 |
} |
| 94 |
|
| 95 |
if (cachedData.length && isLoading) { |
| 96 |
data = cachedData; |
| 97 |
} |
| 98 |
|
| 99 |
return data.map((row, index) => { |
| 100 |
// Add additional row columns and sort the result row |
| 101 |
const result = |
| 102 |
additionalColumns.length > 0 |
| 103 |
? sortResults(allowedColumns, Object.assign(row, ...additionalColumns)) |
| 104 |
: row; |
| 105 |
const RowItems = Object.entries(result) |
| 106 |
// Display only provided columns |
| 107 |
.filter(([key]) => allowedColumns.includes(key)) |
| 108 |
.map(([key, value]) => { |
| 109 |
if (columnFilters[key] && typeof columnFilters[key] === 'function') { |
| 110 |
value = columnFilters[key](value, data[index]); |
| 111 |
} |
| 112 |
|
| 113 |
const currentColumn = visibleColumns.find((column) => column.key === key); |
| 114 |
const columnStyles = currentColumn.styles ? {style: currentColumn.styles} : null; |
| 115 |
|
| 116 |
return ( |
| 117 |
<div className={styles.item} key={key} {...columnStyles}> |
| 118 |
{value} |
| 119 |
</div> |
| 120 |
); |
| 121 |
}); |
| 122 |
|
| 123 |
const rowClasses = classNames( |
| 124 |
'give-table-row', |
| 125 |
{[styles.rowStripped]: stripped}, |
| 126 |
{[styles.row]: !stripped} |
| 127 |
); |
| 128 |
|
| 129 |
return ( |
| 130 |
<div className={rowClasses} key={index}> |
| 131 |
{RowItems} |
| 132 |
</div> |
| 133 |
); |
| 134 |
}); |
| 135 |
}; |
| 136 |
|
| 137 |
return ( |
| 138 |
<> |
| 139 |
{isLoading && !isSorting && <LoadingOverlay spinnerSize="small" />} |
| 140 |
{title && <div className={styles.title}>{title}</div>} |
| 141 |
<div className={styles.table} {...rest}> |
| 142 |
<div className={classNames(styles.header, {[styles.headerStripped]: stripped})}>{getHeaderRow()}</div> |
| 143 |
{getRows()} |
| 144 |
</div> |
| 145 |
</> |
| 146 |
); |
| 147 |
}; |
| 148 |
|
| 149 |
Table.propTypes = { |
| 150 |
// Table title |
| 151 |
title: PropTypes.string, |
| 152 |
// Columns to display |
| 153 |
columns: PropTypes.array.isRequired, |
| 154 |
// Table data rows |
| 155 |
data: PropTypes.array.isRequired, |
| 156 |
// Column filters |
| 157 |
columnFilters: PropTypes.object, |
| 158 |
// Stripped rows |
| 159 |
stripped: PropTypes.bool, |
| 160 |
// Show spinner if data is loading |
| 161 |
isLoading: PropTypes.bool, |
| 162 |
}; |
| 163 |
|
| 164 |
export default Table; |
| 165 |
|