PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.13.0
GiveWP – Donation Plugin and Fundraising Platform v4.13.0
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 9 months ago Notifications 11 months ago Tabs 9 months ago store 1 year ago AdminDetailsPage.module.scss 8 months ago AdminSection.tsx 1 year ago ConfirmationDialog.tsx 9 months ago DefaultPrimaryActionButton.tsx 1 year ago index.tsx 9 months ago types.ts 9 months ago
index.tsx
258 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) {
143 console.error('🔴 Save failed with error:', err);
144 setIsSaving(false);
145
146 dispatch.addSnackbarNotice({
147 id: `save-error`,
148 type: 'error',
149 content: __(`${objectType.charAt(0).toUpperCase() + objectType.slice(1)} update failed`, 'give'),
150 });
151 }
152 }
153 };
154
155 if (isLoading) {
156 return (
157 <div className={styles.loadingContainer}>
158 <div className={styles.loadingContainerContent}>
159 <GiveSpinner />
160 <div className={styles.loadingContainerContentText}>{__(`Loading ${objectType}...`, 'give')}</div>
161 </div>
162 </div>
163 );
164 }
165
166 return (
167 <ErrorBoundary>
168 <FormProvider {...methods}>
169 <SlotFillProvider>
170 <form id={'givewp-details-form'} onSubmit={handleSubmit(onSubmit)}>
171 <article className={`interface-interface-skeleton__content ${styles.page}`}>
172 <TabsRouter tabDefinitions={tabDefinitions}>
173 <header className={styles.pageHeader}>
174 <div className={styles.breadcrumb}>
175 <a href={breadcrumbUrl}>
176 {objectTypePlural.charAt(0).toUpperCase() + objectTypePlural.slice(1)}
177 </a>
178 <BreadcrumbSeparatorIcon />
179 <span>{breadcrumbTitle || record?.name}</span>
180 </div>
181 <div className={styles.flexContainer}>
182 <div className={styles.flexRow}>
183 <h1 className={styles.pageTitle}>{pageTitle || record?.name}</h1>
184 {StatusBadge && <StatusBadge />}
185 </div>
186
187 <div className={`${styles.flexRow} ${styles.justifyContentEnd}`}>
188 {SecondaryActionButton && (
189 <SecondaryActionButton
190 className={`button button-tertiary ${styles.secondaryActionButton}`}
191 />
192 )}
193
194 <PrimaryActionButton
195 isSaving={isSaving}
196 formState={formState}
197 className={`button button-primary ${styles.primaryActionButton}`}
198 />
199
200 {ContextMenuItems && (
201 <>
202 <button
203 ref={contextMenuButtonRef}
204 className={`button button-secondary ${styles.contextMenuButton}`}
205 onClick={(e) => {
206 e.preventDefault();
207 setShowContextMenu(!showContextMenu);
208 }}
209 >
210 <DotsIcons />
211 </button>
212
213 {!isSaving && showContextMenu && (
214 <div ref={contextMenuRef} className={styles.contextMenu}>
215 <ContextMenuItems className={styles.contextMenuItem} />
216 </div>
217 )}
218 </>
219 )}
220 </div>
221 </div>
222 <TabList tabDefinitions={tabDefinitions} />
223 </header>
224
225 <TabPanels tabDefinitions={tabDefinitions} />
226
227 {children}
228 </TabsRouter>
229 </article>
230 </form>
231
232 <NotificationPlaceholder type="snackbar" />
233
234 <PluginArea scope={`givewp-${objectType}-details-page`} />
235 </SlotFillProvider>
236 </FormProvider>
237 </ErrorBoundary>
238 );
239 }
240
241 const exposeAdminComponentsAndHooks = (): void => {
242 (window as any).givewp = (window as any).givewp || {};
243 (window as any).givewp.admin = (window as any).givewp.admin || {};
244 (window as any).givewp.admin.components = (window as any).givewp.admin.components || {};
245 (window as any).givewp.admin.hooks = (window as any).givewp.admin.hooks || {};
246
247 Object.assign((window as any).givewp.admin, {
248 components: {
249 AdminSection,
250 AdminSectionField,
251 },
252 hooks: {
253 useFormContext,
254 useFormState,
255 },
256 });
257 };
258