PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.16.3
GiveWP – Donation Plugin and Fundraising Platform v4.16.3
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 / AdminDetailsPage / index.tsx
give / src / Views / Components / AdminDetailsPage Last commit date
ErrorBoundary 1 year ago Icons 10 months ago Notifications 11 months ago Tabs 9 months ago store 1 year ago AdminDetailsPage.module.scss 7 months ago AdminSection.tsx 7 months ago ConfirmationDialog.tsx 10 months ago DefaultPrimaryActionButton.tsx 1 year ago index.tsx 5 months ago types.ts 9 months ago
index.tsx
262 lines
1 /**
2 * External Dependencies
3 */
4 import {useEffect, useRef, useState} from 'react';
5 import {FormProvider, SubmitHandler, useForm, useFormContext, useFormState} from 'react-hook-form';
6 import {ajvResolver} from '@givewp/admin/ajv';
7
8 import {SlotFillProvider} from '@wordpress/components';
9 import {useDispatch} from '@wordpress/data';
10 import {__} from '@wordpress/i18n';
11 import {PluginArea} from '@wordpress/plugins';
12 import apiFetch from '@wordpress/api-fetch';
13 import {JSONSchemaType} from 'ajv';
14
15 /**
16 * Internal Dependencies
17 */
18 import {Spinner as GiveSpinner} from '@givewp/components';
19 import styles from './AdminDetailsPage.module.scss';
20 import AdminSection, {AdminSectionField} from './AdminSection';
21 import DefaultPrimaryActionButton from './DefaultPrimaryActionButton';
22 import ErrorBoundary from './ErrorBoundary';
23 import {BreadcrumbSeparatorIcon, DotsIcons} from './Icons';
24 import NotificationPlaceholder from './Notifications';
25 import TabsRouter from './Tabs/Router';
26 import TabList from './Tabs/TabList';
27 import TabPanels from './Tabs/TabPanels';
28 import {AdminDetailsPageProps} from './types';
29 import {prepareDefaultValuesFromSchema} from '@givewp/admin/utils';
30
31 import './store';
32
33 /**
34 * @since 4.4.0
35 */
36 export default function AdminDetailsPage<T extends Record<string, any>>({
37 objectId,
38 objectType,
39 objectTypePlural,
40 useObjectEntityRecord,
41 shouldSaveForm,
42 breadcrumbUrl,
43 breadcrumbTitle,
44 pageTitle,
45 StatusBadge,
46 PrimaryActionButton = DefaultPrimaryActionButton,
47 SecondaryActionButton,
48 ContextMenuItems,
49 tabDefinitions,
50 children,
51 }: AdminDetailsPageProps<T>) {
52 const [resolver, setResolver] = useState({});
53 const [isSaving, setIsSaving] = useState(false);
54 const [isLoading, setIsLoading] = useState(true);
55 const [schema, setSchema] = useState<JSONSchemaType<any> | null>(null);
56 const [showContextMenu, setShowContextMenu] = useState<boolean>(false);
57 const contextMenuButtonRef = useRef<HTMLButtonElement>(null);
58 const contextMenuRef = useRef<HTMLDivElement>(null);
59
60 const dispatch = useDispatch(`givewp/admin-details-page-notifications`);
61
62 exposeAdminComponentsAndHooks();
63
64 useEffect(() => {
65 if (!objectId) {
66 return;
67 }
68
69 apiFetch({
70 path: `/givewp/v3/${objectTypePlural}/${objectId}`,
71 method: 'OPTIONS',
72 }).then(({schema}: {schema: JSONSchemaType<any>}) => {
73 setSchema(schema);
74 setResolver({
75 resolver: ajvResolver(schema),
76 });
77 });
78 }, [objectId, objectTypePlural]);
79
80 const {record, hasResolved, save, edit} = useObjectEntityRecord(objectId);
81
82 const methods = useForm<T>({
83 mode: 'onBlur',
84 shouldFocusError: true,
85 ...resolver,
86 });
87
88 const {formState, handleSubmit, reset} = methods;
89
90 // Close context menu when clicked outside
91 useEffect(() => {
92 const handleClickOutside = (e: MouseEvent) => {
93 if (!showContextMenu) {
94 return;
95 }
96
97 if (
98 e.target instanceof HTMLElement &&
99 !contextMenuButtonRef.current?.contains(e.target) &&
100 !contextMenuRef.current?.contains(e.target)
101 ) {
102 setShowContextMenu(false);
103 contextMenuButtonRef.current?.blur();
104 }
105 };
106
107 document.addEventListener('click', handleClickOutside);
108
109 return () => {
110 document.removeEventListener('click', handleClickOutside);
111 };
112 }, [showContextMenu]);
113
114 // Set default values when entity is loaded
115 useEffect(() => {
116 if (hasResolved && schema && record) {
117 const preparedRecord = prepareDefaultValuesFromSchema(record, (schema as any)?.properties) as T;
118 reset(preparedRecord);
119 setIsLoading(false);
120 }
121 }, [hasResolved, !!schema, !!record]);
122
123 const onSubmit: SubmitHandler<T> = async (data) => {
124 const shouldSave = shouldSaveForm ? shouldSaveForm(formState.isDirty, data) : formState.isDirty;
125
126 if (shouldSave) {
127 setIsSaving(true);
128 edit(data);
129
130 try {
131 // @ts-ignore
132 const response: T = await save();
133 setIsSaving(false);
134
135 const preparedRecord = prepareDefaultValuesFromSchema(response, (schema as any)?.properties) as T;
136 reset(preparedRecord);
137
138 dispatch.addSnackbarNotice({
139 id: `save-success`,
140 content: __(`${objectType.charAt(0).toUpperCase() + objectType.slice(1)} updated`, 'give'),
141 });
142 } catch (err: any) {
143 console.error('🔴 Save failed with error:', err);
144 setIsSaving(false);
145
146 const errorMessage = err?.message
147 ? err.message
148 : __(`${objectType.charAt(0).toUpperCase() + objectType.slice(1)} update failed`, 'give');
149
150 dispatch.addSnackbarNotice({
151 id: `save-error`,
152 type: 'error',
153 content: errorMessage,
154 });
155 }
156 }
157 };
158
159 if (isLoading) {
160 return (
161 <div className={styles.loadingContainer}>
162 <div className={styles.loadingContainerContent}>
163 <GiveSpinner />
164 <div className={styles.loadingContainerContentText}>{__(`Loading ${objectType}...`, 'give')}</div>
165 </div>
166 </div>
167 );
168 }
169
170 return (
171 <ErrorBoundary>
172 <FormProvider {...methods}>
173 <SlotFillProvider>
174 <form id={'givewp-details-form'} onSubmit={handleSubmit(onSubmit)}>
175 <article className={`interface-interface-skeleton__content ${styles.page}`}>
176 <TabsRouter tabDefinitions={tabDefinitions}>
177 <header className={styles.pageHeader}>
178 <div className={styles.breadcrumb}>
179 <a href={breadcrumbUrl}>
180 {objectTypePlural.charAt(0).toUpperCase() + objectTypePlural.slice(1)}
181 </a>
182 <BreadcrumbSeparatorIcon />
183 <span>{breadcrumbTitle || record?.name}</span>
184 </div>
185 <div className={styles.flexContainer}>
186 <div className={styles.flexRow}>
187 <h1 className={styles.pageTitle}>{pageTitle || record?.name}</h1>
188 {StatusBadge && <StatusBadge />}
189 </div>
190
191 <div className={`${styles.flexRow} ${styles.justifyContentEnd}`}>
192 {SecondaryActionButton && (
193 <SecondaryActionButton
194 className={`button button-tertiary ${styles.secondaryActionButton}`}
195 />
196 )}
197
198 <PrimaryActionButton
199 isSaving={isSaving}
200 formState={formState}
201 className={`button button-primary ${styles.primaryActionButton}`}
202 />
203
204 {ContextMenuItems && (
205 <>
206 <button
207 ref={contextMenuButtonRef}
208 className={`button button-secondary ${styles.contextMenuButton}`}
209 onClick={(e) => {
210 e.preventDefault();
211 setShowContextMenu(!showContextMenu);
212 }}
213 >
214 <DotsIcons />
215 </button>
216
217 {!isSaving && showContextMenu && (
218 <div ref={contextMenuRef} className={styles.contextMenu}>
219 <ContextMenuItems className={styles.contextMenuItem} />
220 </div>
221 )}
222 </>
223 )}
224 </div>
225 </div>
226 <TabList tabDefinitions={tabDefinitions} />
227 </header>
228
229 <TabPanels tabDefinitions={tabDefinitions} />
230
231 {children}
232 </TabsRouter>
233 </article>
234 </form>
235
236 <NotificationPlaceholder type="snackbar" />
237
238 <PluginArea scope={`givewp-${objectType}-details-page`} />
239 </SlotFillProvider>
240 </FormProvider>
241 </ErrorBoundary>
242 );
243 }
244
245 const exposeAdminComponentsAndHooks = (): void => {
246 (window as any).givewp = (window as any).givewp || {};
247 (window as any).givewp.admin = (window as any).givewp.admin || {};
248 (window as any).givewp.admin.components = (window as any).givewp.admin.components || {};
249 (window as any).givewp.admin.hooks = (window as any).givewp.admin.hooks || {};
250
251 Object.assign((window as any).givewp.admin, {
252 components: {
253 AdminSection,
254 AdminSectionField,
255 },
256 hooks: {
257 useFormContext,
258 useFormState,
259 },
260 });
261 };
262