PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.13.0
GiveWP – Donation Plugin and Fundraising Platform v4.13.0
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 9 months ago index.tsx 8 months ago
index.tsx
459 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 import ListTableStats, { StatConfig } from '../ListTableStats/ListTableStats';
18 import FilterBy from '../FilterBy';
19
20 export interface ListTablePageProps {
21 //required
22 title: string;
23 apiSettings: {apiRoot; apiNonce; table};
24
25 //optional
26 bulkActions?: Array<BulkActionsConfig> | null;
27 pluralName?: string;
28 singleName?: string;
29 children?: JSX.Element | JSX.Element[] | null;
30 rowActions?: JSX.Element | JSX.Element[] | Function | null;
31 filterSettings?;
32 align?: 'start' | 'center' | 'end';
33 paymentMode?: boolean;
34 listTableBlankSlate: JSX.Element;
35 productRecommendation?: JSX.Element;
36 columnFilters?: Array<ColumnFilterConfig>;
37 banner?: () => JSX.Element;
38 contentMode?: boolean;
39 perPage?: number;
40 statsConfig?: Record<string, StatConfig>;
41 }
42
43 interface FilterConfigBase {
44 // required
45 name: string;
46
47 // optional
48 ariaLabel?: string;
49 inlineSize?: string;
50 text?: string;
51 }
52
53 interface FilterConfigWithSimpleOptions extends FilterConfigBase {
54 type: 'select' | 'campaignselect' | 'search' | 'checkbox' | 'hidden';
55 options?: Array<{text: string; value: string}>;
56 }
57
58 export interface FilterByGroupedOptions {
59 id: string;
60 apiParam: string;
61 name: string;
62 type: 'checkbox' | 'radio' | 'toggle';
63 options: Array<{text: string; value: string}>;
64 defaultValue?: string | string[];
65 isVisible?: (values: Record<string, string[]>) => boolean;
66 showTitle?: boolean;
67 }
68
69 interface FilterConfigWithGroupedOptions extends FilterConfigBase {
70 type: 'filterby';
71 groupedOptions?: Array<FilterByGroupedOptions>;
72 }
73
74 export type FilterConfig = FilterConfigWithSimpleOptions | FilterConfigWithGroupedOptions;
75
76 export interface ColumnFilterConfig {
77 column: string;
78 filter: Function;
79 }
80
81 interface BulkActionsConfigBase {
82 //required
83 label: string;
84 value: string | number;
85 confirm: (
86 selected: Array<string | number>,
87 names?: Array<string>,
88 isOpen?: boolean,
89 setOpen?: (isOpen?: boolean) => void
90 ) => JSX.Element | JSX.Element[] | string;
91
92 //optional
93 isVisible?: (data: any, parameters: any) => boolean;
94 isIdSelectable?: (id: string, data: any) => boolean;
95 type?: 'normal' | 'warning' | 'danger' | 'custom';
96 }
97
98 // Makes the "action" property required for the standard types
99 interface BulkActionsConfigWithAction extends BulkActionsConfigBase {
100 type: 'normal' | 'warning' | 'danger';
101 action: (selected: Array<string | number>) => Promise<{errors: string | number; successes: string | number}>;
102 }
103
104 // Makes the "action" property required for the undefined type
105 interface BulkActionsConfigWithoutType extends BulkActionsConfigBase {
106 type?: undefined;
107 action: (selected: Array<string | number>) => Promise<{errors: string | number; successes: string | number}>;
108 }
109
110 // Makes the "action" property forbidden for the custom type
111 export interface BulkActionsConfigWithoutAction extends BulkActionsConfigBase {
112 type: 'custom';
113 }
114
115 export type BulkActionsConfig =
116 | BulkActionsConfigWithAction
117 | BulkActionsConfigWithoutType
118 | BulkActionsConfigWithoutAction;
119
120 export const ShowConfirmModalContext = createContext(
121 (label, confirm, action, type = null, confirmButtonText = __('Confirm', 'give')) => {}
122 );
123 export const CheckboxContext = createContext(null);
124
125 export interface ListTablePageRef {
126 refresh: () => Promise<any>;
127 }
128
129 const ListTablePage = forwardRef<ListTablePageRef, ListTablePageProps>(({
130 title,
131 apiSettings,
132 bulkActions = null,
133 filterSettings = [],
134 singleName = __('item', 'give'),
135 pluralName = __('items', 'give'),
136 rowActions = null,
137 children = null,
138 align = 'start',
139 paymentMode,
140 listTableBlankSlate,
141 productRecommendation,
142 columnFilters = [],
143 banner,
144 contentMode,
145 perPage = 30,
146 statsConfig,
147 }: ListTablePageProps, ref) => {
148 const [page, setPage] = useState<number>(1);
149 const [filters, setFilters] = useState(getInitialFilterState(filterSettings));
150 const [isOpen, setOpen] = useState(false);
151 const [modalContent, setModalContent] = useState<{
152 confirm;
153 action?;
154 label;
155 confirmButtonText?: string;
156 type?: 'normal' | 'warning' | 'danger' | 'custom';
157 }>({
158 confirm: (selected) => {},
159 action: (selected) => {},
160 label: '',
161 confirmButtonText: '',
162 });
163 const [selectedAction, setSelectedAction] = useState<string>('');
164 const [selectedIds, setSelectedIds] = useState([]);
165 const [selectedNames, setSelectedNames] = useState([]);
166 const dialog = useRef() as {current: A11yDialogInstance};
167 const checkboxRefs = useRef([]);
168 const [sortField, setSortField] = useState<{sortColumn: string; sortDirection: string}>({
169 sortColumn: 'id',
170 sortDirection: 'desc',
171 });
172 const [testMode, setTestMode] = useState(paymentMode);
173
174 const {sortColumn, sortDirection} = sortField;
175 const locale = navigator.language || navigator.languages[0];
176 const testModeFilter = filterSettings.find((filter) => filter.name === 'toggle');
177
178 const parameters = {
179 page,
180 perPage,
181 sortColumn,
182 sortDirection,
183 locale,
184 testMode,
185 ...filters,
186 };
187
188 const archiveApi = useRef(new ListTableApi(apiSettings)).current;
189
190 const {data, error, isValidating, mutate} = archiveApi.useListTable(parameters);
191 const {data: statsData, error: statsError, isValidating: statsIsValidating, mutate: mutateStats} = statsConfig ? archiveApi.useStats(testMode) : {data: null, error: null, isValidating: false, mutate: async () => {}};
192
193 useResetPage(data, page, setPage, filters);
194
195 useImperativeHandle(ref, () => ({
196 refresh: async () => {
197 await mutate();
198 statsConfig && await mutateStats();
199 }
200 }), [mutate, mutateStats, statsConfig]);
201
202 const handleFilterChange = (name, value) => {
203 setFilters((prevState) => {
204 if (!value || (Array.isArray(value) && value.length === 0)) {
205 const {[name]: _, ...rest} = prevState as Record<string, string[]>;
206 return rest;
207 }
208 return {...prevState, [name]: value};
209 });
210 };
211
212 const handleDebouncedFilterChange = useDebounce(handleFilterChange);
213
214 const showConfirmActionModal = (
215 label,
216 confirm,
217 action,
218 type?: 'normal' | 'warning' | 'danger' | 'custom' | null,
219 confirmButtonText?: string
220 ) => {
221 setModalContent({label, confirm, action, type, confirmButtonText});
222 dialog.current.show();
223 };
224
225 const openBulkActionModal = (event) => {
226 event.preventDefault();
227
228 if (window.GiveDonations && window.GiveDonations.addonsBulkActions) {
229 bulkActions = [...bulkActions, ...window.GiveDonations.addonsBulkActions];
230 }
231
232 const bulkAction = bulkActions.find((config) => selectedAction === config.value);
233
234 if (!bulkAction) return;
235
236 const selected = [];
237 const names = [];
238 const selectedRefs = checkboxRefs.current.filter((checkbox) => {
239 const isSelectable = bulkAction?.isIdSelectable?.(checkbox.dataset.id, data) ?? true;
240 return checkbox.checked && isSelectable;
241 });
242 selectedRefs.forEach((checkbox) => {
243 selected.push(checkbox.dataset.id);
244 names.push(checkbox.dataset.name);
245 });
246 setSelectedIds(selected);
247 setSelectedNames(names);
248 if (selected.length) {
249 setModalContent({...bulkAction});
250 if ('custom' === bulkAction.type) {
251 setOpen(true);
252 bulkAction?.confirm(selected, names, isOpen, setOpen);
253 } else {
254 dialog.current.show();
255 }
256 }
257 };
258
259 const setSortDirectionForColumn = (column, direction) => {
260 setSortField((previousState) => {
261 return {
262 ...previousState,
263 sortColumn: column,
264 sortDirection: direction,
265 };
266 });
267 };
268
269 const showPagination = () => (
270 <Pagination
271 currentPage={page}
272 totalPages={data ? data.totalPages : 1}
273 disabled={!data}
274 totalItems={data ? parseInt(data.totalItems) : -1}
275 setPage={setPage}
276 singleName={__('result', 'give')}
277 pluralName={__('results', 'give')}
278 />
279 );
280
281 const PageActions = ({PageActionsTop}: {PageActionsTop?: boolean}) => {
282 return (
283 <div className={cx(styles.pageActions, {[styles.alignEnd]: !bulkActions})}>
284 {PageActionsTop ? (
285 <BulkActionSelect
286 selectedState={[selectedAction, setSelectedAction]}
287 parameters={parameters}
288 data={data}
289 bulkActions={bulkActions}
290 showModal={openBulkActionModal}
291 />
292 ) : (
293 <>
294 {page && setPage && showPagination()}
295 </>
296 )}
297 </div>
298 );
299 };
300
301 const TestModeFilter = () => (
302 <ToggleSwitch ariaLabel={testModeFilter?.ariaLabel} onChange={setTestMode} checked={testMode} />
303 );
304
305 const TestModeBadge = () => <span className={styles.testModeBadge}>{testModeFilter?.text}</span>;
306
307 const SearchSection = () => (
308 <section role="search" className={styles.searchContainer}>
309 <div className={styles.flexRow}>
310 <PageActions PageActionsTop />
311 </div>
312 <div className={styles.flexRow}>
313 {filterSettings.map((filter) => (
314 filter.type === 'filterby' ? (
315 <FilterBy
316 key={filter.name}
317 groupedOptions={filter.groupedOptions}
318 onChange={handleFilterChange}
319 values={filters}
320 />
321 ) : (
322 <Filter
323 key={filter.name}
324 value={filters[filter.name]}
325 filter={filter}
326 onChange={handleFilterChange}
327 debouncedOnChange={handleDebouncedFilterChange}
328 />
329 )
330 ))}
331 </div>
332 </section>
333 );
334
335 return (
336 <>
337 <article className={styles.page}>
338 {!contentMode && (
339 <>
340 <header className={styles.pageHeader}>
341 <div className={styles.flexRow}>
342 <GiveIcon size={'2.25rem'} />
343 <h1 className={styles.pageTitle}>{title}</h1>
344 {testModeFilter && testMode && <TestModeBadge />}
345 </div>
346 {children && <div className={styles.flexRow}>{children}</div>}
347 </header>
348
349 <div className={cx('wp-header-end', 'hidden')} />
350
351 {banner && <section role="banner">{banner()}</section>}
352 {testModeFilter && (
353 <div className={styles.filtersRow}>
354 <TestModeFilter />
355 </div>
356 )}
357 {statsConfig && !statsIsValidating && <ListTableStats config={statsConfig} values={statsData} />}
358 </>
359 )}
360
361 <div className={styles.pageContent}>
362 <SearchSection />
363 {contentMode && children ? <>{children}</> : <></>}
364 <CheckboxContext.Provider value={checkboxRefs}>
365 <ShowConfirmModalContext.Provider value={showConfirmActionModal}>
366 <ListTable
367 apiSettings={apiSettings}
368 sortField={sortField}
369 setSortDirectionForColumn={setSortDirectionForColumn}
370 singleName={singleName}
371 pluralName={pluralName}
372 title={title}
373 rowActions={rowActions}
374 parameters={parameters}
375 data={data}
376 error={error}
377 isLoading={isValidating}
378 align={align}
379 testMode={testMode}
380 listTableBlankSlate={listTableBlankSlate}
381 productRecommendation={productRecommendation}
382 columnFilters={columnFilters}
383 includeBulkActionsCheckbox={bulkActions?.length > 0}
384 />
385 </ShowConfirmModalContext.Provider>
386 </CheckboxContext.Provider>
387 <PageActions />
388 </div>
389 </article>
390 <A11yDialog
391 id="giveListTableModal"
392 dialogRef={(instance) => (dialog.current = instance)}
393 title={
394 <>
395 {modalContent?.type === 'danger' && <DeleteIcon />}
396 {modalContent?.label}
397 </>
398 }
399 titleId={styles.modalTitle}
400 classNames={{
401 container: styles.container,
402 overlay: styles.overlay,
403 dialog: cx(styles.dialog, {
404 [styles.warning]: modalContent?.type === 'warning',
405 [styles.danger]: modalContent?.type === 'danger',
406 }),
407 closeButton: 'hidden',
408 }}
409 >
410 <div className={styles.modalContent}>
411 {modalContent?.confirm(selectedIds, selectedNames, isOpen, setOpen) || null}
412 </div>
413 <div className={styles.gutter}>
414 <button id={styles.cancel} onClick={(event) => dialog.current?.hide()}>
415 {__('Cancel', 'give')}
416 </button>
417 <button
418 id={styles.confirm}
419 onClick={async (event) => {
420 dialog.current?.hide();
421 try {
422 await modalContent.action(selectedIds);
423 await mutate();
424 await mutateStats();
425 } catch (error) {
426 console.error('Bulk action error:', error);
427
428 // Create a user-friendly error message
429 let errorMessage = __('An error occurred while performing this action.', 'give');
430
431 if (error.message && error.message.includes('permission')) {
432 errorMessage = __('You don\'t have permission to perform this action.', 'give');
433 } else if (error.message && error.message.includes('403')) {
434 errorMessage = __('Access denied. You don\'t have permission to perform this action.', 'give');
435 } else if (error.message) {
436 // Try to extract a meaningful message from the error
437 const match = error.message.match(/You don't have permission[^"]*|You don&#039;t have permission[^"]*/i);
438 if (match) {
439 errorMessage = match[0].replace(/&#039;/g, "'");
440 }
441 }
442
443 // Show error as a notice/alert
444 alert(errorMessage);
445 }
446 }}
447 >
448 {modalContent?.confirmButtonText ?? __('Confirm', 'give')}
449 </button>
450 </div>
451 </A11yDialog>
452 </>
453 );
454 });
455
456 ListTablePage.displayName = 'ListTablePage';
457
458 export default ListTablePage;
459