forked from akai/readest
Replace the standalone "Purge Data" menu item with an opt-in toggle on the delete confirmation alert (default off). When enabled, the delete escalates to a full purge that also wipes the book's reading-data sidecars (config and nav), instead of leaving the metadata folder behind. The single, bulk, and multi-select deletes all share the same alert, so this also covers batch deletes that previously kept every metadata folder. - Alert: add optional children, confirmLabel, confirmButtonClassName slots - DeleteConfirmAlert: new wrapper owning the toggle and red escalation - BookDetailView: drop the Purge Data menu item and onPurge prop - BookDetailModal: route the standard delete to purge when the toggle is on - Bookshelf/page: route the bulk delete batch to purge when the toggle is on Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { render, cleanup, fireEvent, waitFor, screen } from '@testing-library/re
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
import BookDetailModal from '@/components/metadata/BookDetailModal';
|
||||
import { DropdownProvider } from '@/context/DropdownContext';
|
||||
|
||||
vi.mock('@/hooks/useTranslation', () => ({
|
||||
useTranslation: () => (s: string) => s,
|
||||
@@ -72,7 +73,23 @@ vi.mock('@/components/Dialog', () => ({
|
||||
default: ({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) =>
|
||||
isOpen ? <div>{children}</div> : null,
|
||||
}));
|
||||
vi.mock('@/components/Alert', () => ({ __esModule: true, default: () => null }));
|
||||
// Expose the confirm callback with the purge flag so we can assert routing
|
||||
// without driving the real toggle UI (covered by DeleteConfirmAlert.test.tsx).
|
||||
vi.mock('@/components/DeleteConfirmAlert', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
showPurgeToggle,
|
||||
onConfirm,
|
||||
}: {
|
||||
showPurgeToggle?: boolean;
|
||||
onConfirm: (purgeData: boolean) => void;
|
||||
}) => (
|
||||
<div data-testid='delete-confirm' data-purge-toggle={String(!!showPurgeToggle)}>
|
||||
<button onClick={() => onConfirm(false)}>confirm-keep-data</button>
|
||||
<button onClick={() => onConfirm(true)}>confirm-purge-data</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock('@/components/metadata/SourceSelector', () => ({ __esModule: true, default: () => null }));
|
||||
vi.mock('@/components/Spinner', () => ({ __esModule: true, default: () => null }));
|
||||
|
||||
@@ -122,3 +139,49 @@ describe('BookDetailModal cover refresh after save', () => {
|
||||
expect(screen.getByTestId('cover').getAttribute('src')).toBe('_blank');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BookDetailModal purge-on-delete routing', () => {
|
||||
const renderModal = (handlers: { handleBookDelete: () => void; handleBookPurge: () => void }) =>
|
||||
render(
|
||||
<DropdownProvider>
|
||||
<BookDetailModal
|
||||
book={makeBook()}
|
||||
isOpen
|
||||
onClose={vi.fn()}
|
||||
handleBookDelete={handlers.handleBookDelete}
|
||||
handleBookDeleteCloudBackup={vi.fn()}
|
||||
handleBookDeleteLocalCopy={vi.fn()}
|
||||
handleBookPurge={handlers.handleBookPurge}
|
||||
/>
|
||||
</DropdownProvider>,
|
||||
);
|
||||
|
||||
const openStandardDelete = (container: HTMLElement) => {
|
||||
fireEvent.click(container.querySelector('button[aria-label="Delete Book Options"]')!);
|
||||
fireEvent.click(screen.getByText('Remove from Cloud & Device'));
|
||||
};
|
||||
|
||||
it('shows the purge toggle on the standard delete and routes to purge when enabled', () => {
|
||||
const handleBookDelete = vi.fn();
|
||||
const handleBookPurge = vi.fn();
|
||||
const { container } = renderModal({ handleBookDelete, handleBookPurge });
|
||||
|
||||
openStandardDelete(container);
|
||||
expect(screen.getByTestId('delete-confirm').getAttribute('data-purge-toggle')).toBe('true');
|
||||
|
||||
fireEvent.click(screen.getByText('confirm-purge-data'));
|
||||
expect(handleBookPurge).toHaveBeenCalledTimes(1);
|
||||
expect(handleBookDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes the standard delete to a plain delete when the toggle is off', () => {
|
||||
const handleBookDelete = vi.fn();
|
||||
const handleBookPurge = vi.fn();
|
||||
const { container } = renderModal({ handleBookDelete, handleBookPurge });
|
||||
|
||||
openStandardDelete(container);
|
||||
fireEvent.click(screen.getByText('confirm-keep-data'));
|
||||
expect(handleBookDelete).toHaveBeenCalledTimes(1);
|
||||
expect(handleBookPurge).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,23 +166,17 @@ describe('BookDetailView More menu (Goodreads + Share)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('BookDetailView Purge Data action', () => {
|
||||
it('offers Purge Data in the delete dropdown and calls onPurge', () => {
|
||||
const onPurge = vi.fn();
|
||||
const { container, getByText } = renderView({ onPurge });
|
||||
const toggle = container.querySelector('button[aria-label="Delete Book Options"]');
|
||||
fireEvent.click(toggle!);
|
||||
const purgeButton = getByText('Purge Data').closest('button');
|
||||
expect(purgeButton).toBeTruthy();
|
||||
fireEvent.click(purgeButton!);
|
||||
expect(onPurge).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('omits Purge Data when onPurge is not provided', () => {
|
||||
describe('BookDetailView delete dropdown (purge folded into the confirm alert)', () => {
|
||||
it('no longer offers a Purge Data action and keeps the three remove options', () => {
|
||||
const { container, queryByText } = renderView();
|
||||
const toggle = container.querySelector('button[aria-label="Delete Book Options"]');
|
||||
fireEvent.click(toggle!);
|
||||
// Purge is now an opt-in toggle on the delete confirmation alert, not a
|
||||
// standalone menu item.
|
||||
expect(queryByText('Purge Data')).toBeNull();
|
||||
expect(queryByText('Remove from Cloud & Device')).toBeTruthy();
|
||||
expect(queryByText('Remove from Cloud Only')).toBeTruthy();
|
||||
expect(queryByText('Remove from Device Only')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { render, cleanup, fireEvent, screen } from '@testing-library/react';
|
||||
|
||||
import DeleteConfirmAlert from '@/components/DeleteConfirmAlert';
|
||||
|
||||
vi.mock('@/hooks/useTranslation', () => ({
|
||||
useTranslation: () => (s: string) => s,
|
||||
}));
|
||||
|
||||
// The wrapped <Alert> calls useKeyDownActions, which reads these.
|
||||
vi.mock('@/context/EnvContext', () => ({
|
||||
useEnv: () => ({ appService: null }),
|
||||
}));
|
||||
|
||||
vi.mock('@/store/deviceStore', () => ({
|
||||
useDeviceControlStore: () => ({
|
||||
acquireBackKeyInterception: vi.fn(),
|
||||
releaseBackKeyInterception: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
const setup = (props?: Partial<React.ComponentProps<typeof DeleteConfirmAlert>>) => {
|
||||
const onConfirm = vi.fn();
|
||||
const onCancel = vi.fn();
|
||||
render(
|
||||
<DeleteConfirmAlert
|
||||
title='Confirm Deletion'
|
||||
message='Are you sure to delete the selected book?'
|
||||
showPurgeToggle
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
return { onConfirm, onCancel };
|
||||
};
|
||||
|
||||
describe('DeleteConfirmAlert purge toggle', () => {
|
||||
it('renders the purge toggle OFF by default and confirms without purging', () => {
|
||||
const { onConfirm } = setup();
|
||||
|
||||
const toggle = screen.getByRole('checkbox') as HTMLInputElement;
|
||||
expect(toggle.checked).toBe(false);
|
||||
|
||||
const confirm = screen.getByText('Delete').closest('button')!;
|
||||
expect(confirm.className).toContain('btn-warning');
|
||||
|
||||
fireEvent.click(confirm);
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||
expect(onConfirm).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('escalates to a destructive purge when the toggle is turned on', () => {
|
||||
const { onConfirm } = setup();
|
||||
|
||||
const toggle = screen.getByRole('checkbox') as HTMLInputElement;
|
||||
fireEvent.click(toggle);
|
||||
expect(toggle.checked).toBe(true);
|
||||
|
||||
const confirm = screen.getByText('Purge & Delete').closest('button')!;
|
||||
expect(confirm.className).toContain('btn-error');
|
||||
|
||||
fireEvent.click(confirm);
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||
expect(onConfirm).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('omits the toggle and always confirms without purging when showPurgeToggle is false', () => {
|
||||
const { onConfirm } = setup({ showPurgeToggle: false });
|
||||
|
||||
expect(screen.queryByRole('checkbox')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByText('Delete').closest('button')!);
|
||||
expect(onConfirm).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('invokes onCancel from the Cancel button', () => {
|
||||
const { onCancel } = setup();
|
||||
fireEvent.click(screen.getByText('Cancel').closest('button')!);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -51,7 +51,7 @@ import { MIMETYPES, EXTS } from '@/libs/document';
|
||||
import { makeSafeFilename } from '@/utils/misc';
|
||||
|
||||
import { useSpatialNavigation } from '../hooks/useSpatialNavigation';
|
||||
import Alert from '@/components/Alert';
|
||||
import DeleteConfirmAlert from '@/components/DeleteConfirmAlert';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import ModalPortal from '@/components/ModalPortal';
|
||||
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
|
||||
@@ -74,6 +74,7 @@ interface BookshelfProps {
|
||||
) => Promise<boolean>;
|
||||
handleBookUpload: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleBookPurge: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleSetSelectMode: (selectMode: boolean) => void;
|
||||
handleShowDetailsBook: (book: Book) => void;
|
||||
handleLibraryNavigation: (targetGroup: string) => void;
|
||||
@@ -146,6 +147,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
handleBookUpload,
|
||||
handleBookDownload,
|
||||
handleBookDelete,
|
||||
handleBookPurge,
|
||||
handleSetSelectMode,
|
||||
handleShowDetailsBook,
|
||||
handleLibraryNavigation,
|
||||
@@ -390,8 +392,12 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
return filteredBooks.filter((book) => wanted.has(book.hash) && !book.deletedAt);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
const confirmDelete = async (purgeData: boolean) => {
|
||||
const books = getBooksToDelete();
|
||||
// Toggling "purge all reading data" on the confirmation routes the whole
|
||||
// batch through the purge path, which also wipes each book's reading-data
|
||||
// sidecars (config/nav) instead of leaving the metadata folder behind.
|
||||
const deleteBook = purgeData ? handleBookPurge : handleBookDelete;
|
||||
const concurrency = 20;
|
||||
|
||||
for (let i = 0; i < books.length; i += concurrency) {
|
||||
@@ -400,7 +406,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
break;
|
||||
}
|
||||
const batch = books.slice(i, i + concurrency);
|
||||
await Promise.all(batch.map((book) => handleBookDelete(book, false)));
|
||||
await Promise.all(batch.map((book) => deleteBook(book, false)));
|
||||
}
|
||||
handlePushLibrary();
|
||||
setSelectedBooks([]);
|
||||
@@ -843,11 +849,12 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
paddingBottom: `${(safeAreaInsets?.bottom || 0) + 16}px`,
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
<DeleteConfirmAlert
|
||||
title={_('Confirm Deletion')}
|
||||
message={_('Are you sure to delete {{count}} selected book(s)?', {
|
||||
count: getBooksToDelete().length,
|
||||
})}
|
||||
showPurgeToggle
|
||||
onCancel={() => {
|
||||
abortDeletionRef.current = true;
|
||||
setShowDeleteAlert(false);
|
||||
|
||||
@@ -1453,6 +1453,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
handleBookDelete={handleBookDelete('both')}
|
||||
handleBookPurge={handleBookDelete('purge')}
|
||||
handleSetSelectMode={handleSetSelectMode}
|
||||
handleShowDetailsBook={handleShowDetailsBook}
|
||||
handleLibraryNavigation={handleLibraryNavigation}
|
||||
|
||||
@@ -8,7 +8,20 @@ const Alert: React.FC<{
|
||||
message: string;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}> = ({ title, message, onCancel, onConfirm }) => {
|
||||
// Optional content rendered between the title/message and the actions row
|
||||
// (e.g. the delete confirmation's "purge reading data" toggle).
|
||||
children?: React.ReactNode;
|
||||
confirmLabel?: string;
|
||||
confirmButtonClassName?: string;
|
||||
}> = ({
|
||||
title,
|
||||
message,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
children,
|
||||
confirmLabel,
|
||||
confirmButtonClassName = 'btn-warning',
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const [isProcessing, setIsProcessing] = React.useState(false);
|
||||
const divRef = useKeyDownActions({ onCancel, onConfirm });
|
||||
@@ -50,18 +63,19 @@ const Alert: React.FC<{
|
||||
<div className='text-start text-sm'>{message}</div>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
<div className='buttons flex items-center justify-end gap-2'>
|
||||
<button className='btn btn-sm btn-neutral' onClick={onCancel}>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
<button
|
||||
className={clsx('btn btn-sm btn-warning', { 'btn-disabled': isProcessing })}
|
||||
className={clsx('btn btn-sm', confirmButtonClassName, { 'btn-disabled': isProcessing })}
|
||||
onClick={() => {
|
||||
setIsProcessing(true);
|
||||
onConfirm();
|
||||
}}
|
||||
>
|
||||
{_('Confirm')}
|
||||
{confirmLabel ?? _('Confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Alert from './Alert';
|
||||
|
||||
/**
|
||||
* Delete confirmation alert with an optional, opt-in "purge all reading data"
|
||||
* toggle. Used by both the single-book delete (BookDetailModal) and the
|
||||
* bulk/multi-select delete (Bookshelf), so the same purge-on-delete affordance
|
||||
* covers every standard delete path (issue #4698).
|
||||
*
|
||||
* The toggle defaults to OFF and is ephemeral — it resets every time the alert
|
||||
* mounts. When turned ON the confirm action escalates to a destructive purge
|
||||
* (wipes the book's reading progress, notes, and bookmarks), signalled by the
|
||||
* red button + relabel so the irreversible choice reads clearly even on e-ink
|
||||
* where colour alone is not enough.
|
||||
*/
|
||||
const DeleteConfirmAlert: React.FC<{
|
||||
title: string;
|
||||
message: string;
|
||||
showPurgeToggle?: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: (purgeData: boolean) => void;
|
||||
}> = ({ title, message, showPurgeToggle = false, onCancel, onConfirm }) => {
|
||||
const _ = useTranslation();
|
||||
const [purgeData, setPurgeData] = useState(false);
|
||||
|
||||
return (
|
||||
<Alert
|
||||
title={title}
|
||||
message={message}
|
||||
confirmLabel={purgeData ? _('Purge & Delete') : _('Delete')}
|
||||
confirmButtonClassName={purgeData ? 'btn-error' : 'btn-warning'}
|
||||
onCancel={onCancel}
|
||||
onConfirm={() => onConfirm(purgeData)}
|
||||
>
|
||||
{showPurgeToggle && (
|
||||
<label
|
||||
className={clsx(
|
||||
'eink-bordered flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors',
|
||||
purgeData
|
||||
? 'not-eink:border-error/40 not-eink:bg-error/10'
|
||||
: 'not-eink:border-base-content/10 not-eink:bg-base-100/60',
|
||||
)}
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
|
||||
<span className={clsx('text-sm font-medium', purgeData && 'not-eink:text-error')}>
|
||||
{_('Purge all reading data')}
|
||||
</span>
|
||||
<span className='text-neutral-content text-xs'>
|
||||
{purgeData
|
||||
? _(
|
||||
'This permanently erases reading progress, notes, and bookmarks. This cannot be undone.',
|
||||
)
|
||||
: _('Also erase reading progress, notes, and bookmarks.')}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type='checkbox'
|
||||
className={clsx('toggle toggle-sm shrink-0', purgeData && 'not-eink:toggle-error')}
|
||||
checked={purgeData}
|
||||
onChange={(e) => setPurgeData(e.target.checked)}
|
||||
aria-label={_('Purge all reading data')}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteConfirmAlert;
|
||||
@@ -12,7 +12,7 @@ import { useMetadataEdit } from './useMetadataEdit';
|
||||
import { DeleteAction } from '@/types/system';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import Alert from '@/components/Alert';
|
||||
import DeleteConfirmAlert from '@/components/DeleteConfirmAlert';
|
||||
import Dialog from '@/components/Dialog';
|
||||
import BookDetailView from './BookDetailView';
|
||||
import BookDetailEdit from './BookDetailEdit';
|
||||
@@ -32,10 +32,15 @@ interface BookDetailModalProps {
|
||||
handleBookMetadataUpdate?: (book: Book, updatedMetadata: BookMetadata) => void;
|
||||
}
|
||||
|
||||
// Purge is no longer a standalone menu action — it is an opt-in toggle on the
|
||||
// standard ('both') delete confirmation, so the menu only triggers these three.
|
||||
type DeleteMenuAction = Exclude<DeleteAction, 'purge'>;
|
||||
|
||||
interface DeleteConfig {
|
||||
title: string;
|
||||
message: string;
|
||||
handler?: (book: Book) => void;
|
||||
showPurgeToggle?: boolean;
|
||||
}
|
||||
|
||||
const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
@@ -54,7 +59,7 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { user } = useAuth();
|
||||
const { safeAreaInsets } = useThemeStore();
|
||||
const [activeDeleteAction, setActiveDeleteAction] = useState<DeleteAction | null>(null);
|
||||
const [activeDeleteAction, setActiveDeleteAction] = useState<DeleteMenuAction | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [bookMeta, setBookMeta] = useState<BookMetadata | null>(null);
|
||||
@@ -83,11 +88,12 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
resetToOriginal,
|
||||
} = useMetadataEdit(bookMeta);
|
||||
|
||||
const deleteConfigs: Record<DeleteAction, DeleteConfig> = {
|
||||
const deleteConfigs: Record<DeleteMenuAction, DeleteConfig> = {
|
||||
both: {
|
||||
title: _('Confirm Deletion'),
|
||||
message: _('Are you sure to delete the selected book?'),
|
||||
handler: handleBookDelete,
|
||||
showPurgeToggle: !!handleBookPurge,
|
||||
},
|
||||
cloud: {
|
||||
title: _('Confirm Deletion'),
|
||||
@@ -99,13 +105,6 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
message: _('Are you sure to delete the local copy of the selected book?'),
|
||||
handler: handleBookDeleteLocalCopy,
|
||||
},
|
||||
purge: {
|
||||
title: _('Purge Book Data'),
|
||||
message: _(
|
||||
'This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.',
|
||||
),
|
||||
handler: handleBookPurge,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -157,17 +156,22 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAction = (action: DeleteAction) => {
|
||||
const handleDeleteAction = (action: DeleteMenuAction) => {
|
||||
setActiveDeleteAction(action);
|
||||
};
|
||||
|
||||
const confirmDeleteAction = async () => {
|
||||
const confirmDeleteAction = async (purgeData: boolean) => {
|
||||
if (!activeDeleteAction) return;
|
||||
|
||||
const config = deleteConfigs[activeDeleteAction];
|
||||
handleClose();
|
||||
|
||||
if (config.handler) {
|
||||
// The standard "Cloud & Device" delete escalates to a full purge when the
|
||||
// user opts in via the confirmation toggle. The cloud-only / device-only
|
||||
// variants keep the library entry, so purging reading data does not apply.
|
||||
if (activeDeleteAction === 'both' && purgeData && handleBookPurge) {
|
||||
handleBookPurge(book);
|
||||
} else if (config.handler) {
|
||||
config.handler(book);
|
||||
}
|
||||
};
|
||||
@@ -179,7 +183,6 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
const handleDelete = () => handleDeleteAction('both');
|
||||
const handleDeleteCloudBackup = () => handleDeleteAction('cloud');
|
||||
const handleDeleteLocalCopy = () => handleDeleteAction('local');
|
||||
const handlePurge = () => handleDeleteAction('purge');
|
||||
|
||||
const handleShare = () => {
|
||||
// Close this modal first, then hand off to the share dialog hosted by
|
||||
@@ -267,7 +270,6 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
handleBookDeleteCloudBackup ? handleDeleteCloudBackup : undefined
|
||||
}
|
||||
onDeleteLocalCopy={handleBookDeleteLocalCopy ? handleDeleteLocalCopy : undefined}
|
||||
onPurge={handleBookPurge ? handlePurge : undefined}
|
||||
onDownload={handleBookDownload ? handleRedownload : undefined}
|
||||
onUpload={handleBookUpload ? handleReupload : undefined}
|
||||
onShare={handleShare}
|
||||
@@ -300,9 +302,10 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
paddingBottom: `${(safeAreaInsets?.bottom || 0) + 16}px`,
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
<DeleteConfirmAlert
|
||||
title={currentDeleteConfig.title}
|
||||
message={currentDeleteConfig.message}
|
||||
showPurgeToggle={currentDeleteConfig.showPurgeToggle}
|
||||
onCancel={cancelDeleteAction}
|
||||
onConfirm={confirmDeleteAction}
|
||||
/>
|
||||
|
||||
@@ -39,7 +39,6 @@ interface BookDetailViewProps {
|
||||
onDelete?: () => void;
|
||||
onDeleteCloudBackup?: () => void;
|
||||
onDeleteLocalCopy?: () => void;
|
||||
onPurge?: () => void;
|
||||
onDownload?: () => void;
|
||||
onUpload?: () => void;
|
||||
onShare?: () => void;
|
||||
@@ -55,7 +54,6 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
|
||||
onDelete,
|
||||
onDeleteCloudBackup,
|
||||
onDeleteLocalCopy,
|
||||
onPurge,
|
||||
onDownload,
|
||||
onUpload,
|
||||
onShare,
|
||||
@@ -153,16 +151,6 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
|
||||
onClick={onDeleteLocalCopy}
|
||||
disabled={!book.downloadedAt}
|
||||
/>
|
||||
{onPurge && (
|
||||
<MenuItem
|
||||
noIcon
|
||||
transient
|
||||
label={_('Purge Data')}
|
||||
labelClass='text-red-500'
|
||||
tooltip={_('Erase the book and all its data, including reading progress')}
|
||||
onClick={onPurge}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user