Sid Gifari File Manager
🏠 Root
/
home2
/
nuveprin
/
ahavahome.org
/
wp-content
/
plugins
/
give
/
src
/
Views
/
Components
/
ListTable
/
ListTablePage
/
Editing: index.tsx
import {createContext, useRef, useState, forwardRef, useImperativeHandle} from 'react'; import {__} from '@wordpress/i18n'; import {A11yDialog} from 'react-a11y-dialog'; import A11yDialogInstance from 'a11y-dialog'; import {GiveIcon} from '@givewp/components'; import {ListTable} from '../ListTable'; import Pagination from '../Pagination'; import {Filter, getInitialFilterState} from '../Filters'; import useDebounce from '../hooks/useDebounce'; import {useResetPage} from '../hooks/useResetPage'; import ListTableApi from '../api'; import styles from './ListTablePage.module.scss'; import cx from 'classnames'; import {BulkActionSelect} from '@givewp/components/ListTable/BulkActions/BulkActionSelect'; import ToggleSwitch from '@givewp/components/ListTable/ToggleSwitch'; import DeleteIcon from '@givewp/components/ListTable/ListTablePage/DeleteIcon'; import ListTableStats, { StatConfig } from '../ListTableStats/ListTableStats'; import FilterBy from '../FilterBy'; export interface ListTablePageProps { //required title: string; apiSettings: {apiRoot; apiNonce; table}; //optional bulkActions?: Array<BulkActionsConfig> | null; pluralName?: string; singleName?: string; children?: JSX.Element | JSX.Element[] | null; rowActions?: JSX.Element | JSX.Element[] | Function | null; filterSettings?; align?: 'start' | 'center' | 'end'; paymentMode?: boolean; listTableBlankSlate: JSX.Element; productRecommendation?: JSX.Element; columnFilters?: Array<ColumnFilterConfig>; banner?: () => JSX.Element; contentMode?: boolean; perPage?: number; statsConfig?: Record<string, StatConfig>; } interface FilterConfigBase { // required name: string; // optional ariaLabel?: string; inlineSize?: string; text?: string; } interface FilterConfigWithSimpleOptions extends FilterConfigBase { type: 'select' | 'campaignselect' | 'search' | 'checkbox' | 'hidden'; options?: Array<{text: string; value: string}>; } export interface FilterByGroupedOptions { id: string; apiParam: string; name: string; type: 'checkbox' | 'radio' | 'toggle'; options: Array<{text: string; value: string}>; defaultValue?: string | string[]; isVisible?: (values: Record<string, string[]>) => boolean; showTitle?: boolean; } interface FilterConfigWithGroupedOptions extends FilterConfigBase { type: 'filterby'; groupedOptions?: Array<FilterByGroupedOptions>; } export type FilterConfig = FilterConfigWithSimpleOptions | FilterConfigWithGroupedOptions; export interface ColumnFilterConfig { column: string; filter: Function; } interface BulkActionsConfigBase { //required label: string; value: string | number; confirm: ( selected: Array<string | number>, names?: Array<string>, isOpen?: boolean, setOpen?: (isOpen?: boolean) => void ) => JSX.Element | JSX.Element[] | string; //optional isVisible?: (data: any, parameters: any) => boolean; isIdSelectable?: (id: string, data: any) => boolean; type?: 'normal' | 'warning' | 'danger' | 'custom'; } // Makes the "action" property required for the standard types interface BulkActionsConfigWithAction extends BulkActionsConfigBase { type: 'normal' | 'warning' | 'danger'; action: (selected: Array<string | number>) => Promise<{errors: string | number; successes: string | number}>; } // Makes the "action" property required for the undefined type interface BulkActionsConfigWithoutType extends BulkActionsConfigBase { type?: undefined; action: (selected: Array<string | number>) => Promise<{errors: string | number; successes: string | number}>; } // Makes the "action" property forbidden for the custom type export interface BulkActionsConfigWithoutAction extends BulkActionsConfigBase { type: 'custom'; } export type BulkActionsConfig = | BulkActionsConfigWithAction | BulkActionsConfigWithoutType | BulkActionsConfigWithoutAction; export const ShowConfirmModalContext = createContext( (label, confirm, action, type = null, confirmButtonText = __('Confirm', 'give')) => {} ); export const CheckboxContext = createContext(null); export interface ListTablePageRef { refresh: () => Promise<any>; } const ListTablePage = forwardRef<ListTablePageRef, ListTablePageProps>(({ title, apiSettings, bulkActions = null, filterSettings = [], singleName = __('item', 'give'), pluralName = __('items', 'give'), rowActions = null, children = null, align = 'start', paymentMode, listTableBlankSlate, productRecommendation, columnFilters = [], banner, contentMode, perPage = 30, statsConfig, }: ListTablePageProps, ref) => { const [page, setPage] = useState<number>(1); const [filters, setFilters] = useState(getInitialFilterState(filterSettings)); const [isOpen, setOpen] = useState(false); const [modalContent, setModalContent] = useState<{ confirm; action?; label; confirmButtonText?: string; type?: 'normal' | 'warning' | 'danger' | 'custom'; }>({ confirm: (selected) => {}, action: (selected) => {}, label: '', confirmButtonText: '', }); const [selectedAction, setSelectedAction] = useState<string>(''); const [selectedIds, setSelectedIds] = useState([]); const [selectedNames, setSelectedNames] = useState([]); const dialog = useRef() as {current: A11yDialogInstance}; const checkboxRefs = useRef([]); const [sortField, setSortField] = useState<{sortColumn: string; sortDirection: string}>({ sortColumn: 'id', sortDirection: 'desc', }); const [testMode, setTestMode] = useState(paymentMode); const {sortColumn, sortDirection} = sortField; const locale = navigator.language || navigator.languages[0]; const testModeFilter = filterSettings.find((filter) => filter.name === 'toggle'); const parameters = { page, perPage, sortColumn, sortDirection, locale, testMode, ...filters, }; const archiveApi = useRef(new ListTableApi(apiSettings)).current; const {data, error, isValidating, mutate} = archiveApi.useListTable(parameters); const {data: statsData, error: statsError, isValidating: statsIsValidating, mutate: mutateStats} = statsConfig ? archiveApi.useStats(testMode) : {data: null, error: null, isValidating: false, mutate: async () => {}}; useResetPage(data, page, setPage, filters); useImperativeHandle(ref, () => ({ refresh: async () => { await mutate(); statsConfig && await mutateStats(); } }), [mutate, mutateStats, statsConfig]); const handleFilterChange = (name, value) => { setFilters((prevState) => { if (!value || (Array.isArray(value) && value.length === 0)) { const {[name]: _, ...rest} = prevState as Record<string, string[]>; return rest; } return {...prevState, [name]: value}; }); }; const handleDebouncedFilterChange = useDebounce(handleFilterChange); const showConfirmActionModal = ( label, confirm, action, type?: 'normal' | 'warning' | 'danger' | 'custom' | null, confirmButtonText?: string ) => { setModalContent({label, confirm, action, type, confirmButtonText}); dialog.current.show(); }; const openBulkActionModal = (event) => { event.preventDefault(); if (window.GiveDonations && window.GiveDonations.addonsBulkActions) { bulkActions = [...bulkActions, ...window.GiveDonations.addonsBulkActions]; } const bulkAction = bulkActions.find((config) => selectedAction === config.value); if (!bulkAction) return; const selected = []; const names = []; const selectedRefs = checkboxRefs.current.filter((checkbox) => { const isSelectable = bulkAction?.isIdSelectable?.(checkbox.dataset.id, data) ?? true; return checkbox.checked && isSelectable; }); selectedRefs.forEach((checkbox) => { selected.push(checkbox.dataset.id); names.push(checkbox.dataset.name); }); setSelectedIds(selected); setSelectedNames(names); if (selected.length) { setModalContent({...bulkAction}); if ('custom' === bulkAction.type) { setOpen(true); bulkAction?.confirm(selected, names, isOpen, setOpen); } else { dialog.current.show(); } } }; const setSortDirectionForColumn = (column, direction) => { setSortField((previousState) => { return { ...previousState, sortColumn: column, sortDirection: direction, }; }); }; const showPagination = () => ( <Pagination currentPage={page} totalPages={data ? data.totalPages : 1} disabled={!data} totalItems={data ? parseInt(data.totalItems) : -1} setPage={setPage} singleName={__('result', 'give')} pluralName={__('results', 'give')} /> ); const PageActions = ({PageActionsTop}: {PageActionsTop?: boolean}) => { return ( <div className={cx(styles.pageActions, {[styles.alignEnd]: !bulkActions})}> {PageActionsTop ? ( <BulkActionSelect selectedState={[selectedAction, setSelectedAction]} parameters={parameters} data={data} bulkActions={bulkActions} showModal={openBulkActionModal} /> ) : ( <> {page && setPage && showPagination()} </> )} </div> ); }; const TestModeFilter = () => ( <ToggleSwitch ariaLabel={testModeFilter?.ariaLabel} onChange={setTestMode} checked={testMode} /> ); const TestModeBadge = () => <span className={styles.testModeBadge}>{testModeFilter?.text}</span>; const SearchSection = () => ( <section role="search" className={styles.searchContainer}> <div className={styles.flexRow}> <PageActions PageActionsTop /> </div> <div className={styles.flexRow}> {filterSettings.map((filter) => ( filter.type === 'filterby' ? ( <FilterBy key={filter.name} groupedOptions={filter.groupedOptions} onChange={handleFilterChange} values={filters} /> ) : ( <Filter key={filter.name} value={filters[filter.name]} filter={filter} onChange={handleFilterChange} debouncedOnChange={handleDebouncedFilterChange} /> ) ))} </div> </section> ); return ( <> <article className={styles.page}> {!contentMode && ( <> <header className={styles.pageHeader}> <div className={styles.flexRow}> <GiveIcon size={'2.25rem'} /> <h1 className={styles.pageTitle}>{title}</h1> {testModeFilter && testMode && <TestModeBadge />} </div> {children && <div className={styles.flexRow}>{children}</div>} </header> <div className={cx('wp-header-end', 'hidden')} /> {banner && <section role="banner">{banner()}</section>} {testModeFilter && ( <div className={styles.filtersRow}> <TestModeFilter /> </div> )} {statsConfig && !statsIsValidating && <ListTableStats config={statsConfig} values={statsData} />} </> )} <div className={styles.pageContent}> <SearchSection /> {contentMode && children ? <>{children}</> : <></>} <CheckboxContext.Provider value={checkboxRefs}> <ShowConfirmModalContext.Provider value={showConfirmActionModal}> <ListTable apiSettings={apiSettings} sortField={sortField} setSortDirectionForColumn={setSortDirectionForColumn} singleName={singleName} pluralName={pluralName} title={title} rowActions={rowActions} parameters={parameters} data={data} error={error} isLoading={isValidating} align={align} testMode={testMode} listTableBlankSlate={listTableBlankSlate} productRecommendation={productRecommendation} columnFilters={columnFilters} includeBulkActionsCheckbox={bulkActions?.length > 0} /> </ShowConfirmModalContext.Provider> </CheckboxContext.Provider> <PageActions /> </div> </article> <A11yDialog id="giveListTableModal" dialogRef={(instance) => (dialog.current = instance)} title={ <> {modalContent?.type === 'danger' && <DeleteIcon />} {modalContent?.label} </> } titleId={styles.modalTitle} classNames={{ container: styles.container, overlay: styles.overlay, dialog: cx(styles.dialog, { [styles.warning]: modalContent?.type === 'warning', [styles.danger]: modalContent?.type === 'danger', }), closeButton: 'hidden', }} > <div className={styles.modalContent}> {modalContent?.confirm(selectedIds, selectedNames, isOpen, setOpen) || null} </div> <div className={styles.gutter}> <button id={styles.cancel} onClick={(event) => dialog.current?.hide()}> {__('Cancel', 'give')} </button> <button id={styles.confirm} onClick={async (event) => { dialog.current?.hide(); try { await modalContent.action(selectedIds); await mutate(); await mutateStats(); } catch (error) { console.error('Bulk action error:', error); // Create a user-friendly error message let errorMessage = __('An error occurred while performing this action.', 'give'); if (error.message && error.message.includes('permission')) { errorMessage = __('You don\'t have permission to perform this action.', 'give'); } else if (error.message && error.message.includes('403')) { errorMessage = __('Access denied. You don\'t have permission to perform this action.', 'give'); } else if (error.message) { // Try to extract a meaningful message from the error const match = error.message.match(/You don't have permission[^"]*|You don't have permission[^"]*/i); if (match) { errorMessage = match[0].replace(/'/g, "'"); } } // Show error as a notice/alert alert(errorMessage); } }} > {modalContent?.confirmButtonText ?? __('Confirm', 'give')} </button> </div> </A11yDialog> </> ); }); ListTablePage.displayName = 'ListTablePage'; export default ListTablePage;
Save
Cancel