import type { PageView, PondView } from '@dorfteich/shared'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { FormError } from '../components/forms'; import { apiDelete, apiGet, apiPost } from '../lib/api'; import { useDocumentTitle } from '../lib/use-document-title'; /** Pond trash view (issue #31): list, restore, and permanently delete — * single pages or a checkbox multi-selection at once (#128). */ export function TrashPage(): React.JSX.Element { const { t } = useTranslation('editor'); const { pondSlug = '' } = useParams<{ pondSlug: string }>(); const queryClient = useQueryClient(); const [selected, setSelected] = useState>(new Set()); const [busy, setBusy] = useState(false); const [failedCount, setFailedCount] = useState(0); const pond = useQuery({ queryKey: ['pond', pondSlug], queryFn: () => apiGet(`/ponds/${pondSlug}`), }); useDocumentTitle(pond.data ? t('trash.title', { pond: pond.data.name }) : undefined); const trash = useQuery({ queryKey: ['trash', pond.data?.id], queryFn: () => apiGet(`/ponds/${pond.data!.id}/trash`), enabled: Boolean(pond.data), }); // Selection pruned to pages still in the trash — restored/purged entries // drop out on refresh, failed ones stay selected for a retry. const pages = trash.data ?? []; const selectedIds = pages.filter((page) => selected.has(page.id)).map((page) => page.id); const allSelected = pages.length > 0 && selectedIds.length === pages.length; // Native indeterminate state for a partial selection — only settable via // the DOM property, not an attribute. const selectAllRef = useRef(null); useEffect(() => { if (selectAllRef.current) { selectAllRef.current.indeterminate = selectedIds.length > 0 && !allSelected; } }, [selectedIds.length, allSelected]); async function refresh(): Promise { await queryClient.invalidateQueries({ queryKey: ['trash', pond.data?.id] }); await queryClient.invalidateQueries({ queryKey: ['pages', pond.data?.id] }); } function toggle(id: string): void { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function toggleAll(): void { setSelected(allSelected ? new Set() : new Set(pages.map((page) => page.id))); } /** Run one action over the ids, keep going past failures (#128): a page * that stopped being restorable must not strand the rest of the selection. * Sequential on purpose — purge promotes leftover children (#107), so * concurrent tree mutations would race each other. */ async function runBulk(ids: string[], action: (id: string) => Promise): Promise { setBusy(true); setFailedCount(0); let failed = 0; for (const id of ids) { try { await action(id); setSelected((prev) => { const next = new Set(prev); next.delete(id); return next; }); } catch { failed += 1; } } setFailedCount(failed); setBusy(false); await refresh(); } async function restore(id: string): Promise { await runBulk([id], (pageId) => apiPost(`/pages/${pageId}/restore`)); } async function purge(id: string): Promise { if (!window.confirm(t('trash.purgeConfirm'))) return; await runBulk([id], (pageId) => apiDelete(`/pages/${pageId}/purge`)); } async function restoreSelected(): Promise { await runBulk(selectedIds, (pageId) => apiPost(`/pages/${pageId}/restore`)); } async function purgeSelected(): Promise { if (!window.confirm(t('trash.purgeConfirmMany', { count: selectedIds.length }))) return; await runBulk(selectedIds, (pageId) => apiDelete(`/pages/${pageId}/purge`)); } if (pond.error || trash.error) return ; if (!pond.data || !trash.data) return <>; return (

{t('trash.title', { pond: pond.data.name })}

{failedCount > 0 && (

{t('trash.bulkFailed', { count: failedCount })}

)} {pages.length === 0 ? (

{t('trash.empty')}

) : ( <>
    {pages.map((page) => (
  • toggle(page.id)} /> {page.title} {page.deletedAt && t('trash.deletedAt', { date: new Date(page.deletedAt).toLocaleDateString() })}
  • ))}
)}
); }