PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.9.0
GiveWP – Donation Plugin and Fundraising Platform v4.9.0
4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / src / Views / Components / ListTable / ListTablePage / index.tsx
give / src / Views / Components / ListTable / ListTablePage Last commit date
DeleteIcon.tsx 1 year ago ListTablePage.module.scss 10 months ago index.tsx 10 months ago
index.tsx
417 lines
1 import {createContext, useRef, useState, forwardRef, useImperativeHandle} from 'react';
2 import {__} from '@wordpress/i18n';
3 import {A11yDialog} from 'react-a11y-dialog';
4 import A11yDialogInstance from 'a11y-dialog';
5 import {GiveIcon} from '@givewp/components';
6 import {ListTable} from '../ListTable';
7 import Pagination from '../Pagination';
8 import {Filter, getInitialFilterState} from '../Filters';
9 import useDebounce from '../hooks/useDebounce';
10 import {useResetPage} from '../hooks/useResetPage';
11 import ListTableApi from '../api';
12 import styles from './ListTablePage.module.scss';
13 import cx from 'classnames';
14 import {BulkActionSelect} from '@givewp/components/ListTable/BulkActions/BulkActionSelect';
15 import ToggleSwitch from '@givewp/components/ListTable/ToggleSwitch';
16 import DeleteIcon from '@givewp/components/ListTable/ListTablePage/DeleteIcon';
17
18 export interface ListTablePageProps {
19 //required
20 title: string;
21 apiSettings: {apiRoot; apiNonce; table};
22
23 //optional
24 bulkActions?: Array<BulkActionsConfig> | null;
25 pluralName?: string;
26 singleName?: string;
27 children?: JSX.Element | JSX.Element[] | null;
28 rowActions?: JSX.Element | JSX.Element[] | Function | null;
29 filterSettings?;
30 align?: 'start' | 'center' | 'end';
31 paymentMode?: boolean;
32 listTableBlankSlate: JSX.Element;
33 productRecommendation?: JSX.Element;
34 columnFilters?: Array<ColumnFilterConfig>;
35 banner?: () => JSX.Element;
36 contentMode?: boolean;
37 perPage?: number;
38 }
39
40 export interface FilterConfig {
41 // required
42 name: string;
43 type: 'select' | 'formselect' | 'search' | 'checkbox' | 'hidden';
44
45 // optional
46 ariaLabel?: string;
47 inlineSize?: string;
48 text?: string;
49 options?: Array<{text: string; value: string}>;
50 }
51
52 export interface ColumnFilterConfig {
53 column: string;
54 filter: Function;
55 }
56
57 interface BulkActionsConfigBase {
58 //required
59 label: string;
60 value: string | number;
61 confirm: (
62 selected: Array<string | number>,
63 names?: Array<string>,
64 isOpen?: boolean,
65 setOpen?: (isOpen?: boolean) => void
66 ) => JSX.Element | JSX.Element[] | string;
67
68 //optional
69 isVisible?: (data: any, parameters: any) => boolean;
70 isIdSelectable?: (id: string, data: any) => boolean;
71 type?: 'normal' | 'warning' | 'danger' | 'custom';
72 }
73
74 // Makes the "action" property required for the standard types
75 interface BulkActionsConfigWithAction extends BulkActionsConfigBase {
76 type: 'normal' | 'warning' | 'danger';
77 action: (selected: Array<string | number>) => Promise<{errors: string | number; successes: string | number}>;
78 }
79
80 // Makes the "action" property required for the undefined type
81 interface BulkActionsConfigWithoutType extends BulkActionsConfigBase {
82 type?: undefined;
83 action: (selected: Array<string | number>) => Promise<{errors: string | number; successes: string | number}>;
84 }
85
86 // Makes the "action" property forbidden for the custom type
87 export interface BulkActionsConfigWithoutAction extends BulkActionsConfigBase {
88 type: 'custom';
89 }
90
91 export type BulkActionsConfig =
92 | BulkActionsConfigWithAction
93 | BulkActionsConfigWithoutType
94 | BulkActionsConfigWithoutAction;
95
96 export const ShowConfirmModalContext = createContext(
97 (label, confirm, action, type = null, confirmButtonText = __('Confirm', 'give')) => {}
98 );
99 export const CheckboxContext = createContext(null);
100
101 export interface ListTablePageRef {
102 refresh: () => Promise<any>;
103 }
104
105 const ListTablePage = forwardRef<ListTablePageRef, ListTablePageProps>(({
106 title,
107 apiSettings,
108 bulkActions = null,
109 filterSettings = [],
110 singleName = __('item', 'give'),
111 pluralName = __('items', 'give'),
112 rowActions = null,
113 children = null,
114 align = 'start',
115 paymentMode,
116 listTableBlankSlate,
117 productRecommendation,
118 columnFilters = [],
119 banner,
120 contentMode,
121 perPage = 30,
122 }: ListTablePageProps, ref) => {
123 const [page, setPage] = useState<number>(1);
124 const [filters, setFilters] = useState(getInitialFilterState(filterSettings));
125 const [isOpen, setOpen] = useState(false);
126 const [modalContent, setModalContent] = useState<{
127 confirm;
128 action?;
129 label;
130 confirmButtonText?: string;
131 type?: 'normal' | 'warning' | 'danger' | 'custom';
132 }>({
133 confirm: (selected) => {},
134 action: (selected) => {},
135 label: '',
136 confirmButtonText: '',
137 });
138 const [selectedAction, setSelectedAction] = useState<string>('');
139 const [selectedIds, setSelectedIds] = useState([]);
140 const [selectedNames, setSelectedNames] = useState([]);
141 const dialog = useRef() as {current: A11yDialogInstance};
142 const checkboxRefs = useRef([]);
143 const [sortField, setSortField] = useState<{sortColumn: string; sortDirection: string}>({
144 sortColumn: 'id',
145 sortDirection: 'desc',
146 });
147 const [testMode, setTestMode] = useState(paymentMode);
148
149 const {sortColumn, sortDirection} = sortField;
150 const locale = navigator.language || navigator.languages[0];
151 const testModeFilter = filterSettings.find((filter) => filter.name === 'toggle');
152
153 const parameters = {
154 page,
155 perPage,
156 sortColumn,
157 sortDirection,
158 locale,
159 testMode,
160 ...filters,
161 };
162
163 const archiveApi = useRef(new ListTableApi(apiSettings)).current;
164
165 const {data, error, isValidating, mutate} = archiveApi.useListTable(parameters);
166
167 useResetPage(data, page, setPage, filters);
168
169 useImperativeHandle(ref, () => ({
170 refresh: () => mutate()
171 }), [mutate]);
172
173 const handleFilterChange = (name, value) => {
174 setFilters((prevState) => ({...prevState, [name]: value}));
175 };
176
177 const handleDebouncedFilterChange = useDebounce(handleFilterChange);
178
179 const showConfirmActionModal = (
180 label,
181 confirm,
182 action,
183 type?: 'normal' | 'warning' | 'danger' | 'custom' | null,
184 confirmButtonText?: string
185 ) => {
186 setModalContent({label, confirm, action, type, confirmButtonText});
187 dialog.current.show();
188 };
189
190 const openBulkActionModal = (event) => {
191 event.preventDefault();
192
193 if (window.GiveDonations && window.GiveDonations.addonsBulkActions) {
194 bulkActions = [...bulkActions, ...window.GiveDonations.addonsBulkActions];
195 }
196
197 const bulkAction = bulkActions.find((config) => selectedAction === config.value);
198
199 if (!bulkAction) return;
200
201 const selected = [];
202 const names = [];
203 const selectedRefs = checkboxRefs.current.filter((checkbox) => {
204 const isSelectable = bulkAction?.isIdSelectable?.(checkbox.dataset.id, data) ?? true;
205 return checkbox.checked && isSelectable;
206 });
207 selectedRefs.forEach((checkbox) => {
208 selected.push(checkbox.dataset.id);
209 names.push(checkbox.dataset.name);
210 });
211 setSelectedIds(selected);
212 setSelectedNames(names);
213 if (selected.length) {
214 setModalContent({...bulkAction});
215 if ('custom' === bulkAction.type) {
216 setOpen(true);
217 bulkAction?.confirm(selected, names, isOpen, setOpen);
218 } else {
219 dialog.current.show();
220 }
221 }
222 };
223
224 const setSortDirectionForColumn = (column, direction) => {
225 setSortField((previousState) => {
226 return {
227 ...previousState,
228 sortColumn: column,
229 sortDirection: direction,
230 };
231 });
232 };
233
234 const showPagination = () => (
235 <Pagination
236 currentPage={page}
237 totalPages={data ? data.totalPages : 1}
238 disabled={!data}
239 totalItems={data ? parseInt(data.totalItems) : -1}
240 setPage={setPage}
241 singleName={singleName}
242 pluralName={pluralName}
243 />
244 );
245
246 const PageActions = ({PageActionsTop}: {PageActionsTop?: boolean}) => {
247 return (
248 <div className={cx(styles.pageActions, {[styles.alignEnd]: !bulkActions})}>
249 <BulkActionSelect
250 selectedState={[selectedAction, setSelectedAction]}
251 parameters={parameters}
252 data={data}
253 bulkActions={bulkActions}
254 showModal={openBulkActionModal}
255 />
256 {PageActionsTop && testModeFilter && <TestModeFilter />}
257 {!PageActionsTop && page && setPage && showPagination()}
258 </div>
259 );
260 };
261
262 const TestModeFilter = () => (
263 <ToggleSwitch ariaLabel={testModeFilter?.ariaLabel} onChange={setTestMode} checked={testMode} />
264 );
265
266 const TestModeBadge = () => <span>{testModeFilter?.text}</span>;
267
268 return (
269 <>
270 <article className={styles.page}>
271 {contentMode ? (
272 <>
273 <section role="search" className={styles.searchContainer}>
274 <div className={styles.flexRow}>
275 <PageActions PageActionsTop />
276 </div>
277 <div className={styles.flexRow}>
278 {filterSettings.map((filter) => (
279 <Filter
280 key={filter.name}
281 value={filters[filter.name]}
282 filter={filter}
283 onChange={handleFilterChange}
284 debouncedOnChange={handleDebouncedFilterChange}
285 />
286 ))}
287 </div>
288 </section>
289 </>
290 ) : (
291 <>
292 <header className={styles.pageHeader}>
293 <div className={styles.flexRow}>
294 <GiveIcon size={'1.875rem'} />
295 <h1 className={styles.pageTitle}>{title}</h1>
296 {testModeFilter && testMode && <TestModeBadge />}
297 </div>
298 {children && <div className={styles.flexRow}>{children}</div>}
299 </header>
300 {banner && <section role="banner">{banner()}</section>}
301 <section role="search" className={styles.searchContainer}>
302 <div className={styles.flexRow}>
303 <PageActions PageActionsTop />
304 </div>
305 <div className={styles.flexRow}>
306 {filterSettings.map((filter) => (
307 <Filter
308 key={filter.name}
309 value={filters[filter.name]}
310 filter={filter}
311 onChange={handleFilterChange}
312 debouncedOnChange={handleDebouncedFilterChange}
313 />
314 ))}
315 </div>
316 </section>
317 </>
318 )}
319
320 <div className={cx('wp-header-end', 'hidden')} />
321 <div className={styles.pageContent}>
322 {contentMode && children ? <>{children}</> : <></>}
323 <CheckboxContext.Provider value={checkboxRefs}>
324 <ShowConfirmModalContext.Provider value={showConfirmActionModal}>
325 <ListTable
326 apiSettings={apiSettings}
327 sortField={sortField}
328 setSortDirectionForColumn={setSortDirectionForColumn}
329 singleName={singleName}
330 pluralName={pluralName}
331 title={title}
332 rowActions={rowActions}
333 parameters={parameters}
334 data={data}
335 error={error}
336 isLoading={isValidating}
337 align={align}
338 testMode={testMode}
339 listTableBlankSlate={listTableBlankSlate}
340 productRecommendation={productRecommendation}
341 columnFilters={columnFilters}
342 includeBulkActionsCheckbox={bulkActions?.length > 0}
343 />
344 </ShowConfirmModalContext.Provider>
345 </CheckboxContext.Provider>
346 <PageActions />
347 </div>
348 </article>
349 <A11yDialog
350 id="giveListTableModal"
351 dialogRef={(instance) => (dialog.current = instance)}
352 title={
353 <>
354 {modalContent?.type === 'danger' && <DeleteIcon />}
355 {modalContent?.label}
356 </>
357 }
358 titleId={styles.modalTitle}
359 classNames={{
360 container: styles.container,
361 overlay: styles.overlay,
362 dialog: cx(styles.dialog, {
363 [styles.warning]: modalContent?.type === 'warning',
364 [styles.danger]: modalContent?.type === 'danger',
365 }),
366 closeButton: 'hidden',
367 }}
368 >
369 <div className={styles.modalContent}>
370 {modalContent?.confirm(selectedIds, selectedNames, isOpen, setOpen) || null}
371 </div>
372 <div className={styles.gutter}>
373 <button id={styles.cancel} onClick={(event) => dialog.current?.hide()}>
374 {__('Cancel', 'give')}
375 </button>
376 <button
377 id={styles.confirm}
378 onClick={async (event) => {
379 dialog.current?.hide();
380 try {
381 await modalContent.action(selectedIds);
382 await mutate();
383 } catch (error) {
384 console.error('Bulk action error:', error);
385
386 // Create a user-friendly error message
387 let errorMessage = __('An error occurred while performing this action.', 'give');
388
389 if (error.message && error.message.includes('permission')) {
390 errorMessage = __('You don\'t have permission to perform this action.', 'give');
391 } else if (error.message && error.message.includes('403')) {
392 errorMessage = __('Access denied. You don\'t have permission to perform this action.', 'give');
393 } else if (error.message) {
394 // Try to extract a meaningful message from the error
395 const match = error.message.match(/You don't have permission[^"]*|You don&#039;t have permission[^"]*/i);
396 if (match) {
397 errorMessage = match[0].replace(/&#039;/g, "'");
398 }
399 }
400
401 // Show error as a notice/alert
402 alert(errorMessage);
403 }
404 }}
405 >
406 {modalContent?.confirmButtonText ?? __('Confirm', 'give')}
407 </button>
408 </div>
409 </A11yDialog>
410 </>
411 );
412 });
413
414 ListTablePage.displayName = 'ListTablePage';
415
416 export default ListTablePage;
417