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