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