hooks
4 years ago
images
4 years ago
BulkActionCheckbox.tsx
4 years ago
BulkActionSelect.module.scss
4 years ago
BulkActionSelect.tsx
4 years ago
Filters.tsx
4 years ago
FormSelect.module.scss
4 years ago
FormSelect.tsx
4 years ago
Input.module.scss
4 years ago
Input.tsx
4 years ago
ListTable.module.scss
4 years ago
ListTable.tsx
4 years ago
ListTablePage.module.scss
3 years ago
ListTableRows.module.scss
3 years ago
ListTableRows.tsx
4 years ago
Pagination.module.scss
4 years ago
Pagination.tsx
4 years ago
README.MD
4 years ago
RowAction.module.scss
4 years ago
RowAction.tsx
4 years ago
Select.module.scss
4 years ago
Select.tsx
4 years ago
TableCell.module.scss
4 years ago
TableCell.tsx
4 years ago
TestLabel.module.scss
4 years ago
TestLabel.tsx
4 years ago
TypeBadge.module.scss
4 years ago
TypeBadge.tsx
4 years ago
api.ts
4 years ago
index.tsx
4 years ago
ListTable.tsx
241 lines
| 1 | import {useEffect, useRef, useState} from 'react'; |
| 2 | import {__, _n, sprintf} from '@wordpress/i18n'; |
| 3 | import cx from 'classnames'; |
| 4 | |
| 5 | import styles from './ListTable.module.scss'; |
| 6 | import ListTableRows from './ListTableRows'; |
| 7 | import {Spinner} from '../index'; |
| 8 | import {BulkActionCheckboxAll} from "@givewp/components/ListTable/BulkActionCheckbox"; |
| 9 | |
| 10 | export interface ListTableProps { |
| 11 | //required |
| 12 | columns: Array<ListTableColumn>; |
| 13 | title: string; |
| 14 | data: {items: Array<{}>}; |
| 15 | |
| 16 | //optional |
| 17 | pluralName?: string; |
| 18 | singleName?: string; |
| 19 | rowActions?: (({item, data, addRow, removeRow}) => JSX.Element)|JSX.Element|JSX.Element[]|Function|null; |
| 20 | parameters?: {}; |
| 21 | error?: {}|Boolean; |
| 22 | isLoading?: Boolean; |
| 23 | align?: 'start'|'center'|'end'; |
| 24 | } |
| 25 | |
| 26 | export interface ListTableColumn { |
| 27 | //required |
| 28 | name: string; |
| 29 | text: string; |
| 30 | |
| 31 | //optional |
| 32 | inlineSize?: string; |
| 33 | preset?: string; |
| 34 | heading?: boolean; |
| 35 | alignColumn?: 'start'|'center'|'end'; |
| 36 | addClass?: string; |
| 37 | render?: ((item: {}) => JSX.Element)|JSX.Element|JSX.Element[]|null; |
| 38 | } |
| 39 | |
| 40 | export const ListTable = ({ |
| 41 | columns, |
| 42 | singleName = __('item', 'give'), |
| 43 | pluralName = __('items', 'give'), |
| 44 | title, |
| 45 | data, |
| 46 | rowActions = null, |
| 47 | parameters = {}, |
| 48 | error = false, |
| 49 | isLoading = false, |
| 50 | align = 'start', |
| 51 | }: ListTableProps) => { |
| 52 | const [updateErrors, setUpdateErrors] = useState<{errors: Array<number>, successes: Array<number>}>({errors: [], successes: []}); |
| 53 | const [errorOverlay, setErrorOverlay] = useState<string|boolean>(false); |
| 54 | const [initialLoad, setInitialLoad] = useState<boolean>(true); |
| 55 | const [loadingOverlay, setLoadingOverlay] = useState<string|boolean>(false); |
| 56 | const [overlayWidth, setOverlayWidth] = useState(0); |
| 57 | const tableRef = useRef<null|HTMLTableElement>(); |
| 58 | const isEmpty = !error && data?.items.length === 0; |
| 59 | |
| 60 | useEffect(() => { |
| 61 | initialLoad && data && setInitialLoad(false); |
| 62 | }, [data]); |
| 63 | |
| 64 | useEffect(() => { |
| 65 | if (isLoading) { |
| 66 | // we need to set the overlay width in JS because tables only respect 'position: relative' in FireFox |
| 67 | if(tableRef.current){ |
| 68 | setOverlayWidth(tableRef.current.getBoundingClientRect().width); |
| 69 | } |
| 70 | setLoadingOverlay(styles.appear); |
| 71 | } |
| 72 | if (!isLoading && loadingOverlay) { |
| 73 | setLoadingOverlay(styles.disappear); |
| 74 | const timeoutId = setTimeout(() => setLoadingOverlay(false), 100); |
| 75 | return () => clearTimeout(timeoutId); |
| 76 | } |
| 77 | }, [isLoading]); |
| 78 | |
| 79 | useEffect(() => { |
| 80 | let timeoutId; |
| 81 | if (updateErrors.errors.length) { |
| 82 | setErrorOverlay(styles.appear); |
| 83 | timeoutId = setTimeout( |
| 84 | () => |
| 85 | document.getElementById(styles.updateError).scrollIntoView?.({behavior: 'smooth', block: 'center'}), |
| 86 | 100 |
| 87 | ); |
| 88 | } else if (errorOverlay) { |
| 89 | setErrorOverlay(styles.disappear); |
| 90 | timeoutId = setTimeout(() => setErrorOverlay(false), 100); |
| 91 | } |
| 92 | return () => clearTimeout(timeoutId); |
| 93 | }, [updateErrors.errors]); |
| 94 | |
| 95 | const clearUpdateErrors = () => { |
| 96 | setUpdateErrors({errors: [], successes: []}) |
| 97 | } |
| 98 | |
| 99 | return ( |
| 100 | <> |
| 101 | |
| 102 | {( initialLoad && !error ) ? ( |
| 103 | <div className={styles.initialLoad}> |
| 104 | <div |
| 105 | role="dialog" |
| 106 | aria-labelledby="giveListTableLoadingMessage" |
| 107 | className={cx(styles.tableGroup)} |
| 108 | > |
| 109 | <Spinner size={'large'} /> |
| 110 | <h2 id="giveListTableLoadingMessage"> |
| 111 | {sprintf(__('Loading %s', 'give'), pluralName)} |
| 112 | </h2> |
| 113 | </div> |
| 114 | </div> |
| 115 | ) : ( |
| 116 | <div |
| 117 | role="group" |
| 118 | aria-labelledby="giveListTableCaption" |
| 119 | aria-describedby="giveListTableMessage" |
| 120 | className={styles.tableGroup} |
| 121 | tabIndex={0} |
| 122 | > |
| 123 | {loadingOverlay && ( |
| 124 | <div className={cx(styles.overlay, loadingOverlay)} style={{width: overlayWidth && overlayWidth + 'px'}}> |
| 125 | <Spinner size={'medium'} /> |
| 126 | </div> |
| 127 | )} |
| 128 | <table ref={tableRef} className={styles.table}> |
| 129 | <caption id="giveListTableCaption" className={styles.tableCaption}> |
| 130 | {title} |
| 131 | </caption> |
| 132 | <thead> |
| 133 | <tr> |
| 134 | <th |
| 135 | scope="col" |
| 136 | aria-sort="none" |
| 137 | className={cx(styles.tableColumnHeader, styles.selectAll)} |
| 138 | data-column='select' |
| 139 | > |
| 140 | <BulkActionCheckboxAll pluralName={pluralName} data={data}/> |
| 141 | </th> |
| 142 | <> |
| 143 | {columns.map(column => |
| 144 | <th |
| 145 | scope="col" |
| 146 | aria-sort="none" |
| 147 | className={cx(styles.tableColumnHeader, |
| 148 | { |
| 149 | [styles[align]]: !column?.alignColumn, |
| 150 | [styles.center]: column?.alignColumn === 'center', |
| 151 | [styles.start]: column?.alignColumn === 'start', |
| 152 | } |
| 153 | )} |
| 154 | data-column={column.name} |
| 155 | key={column.name} |
| 156 | style={{inlineSize: (column?.inlineSize || '8rem')}} |
| 157 | > |
| 158 | {column.text} |
| 159 | </th> |
| 160 | )} |
| 161 | </> |
| 162 | </tr> |
| 163 | </thead> |
| 164 | <tbody className={styles.tableContent}> |
| 165 | <ListTableRows |
| 166 | columns={columns} |
| 167 | data={data} |
| 168 | isLoading={isLoading} |
| 169 | singleName={singleName} |
| 170 | rowActions={rowActions} |
| 171 | parameters={parameters} |
| 172 | setUpdateErrors={setUpdateErrors} |
| 173 | align={align} |
| 174 | /> |
| 175 | </tbody> |
| 176 | </table> |
| 177 | {errorOverlay && ( |
| 178 | <div className={cx(styles.overlay, errorOverlay)}> |
| 179 | <div |
| 180 | id={styles.updateError} |
| 181 | role="dialog" |
| 182 | aria-labelledby="giveListTableErrorMessage" |
| 183 | > |
| 184 | {Boolean(updateErrors.successes.length) && ( |
| 185 | <span> |
| 186 | {updateErrors.successes.length + ' ' + |
| 187 | // translators: |
| 188 | // Like '1 item was updated successfully' |
| 189 | // or '3 items were updated successfully' |
| 190 | _n( |
| 191 | sprintf('%s was updated successfully.', singleName), |
| 192 | sprintf('%s were updated successfully.', pluralName), |
| 193 | updateErrors.successes.length, |
| 194 | 'give' |
| 195 | ) |
| 196 | } |
| 197 | </span> |
| 198 | )} |
| 199 | <span id="giveListTableErrorMessage"> |
| 200 | {updateErrors.errors.length + |
| 201 | ' ' + |
| 202 | _n( |
| 203 | `${singleName} couldn't be updated.`, |
| 204 | `${pluralName} couldn't be updated.`, |
| 205 | updateErrors.errors.length, |
| 206 | 'give' |
| 207 | )} |
| 208 | </span> |
| 209 | <button |
| 210 | type="button" |
| 211 | className={cx('dashicons dashicons-dismiss', styles.dismiss)} |
| 212 | onClick={clearUpdateErrors} |
| 213 | > |
| 214 | <span className="give-visually-hidden">{__('dismiss', 'give')}</span> |
| 215 | </button> |
| 216 | </div> |
| 217 | </div> |
| 218 | )} |
| 219 | <div id="giveListTableMessage"> |
| 220 | {isEmpty && ( |
| 221 | <div role='status' className={styles.statusMessage}> |
| 222 | {sprintf(__('No %s found.', 'give'), pluralName)} |
| 223 | </div> |
| 224 | )} |
| 225 | {error && ( |
| 226 | <> |
| 227 | <div role='alert' className={styles.statusMessage}> |
| 228 | {sprintf(__('There was a problem retrieving the %s.', 'give'), pluralName)} |
| 229 | </div> |
| 230 | <div className={styles.statusMessage}> |
| 231 | <a href={window.location.href.toString()}>{__('Click here to reload the page.', 'give')}</a> |
| 232 | </div> |
| 233 | </> |
| 234 | )} |
| 235 | </div> |
| 236 | </div> |
| 237 | )} |
| 238 | </> |
| 239 | ); |
| 240 | } |
| 241 |