import {__} from '@wordpress/i18n'; import {addQueryArgs} from '@wordpress/url'; import useSWR from 'swr'; import {useState} from 'react'; import apiFetch from '@wordpress/api-fetch'; import {useDispatch} from '@wordpress/data'; import {ConfirmationDialogIcon, DeleteIcon, DotsMenuIcon, EditIcon, NotesIcon} from './Icons'; import Spinner from '../Spinner'; import ModalDialog from '@givewp/components/AdminUI/ModalDialog'; import style from './style.module.scss'; import cx from 'classnames'; import {formatTimestamp} from '@givewp/src/Admin/utils'; import Header from '@givewp/src/Admin/components/Header'; /** * @since 4.5.0 */ type DonorNote = { id: number; donorId: number; content: string; createdAt: { date: string; }; } /** * @since 4.8.0 Include loadingId in the state to manage loading states per note. * @since 4.5.0 */ type NoteState = { notes: DonorNote[]; loadingId: number | null; totalItems: number; isAddingNote: boolean; isSavingNote: boolean; note: string; perPage: number; } /** * @since 4.6.0 */ export function DonorNotes({donorId}: {donorId: number}) { return } /** * @since 4.6.0 */ export function DonationNotes({donationId}: {donationId: number}) { return } /** * @since 4.8.0 */ export function SubscriptionNotes({subscriptionId}: {subscriptionId: number}) { return } /** * @since 4.8.0 Manage local state to handle loading indicators per note. * @since 4.4.0 */ function PrivateNotes({endpoint}: {endpoint: string}) { const [state, setNoteState] = useState({ notes: [], loadingId: undefined, totalItems: 0, isAddingNote: false, isSavingNote: false, note: '', perPage: 5, }); const dispatch = useDispatch('givewp/admin-details-page-notifications'); const { data, isLoading, isValidating, mutate, } = useSWR<{data: DonorNote[]; totalPages: number; totalItems: number}>(endpoint, async (url) => { const response = await apiFetch({ path: addQueryArgs(url, {page: 1, per_page: state.perPage}), parse: false, }) as Response; const data = await response.json(); setState({ notes: data, totalItems: Number(response.headers.get('X-WP-Total')), }); return { data, totalPages: Number(response.headers.get('X-WP-TotalPages')), totalItems: Number(response.headers.get('X-WP-Total')), }; }, {revalidateOnFocus: false}); const initialLoad = (isLoading || isValidating) && state.loadingId === undefined; const saveNote = () => { const tempId = Date.now(); const tempNote = { id: tempId, content: state.note, createdAt: {date: new Date().toISOString()} }; // Add temporary note to the UI state. setState({ loadingId: tempId, notes: [tempNote, ...state.notes], isAddingNote: false, isSavingNote: true }); apiFetch({path: endpoint, method: 'POST', data: {content: state.note}}) .then(async (response) => { await mutate(response); setState({ isAddingNote: false, isSavingNote: false, }); dispatch.addSnackbarNotice({ id: 'add-note', content: __('You added a private note', 'give'), }); }); }; const deleteNote = (id: number) => { setState({loadingId: id}); apiFetch({path: `${endpoint}/${id}`, method: 'DELETE', data: {id}}) .then(async (response) => { await mutate(response); dispatch.addSnackbarNotice({ id: 'delete-note', content: __('Private note deleted successfully', 'give'), }); }); }; const editNote = (id: number, content: string) => { setState({loadingId: id}); apiFetch({path: `${endpoint}/${id}`, method: 'PATCH', data: {content}}) .then(async (response) => { await mutate(response); setState({ loadingId: null, notes: state.notes.map((note) => note.id === id ? response : note) }); dispatch.addSnackbarNotice({ id: 'edit-note', content: __('Private note edited', 'give'), }); }); }; const setState = (props) => { setNoteState((prevState) => { return { ...prevState, ...props, }; }); }; return ( <> setState({isAddingNote: true})} actionText={__('Add note', 'give')} /> {initialLoad && ( )} {!initialLoad && {state.isAddingNote && ( setState({note: e.target.value})} > setState({isAddingNote: false})} > {__('Cancel', 'give')} { e.preventDefault(); saveNote(); }} > {__('Save', 'give')} )} {state?.notes?.length > 0 ? ( <> {state?.notes?.map((note) => { return ( deleteNote(id)} onEdit={(id: number, content: string) => editNote(id, content)} isLoading={note.id === state.loadingId} /> ); })} > ) : ( <> {!state.isAddingNote && ( {__('No notes yet', 'give')} )} > )} {state?.notes?.length > 0 && state.totalItems > state.perPage && ( { e.preventDefault(); setNoteState((prevState) => { return { ...prevState, perPage: prevState.perPage += 5, }; }); await mutate(endpoint); }}> {__('Show more', 'give')} )} } > ); } /** * @since 4.8.0 Improved accessibility with semantic buttons. Added per-note loading state handling. * @since 4.4.0 */ const Note = ({note, onDelete, onEdit, isLoading}) => { const [showContextMenu, setShowContextMenu] = useState(false); const [currentlyEditing, setCurrentlyEditing] = useState(); const [content, setContent] = useState(note.content); const [showDeleteDialog, setShowDeleteDialog] = useState(false); return ( <> { setShowContextMenu(false); }} > {currentlyEditing ? ( <> setContent(e.target.value)} value={content} > { setCurrentlyEditing(null); setShowContextMenu(false); }} > {__('Cancel', 'give')} { e.preventDefault(); setShowContextMenu(false); setCurrentlyEditing(null); onEdit(note.id, content); }} > {__('Save', 'give')} > ) : ( <> {isLoading ? ( ) : ( <> {note.content} setShowContextMenu(true)} aria-haspopup="true" aria-expanded={showContextMenu} aria-controls="contextMenu" > {showContextMenu && ( { e.preventDefault(); setShowContextMenu(false); setCurrentlyEditing(note.id); }} > {__('Edit', 'give')} { e.preventDefault(); setShowContextMenu(false); setShowDeleteDialog(true); }} > {__('Delete', 'give')} )} {formatTimestamp(note.createdAt.date)} > )} > )} setShowDeleteDialog(false)} handleConfirm={() => { setShowDeleteDialog(false) onDelete(note.id); }} /> > ); }; /** * @since 4.5.0 */ function ConfirmationDialog({ isOpen, title, handleClose, handleConfirm }: { isOpen: boolean; handleClose: () => void; handleConfirm: () => void; title: string; }) { return ( } isOpen={isOpen} showHeader={true} handleClose={handleClose} title={title} > <> {__('Are you sure you want to delete this note?', 'give')} {__('Cancel', 'give')} {__('Delete note', 'give')} > ); }
{__('No notes yet', 'give')}