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