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