PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.6.1
GiveWP – Donation Plugin and Fundraising Platform v4.6.1
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 / Admin / components / PrivateNotes / index.tsx
give / src / Admin / components / PrivateNotes Last commit date
Icons.tsx 1 year ago index.tsx 11 months ago style.module.scss 1 year ago
index.tsx
373 lines
1 import {__} from '@wordpress/i18n';
2 import {addQueryArgs} from '@wordpress/url';
3 import useSWR from 'swr';
4 import React, {useState} from 'react';
5 import apiFetch from '@wordpress/api-fetch';
6 import {useDispatch} from '@wordpress/data';
7 import {ConfirmationDialogIcon, DeleteIcon, DotsMenuIcon, EditIcon, NotesIcon} from './Icons';
8 import Spinner from '../Spinner';
9 import ModalDialog from '@givewp/components/AdminUI/ModalDialog';
10 import style from './style.module.scss';
11 import cx from 'classnames';
12 import {formatTimestamp} from '@givewp/src/Admin/utils';
13 import Header from '@givewp/src/Admin/components/Header';
14
15 /**
16 * @since 4.5.0
17 */
18 type DonorNote = {
19 id: number;
20 donorId: number;
21 content: string;
22 createdAt: {
23 date: string;
24 };
25 }
26
27 /**
28 * @since 4.5.0
29 */
30 type NoteState = {
31 isAddingNote: boolean;
32 isSavingNote: boolean;
33 note: string;
34 perPage: number;
35 }
36
37 /**
38 * @since 4.6.0
39 */
40 export function DonorNotes({donorId}: {donorId: number}) {
41 return <PrivateNotes endpoint={`/givewp/v3/donors/${donorId}/notes`} />
42 }
43
44 /**
45 * @since 4.6.0
46 */
47 export function DonationNotes({donationId}: {donationId: number}) {
48 return <PrivateNotes endpoint={`/givewp/v3/donations/${donationId}/notes`} />
49 }
50
51 /**
52 * @since 4.4.0
53 */
54 function PrivateNotes({endpoint}: {endpoint: string}) {
55 const [state, setNoteState] = useState<NoteState>({
56 isAddingNote: false,
57 isSavingNote: false,
58 note: '',
59 perPage: 5,
60 });
61
62 const dispatch = useDispatch('givewp/admin-details-page-notifications');
63
64 const {
65 data,
66 isLoading,
67 isValidating,
68 mutate,
69 } = useSWR<{data: DonorNote[]; totalPages: number; totalItems: number}>(endpoint, async (url) => {
70 const response = await apiFetch({
71 path: addQueryArgs(url, {page: 1, per_page: state.perPage}),
72 parse: false,
73 }) as Response;
74 const data = await response.json();
75 return {
76 data,
77 totalPages: Number(response.headers.get('X-WP-TotalPages')),
78 totalItems: Number(response.headers.get('X-WP-Total')),
79 };
80 }, {revalidateOnFocus: false});
81
82 const saveNote = () => {
83 setState({isSavingNote: true});
84 apiFetch({path: endpoint, method: 'POST', data: {content: state.note}})
85 .then((response) => {
86 mutate(response).then(() => {
87 setState({isAddingNote: false})
88 dispatch.addSnackbarNotice({
89 id: 'add-note',
90 content: __('You added a private note', 'give'),
91 });
92 });
93 });
94 };
95
96 const deleteNote = (id: number) => {
97 apiFetch({path: `${endpoint}/${id}`, method: 'DELETE', data: {id}})
98 .then(async (response) => {
99 await mutate(response);
100 dispatch.addSnackbarNotice({
101 id: 'delete-note',
102 content: __('Private note deleted successfully', 'give'),
103 });
104 });
105 };
106
107 const editNote = (id: number, content: string) => {
108 apiFetch({path: `${endpoint}/${id}`, method: 'PATCH', data: {content}})
109 .then(async (response) => {
110 await mutate(response);
111 dispatch.addSnackbarNotice({
112 id: 'edit-note',
113 content: __('Private note edited', 'give'),
114 });
115 });
116 };
117
118 const setState = (props) => {
119 setNoteState((prevState) => {
120 return {
121 ...prevState,
122 ...props,
123 };
124 });
125 };
126
127 if (isLoading || isValidating) {
128 return (
129 <div style={{margin: '0 auto'}}>
130 <Spinner />
131 </div>
132 );
133 }
134
135 return (
136 <>
137 <Header
138 title={__('Private Note', 'give')}
139 subtitle={__('This note will be seen by only admins', 'give')}
140 actionOnClick={() => setState({isAddingNote: true})}
141 actionText={__('Add note', 'give')}
142 />
143 <div className={style.notesContainer}>
144 {state.isAddingNote && (
145 <div className={style.addNoteContainer}>
146 <textarea
147 className={style.textarea}
148 onChange={(e) => setState({note: e.target.value})}
149 ></textarea>
150
151 <div className={style.textAreaButtons}>
152 <button
153 className={cx(style.button, style.cancelBtn)}
154 onClick={() => setState({isAddingNote: false})}
155 >
156 {__('Cancel', 'give')}
157 </button>
158 <button
159 className={cx(style.button, style.saveBtn)}
160 onClick={(e) => {
161 e.preventDefault();
162 saveNote();
163 }}
164 >
165 {__('Save', 'give')}
166 </button>
167 </div>
168 </div>
169 )}
170 {data?.data?.length ? (
171 <>
172 {data.data.map((note) => {
173 return (
174 <Note
175 key={note.id}
176 note={note}
177 onDelete={(id: number) => deleteNote(id)}
178 onEdit={(id: number, content: string) => editNote(id, content)}
179 />
180 );
181 })}
182 </>
183 ) : (
184 <>
185 {!state.isAddingNote && (
186 <div style={{margin: '0 auto', textAlign: 'center'}}>
187 <NotesIcon />
188 <p className={style.noNotesText}>{__('No notes yet', 'give')}</p>
189 </div>
190 )}
191 </>
192 )}
193
194 <div className={style.showMoreContainer}>
195 {data?.data?.length > 0 && data.totalItems > state.perPage && (
196 <button
197 className={style.showMoreButton}
198 onClick={async (e) => {
199 e.preventDefault();
200 setNoteState((prevState) => {
201 return {
202 ...prevState,
203 perPage: prevState.perPage += 5,
204 };
205 });
206
207 await mutate(endpoint);
208 }}>
209 {__('Show more', 'give')}
210 </button>
211 )}
212 </div>
213 </div>
214 </>
215 );
216 }
217
218
219 /**
220 * @since 4.4.0
221 */
222 const Note = ({note, onDelete, onEdit}) => {
223 const [showContextMenu, setShowContextMenu] = useState(false);
224 const [currentlyEditing, setCurrentlyEditing] = useState(null);
225 const [content, setContent] = useState(note.content);
226 const [showDeleteDialog, setShowDeleteDialog] = useState(false);
227
228 return (
229 <>
230 <div
231 onMouseLeave={() => {
232 setShowContextMenu(false);
233 }}
234 >
235 {currentlyEditing ? (
236 <>
237 <div className={style.addNoteContainer}>
238 <textarea
239 className={style.textarea}
240 onChange={(e) => setContent(e.target.value)}
241 value={content}
242 ></textarea>
243
244 <div className={style.textAreaButtons}>
245 <button
246 className={cx(style.button, style.cancelBtn)}
247 onClick={() => {
248 setCurrentlyEditing(null);
249 setShowContextMenu(false);
250 }}
251 >
252 {__('Cancel', 'give')}
253 </button>
254 <button
255 className={cx(style.button, style.saveBtn)}
256 onClick={(e) => {
257 e.preventDefault();
258 setShowContextMenu(false);
259 onEdit(note.id, content);
260 }}
261 >
262 {__('Save', 'give')}
263 </button>
264 </div>
265 </div>
266 </>
267 ) : (
268 <>
269 <div className={style.noteContainer}>
270 <div className={style.note}>
271 <div className={style.title}>
272 {note.content}
273 </div>
274
275 <div
276 className={style.dotsMenu}
277 onClick={() => setShowContextMenu(true)}
278 >
279 <DotsMenuIcon />
280 {showContextMenu && (
281 <div className={style.menu}>
282 <a
283 href="#"
284 className={style.menuItem}
285 onClick={(e) => {
286 e.preventDefault();
287 setShowContextMenu(false);
288 setCurrentlyEditing(note.id);
289 }}
290 >
291 <EditIcon /> {__('Edit', 'give')}
292 </a>
293 <a
294 href="#"
295 className={cx(style.menuItem, style.delete)}
296 onClick={(e) => {
297 e.preventDefault();
298 setShowContextMenu(false);
299 setShowDeleteDialog(true);
300 }}
301 >
302 <DeleteIcon /> {__('Delete', 'give')}
303 </a>
304 </div>
305 )}
306 </div>
307 </div>
308 <div className={style.date}>
309 {formatTimestamp(note.createdAt.date)}
310 </div>
311 </div>
312 </>
313 )}
314 <ConfirmationDialog
315 title={__('Delete Note', 'give')}
316 isOpen={showDeleteDialog}
317 handleClose={() => setShowDeleteDialog(false)}
318 handleConfirm={() => {
319 onDelete(note.id);
320 }}
321 />
322 </div>
323 </>
324 );
325 };
326
327
328 /**
329 * @since 4.5.0
330 */
331 function ConfirmationDialog({
332 isOpen,
333 title,
334 handleClose,
335 handleConfirm
336 }: {
337 isOpen: boolean;
338 handleClose: () => void;
339 handleConfirm: () => void;
340 title: string;
341 }) {
342 return (
343 <ModalDialog
344 icon={<ConfirmationDialogIcon />}
345 isOpen={isOpen}
346 showHeader={true}
347 handleClose={handleClose}
348 title={title}
349 >
350 <>
351 <div className={style.dialogContent}>
352 {__('Are you sure you want to delete this note?', 'give')}
353 </div>
354 <div className={style.dialogButtons}>
355 <button
356 className={style.cancelButton}
357 onClick={handleClose}
358 >
359 {__('Cancel', 'give')}
360 </button>
361 <button
362 className={style.confirmButton}
363 onClick={handleConfirm}
364 >
365 {__('Delete note', 'give')}
366 </button>
367 </div>
368 </>
369 </ModalDialog>
370 );
371 }
372
373