mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-29 00:01:42 +08:00
fix(files): show batch operation failure details
This commit is contained in:
@@ -18,6 +18,7 @@ interface DeleteConfirmDialogProps {
|
||||
isPending: boolean
|
||||
operation?: OperationProgressState | null
|
||||
onCancelOperation?: () => void
|
||||
onDismissOperation?: () => void
|
||||
}
|
||||
|
||||
export function DeleteConfirmDialog({
|
||||
@@ -28,6 +29,7 @@ export function DeleteConfirmDialog({
|
||||
isPending,
|
||||
operation,
|
||||
onCancelOperation,
|
||||
onDismissOperation,
|
||||
}: DeleteConfirmDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const running = !!operation
|
||||
@@ -49,7 +51,11 @@ export function DeleteConfirmDialog({
|
||||
{!running && <DialogDescription>{t('files.trashConfirmDescription', { count })}</DialogDescription>}
|
||||
</DialogHeader>
|
||||
{operation ? (
|
||||
<OperationProgress operation={operation} onCancel={onCancelOperation ?? (() => {})} />
|
||||
<OperationProgress
|
||||
operation={operation}
|
||||
onCancel={onCancelOperation ?? (() => {})}
|
||||
onClose={onDismissOperation}
|
||||
/>
|
||||
) : (
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
|
||||
@@ -16,6 +16,7 @@ interface MoveDialogProps {
|
||||
excludeIds: string[]
|
||||
operation?: OperationProgressState | null
|
||||
onCancelOperation?: () => void
|
||||
onDismissOperation?: () => void
|
||||
}
|
||||
|
||||
function buildPath(parent: string, name: string): string {
|
||||
@@ -30,6 +31,7 @@ export function MoveDialog({
|
||||
excludeIds,
|
||||
operation,
|
||||
onCancelOperation,
|
||||
onDismissOperation,
|
||||
}: MoveDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const [browsingPath, setBrowsingPath] = useState('')
|
||||
@@ -84,7 +86,11 @@ export function MoveDialog({
|
||||
</DialogHeader>
|
||||
|
||||
{operation ? (
|
||||
<OperationProgress operation={operation} onCancel={onCancelOperation ?? (() => {})} />
|
||||
<OperationProgress
|
||||
operation={operation}
|
||||
onCancel={onCancelOperation ?? (() => {})}
|
||||
onClose={onDismissOperation}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<nav className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import type React from 'react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DeleteConfirmDialog } from './delete-confirm-dialog'
|
||||
import { MoveDialog } from './move-dialog'
|
||||
import { OperationProgress, type OperationProgressState } from './operation-progress'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/dialog', () => ({
|
||||
Dialog: ({ open, children }: { open?: boolean; children: React.ReactNode }) => (open ? <div>{children}</div> : null),
|
||||
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogDescription: ({ children }: { children: React.ReactNode }) => <p>{children}</p>,
|
||||
DialogFooter: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => <h2>{children}</h2>,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/progress', () => ({
|
||||
Progress: ({ value }: { value?: number }) => <div data-testid="progress">{value}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-files-query', () => ({
|
||||
useFilesQuery: () => ({ data: { items: [] }, isLoading: false }),
|
||||
}))
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
function failedOperation(): OperationProgressState {
|
||||
return {
|
||||
title: 'Move',
|
||||
total: 3,
|
||||
completed: 3,
|
||||
currentName: '',
|
||||
cancelRequested: false,
|
||||
finished: true,
|
||||
failures: [
|
||||
{ name: 'Budget.xlsx', message: 'Name already exists' },
|
||||
{ name: 'Archive', message: 'Permission denied' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
describe('OperationProgress', () => {
|
||||
it('renders each failed item with its error reason', () => {
|
||||
render(<OperationProgress operation={failedOperation()} onCancel={vi.fn()} onClose={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('Budget.xlsx')).toBeTruthy()
|
||||
expect(screen.getByText('Name already exists')).toBeTruthy()
|
||||
expect(screen.getByText('Archive')).toBeTruthy()
|
||||
expect(screen.getByText('Permission denied')).toBeTruthy()
|
||||
expect(screen.getByText('common.close')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('calls onClose from the finished failure state', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<OperationProgress operation={failedOperation()} onCancel={vi.fn()} onClose={onClose} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.close'))
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps cancel available while the operation is still running', () => {
|
||||
const onCancel = vi.fn()
|
||||
render(
|
||||
<OperationProgress
|
||||
operation={{ ...failedOperation(), finished: false, completed: 1, failures: [] }}
|
||||
onCancel={onCancel}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('common.cancel'))
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeleteConfirmDialog', () => {
|
||||
it('shows failure details inside the delete dialog execution state', () => {
|
||||
render(
|
||||
<DeleteConfirmDialog
|
||||
open
|
||||
count={3}
|
||||
isPending
|
||||
operation={failedOperation()}
|
||||
onCancelOperation={vi.fn()}
|
||||
onDismissOperation={vi.fn()}
|
||||
onConfirm={vi.fn()}
|
||||
onOpenChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('files.trashConfirmTitle')).toBeTruthy()
|
||||
expect(screen.getByText('Budget.xlsx')).toBeTruthy()
|
||||
expect(screen.getByText('Permission denied')).toBeTruthy()
|
||||
expect(screen.queryByText('files.trashConfirmDescription')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('MoveDialog', () => {
|
||||
it('shows failure details inside the move dialog execution state', () => {
|
||||
render(
|
||||
<MoveDialog
|
||||
open
|
||||
isPending
|
||||
operation={failedOperation()}
|
||||
onCancelOperation={vi.fn()}
|
||||
onDismissOperation={vi.fn()}
|
||||
excludeIds={[]}
|
||||
onConfirm={vi.fn()}
|
||||
onOpenChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('files.moveTo')).toBeTruthy()
|
||||
expect(screen.getByText('Budget.xlsx')).toBeTruthy()
|
||||
expect(screen.getByText('Name already exists')).toBeTruthy()
|
||||
expect(screen.queryByText('files.noFolders')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -2,22 +2,31 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
|
||||
export interface OperationFailure {
|
||||
name: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface OperationProgressState {
|
||||
title: string
|
||||
total: number
|
||||
completed: number
|
||||
currentName: string
|
||||
cancelRequested: boolean
|
||||
finished: boolean
|
||||
failures: OperationFailure[]
|
||||
}
|
||||
|
||||
interface OperationProgressProps {
|
||||
operation: OperationProgressState
|
||||
onCancel: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export function OperationProgress({ operation, onCancel }: OperationProgressProps) {
|
||||
export function OperationProgress({ operation, onCancel, onClose }: OperationProgressProps) {
|
||||
const { t } = useTranslation()
|
||||
const value = operation.total > 0 ? Math.round((operation.completed / operation.total) * 100) : 0
|
||||
const hasFailures = operation.failures.length > 0
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -32,10 +41,35 @@ export function OperationProgress({ operation, onCancel }: OperationProgressProp
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={value} />
|
||||
{hasFailures && (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/5">
|
||||
<div className="border-b border-destructive/20 px-3 py-2 text-sm font-medium text-destructive">
|
||||
{t('files.operationFailuresTitle', { count: operation.failures.length })}
|
||||
</div>
|
||||
<div className="max-h-44 overflow-y-auto">
|
||||
{operation.failures.map((failure) => (
|
||||
<div key={`${failure.name}-${failure.message}`} className="border-b px-3 py-2 last:border-0">
|
||||
<div className="truncate text-sm font-medium" title={failure.name}>
|
||||
{failure.name}
|
||||
</div>
|
||||
<div className="mt-0.5 line-clamp-2 text-xs text-muted-foreground" title={failure.message}>
|
||||
{failure.message}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" onClick={onCancel} disabled={operation.cancelRequested}>
|
||||
{operation.cancelRequested ? t('files.operationCanceling') : t('common.cancel')}
|
||||
</Button>
|
||||
{operation.finished ? (
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" onClick={onCancel} disabled={operation.cancelRequested}>
|
||||
{operation.cancelRequested ? t('files.operationCanceling') : t('common.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import type React from 'react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { OperationProgressState } from './dialogs/operation-progress'
|
||||
import { FileManagerDialogs } from './file-manager-dialogs'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
useNavigate: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/dialog', () => ({
|
||||
Dialog: ({ open, children }: { open?: boolean; children: React.ReactNode }) => (open ? <div>{children}</div> : null),
|
||||
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogDescription: ({ children }: { children: React.ReactNode }) => <p>{children}</p>,
|
||||
DialogFooter: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => <h2>{children}</h2>,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/progress', () => ({
|
||||
Progress: ({ value }: { value?: number }) => <div data-testid="progress">{value}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./dialogs/rename-dialog', () => ({
|
||||
RenameDialog: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('./dialogs/new-folder-dialog', () => ({
|
||||
NewFolderDialog: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('./dialogs/name-conflict-dialog', () => ({
|
||||
NameConflictDialog: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('./dialogs/share-dialog', () => ({
|
||||
ShareDialog: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('./dialogs/delete-confirm-dialog', () => ({
|
||||
DeleteConfirmDialog: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('./dialogs/move-dialog', () => ({
|
||||
MoveDialog: () => null,
|
||||
}))
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
function failedOperation(): OperationProgressState {
|
||||
return {
|
||||
title: 'Restore',
|
||||
total: 2,
|
||||
completed: 2,
|
||||
currentName: '',
|
||||
cancelRequested: false,
|
||||
finished: true,
|
||||
failures: [{ name: 'Archive', message: 'Permission denied' }],
|
||||
}
|
||||
}
|
||||
|
||||
describe('FileManagerDialogs', () => {
|
||||
it('shows failure details in the generic batch operation dialog', () => {
|
||||
render(
|
||||
<FileManagerDialogs
|
||||
renameTarget={null}
|
||||
onRenameClose={vi.fn()}
|
||||
onRenameConfirm={vi.fn()}
|
||||
renamePending={false}
|
||||
showNewFolder={false}
|
||||
onNewFolderClose={vi.fn()}
|
||||
onNewFolderConfirm={vi.fn()}
|
||||
newFolderPending={false}
|
||||
deleteTargetIds={[]}
|
||||
operation={failedOperation()}
|
||||
onOperationCancel={vi.fn()}
|
||||
onOperationDismiss={vi.fn()}
|
||||
onDeleteClose={vi.fn()}
|
||||
onDeleteConfirm={vi.fn()}
|
||||
deletePending={false}
|
||||
moveTargetIds={[]}
|
||||
onMoveClose={vi.fn()}
|
||||
onMoveConfirm={vi.fn()}
|
||||
movePending={false}
|
||||
shareTarget={null}
|
||||
onShareClose={vi.fn()}
|
||||
conflictDialogState={{
|
||||
request: null,
|
||||
applyToAll: false,
|
||||
onApplyToAllChange: vi.fn(),
|
||||
onChoose: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getAllByText('Restore').length).toBeGreaterThan(0)
|
||||
expect(screen.getByText('Archive')).toBeTruthy()
|
||||
expect(screen.getByText('Permission denied')).toBeTruthy()
|
||||
expect(screen.getByText('common.close')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -22,6 +22,7 @@ interface FileManagerDialogsProps {
|
||||
deleteTargetIds: string[]
|
||||
operation: OperationProgressState | null
|
||||
onOperationCancel: () => void
|
||||
onOperationDismiss: () => void
|
||||
onDeleteClose: () => void
|
||||
onDeleteConfirm: () => void
|
||||
deletePending: boolean
|
||||
@@ -62,6 +63,7 @@ export function FileManagerDialogs(props: FileManagerDialogsProps) {
|
||||
count={props.deleteTargetIds.length}
|
||||
operation={props.deleteTargetIds.length > 0 ? props.operation : null}
|
||||
onCancelOperation={props.onOperationCancel}
|
||||
onDismissOperation={props.onOperationDismiss}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) props.onDeleteClose()
|
||||
}}
|
||||
@@ -79,6 +81,7 @@ export function FileManagerDialogs(props: FileManagerDialogsProps) {
|
||||
excludeIds={props.moveTargetIds}
|
||||
operation={props.moveTargetIds.length > 0 ? props.operation : null}
|
||||
onCancelOperation={props.onOperationCancel}
|
||||
onDismissOperation={props.onOperationDismiss}
|
||||
/>
|
||||
|
||||
<Dialog open={!!props.operation && props.deleteTargetIds.length === 0 && props.moveTargetIds.length === 0}>
|
||||
@@ -86,7 +89,13 @@ export function FileManagerDialogs(props: FileManagerDialogsProps) {
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.operation?.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{props.operation && <OperationProgress operation={props.operation} onCancel={props.onOperationCancel} />}
|
||||
{props.operation && (
|
||||
<OperationProgress
|
||||
operation={props.operation}
|
||||
onCancel={props.onOperationCancel}
|
||||
onClose={props.onOperationDismiss}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
@@ -406,7 +406,15 @@ export function FileManager({
|
||||
) => {
|
||||
operationCancelRef.current = false
|
||||
const namesById = new Map(items.map((item) => [item.id, item.name]))
|
||||
setOperationState({ title, total: ids.length, completed: 0, currentName: '', cancelRequested: false })
|
||||
setOperationState({
|
||||
title,
|
||||
total: ids.length,
|
||||
completed: 0,
|
||||
currentName: '',
|
||||
cancelRequested: false,
|
||||
finished: false,
|
||||
failures: [],
|
||||
})
|
||||
|
||||
const result = await runSequentialOperation({
|
||||
items: ids,
|
||||
@@ -417,18 +425,30 @@ export function FileManager({
|
||||
onItemComplete: (_id, index) => {
|
||||
setOperationState((state) => (state ? { ...state, completed: index + 1 } : state))
|
||||
},
|
||||
onItemFailure: (_id, _error, index) => {
|
||||
setOperationState((state) => (state ? { ...state, completed: index + 1 } : state))
|
||||
onItemFailure: (id, error, index) => {
|
||||
setOperationState((state) =>
|
||||
state
|
||||
? {
|
||||
...state,
|
||||
completed: index + 1,
|
||||
failures: [
|
||||
...state.failures,
|
||||
{ name: namesById.get(id) ?? id, message: error.message || t('common.error') },
|
||||
],
|
||||
}
|
||||
: state,
|
||||
)
|
||||
},
|
||||
runItem: action,
|
||||
})
|
||||
|
||||
invalidation()
|
||||
setOperationState(null)
|
||||
if (result.failed.length > 0) {
|
||||
setOperationState((state) => (state ? { ...state, finished: true, currentName: '' } : state))
|
||||
toast.error(t('files.operationFailedSummary', { failed: result.failed.length, total: ids.length }))
|
||||
return result
|
||||
}
|
||||
setOperationState(null)
|
||||
if (result.cancelled) {
|
||||
toast.info(t('files.operationCancelled', { completed: result.completed, total: ids.length }))
|
||||
return result
|
||||
@@ -444,6 +464,13 @@ export function FileManager({
|
||||
setOperationState((state) => (state ? { ...state, cancelRequested: true } : state))
|
||||
}
|
||||
|
||||
function dismissOperation() {
|
||||
setOperationState(null)
|
||||
if (deleteTargetIds.length > 0) setDeleteTargetIds([])
|
||||
if (moveTargetIds.length > 0) setMoveTargetIds([])
|
||||
setRowSelection({})
|
||||
}
|
||||
|
||||
function handleDndDrop(fileIds: string[], targetFolderId: string) {
|
||||
conflict.reset()
|
||||
runFileOperation(
|
||||
@@ -642,6 +669,7 @@ export function FileManager({
|
||||
deleteTargetIds={deleteTargetIds}
|
||||
operation={operationState}
|
||||
onOperationCancel={requestOperationCancel}
|
||||
onOperationDismiss={dismissOperation}
|
||||
onDeleteClose={() => setDeleteTargetIds([])}
|
||||
onDeleteConfirm={() => {
|
||||
const ids = [...deleteTargetIds]
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"files.operationCanceling": "Canceling...",
|
||||
"files.operationCancelled": "Canceled after {{completed}} / {{total}} item(s)",
|
||||
"files.operationFailedSummary": "{{failed}} / {{total}} item(s) failed",
|
||||
"files.operationFailuresTitle": "{{count}} failed item(s)",
|
||||
"files.uploadBatchCancelled": "{{count}} remaining upload(s) cancelled",
|
||||
"tasks.title": "Tasks",
|
||||
"tasks.count": "{{count}} tasks",
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"files.operationCanceling": "正在取消...",
|
||||
"files.operationCancelled": "已取消,完成 {{completed}} / {{total}} 项",
|
||||
"files.operationFailedSummary": "{{failed}} / {{total}} 项失败",
|
||||
"files.operationFailuresTitle": "{{count}} 个失败项目",
|
||||
"files.uploadBatchCancelled": "已取消剩余 {{count}} 个上传",
|
||||
"tasks.title": "任务",
|
||||
"tasks.count": "{{count}} 个任务",
|
||||
|
||||
@@ -52,7 +52,15 @@ function TrashPage() {
|
||||
): Promise<void> {
|
||||
operationCancelRef.current = false
|
||||
const namesById = new Map(items.map((item) => [item.id, item.name]))
|
||||
setOperationState({ title, total: ids.length, completed: 0, currentName: '', cancelRequested: false })
|
||||
setOperationState({
|
||||
title,
|
||||
total: ids.length,
|
||||
completed: 0,
|
||||
currentName: '',
|
||||
cancelRequested: false,
|
||||
finished: false,
|
||||
failures: [],
|
||||
})
|
||||
|
||||
const result = await runSequentialOperation({
|
||||
items: ids,
|
||||
@@ -63,16 +71,28 @@ function TrashPage() {
|
||||
onItemComplete: (_id, index) => {
|
||||
setOperationState((state) => (state ? { ...state, completed: index + 1 } : state))
|
||||
},
|
||||
onItemFailure: (_id, _error, index) => {
|
||||
setOperationState((state) => (state ? { ...state, completed: index + 1 } : state))
|
||||
onItemFailure: (id, error, index) => {
|
||||
setOperationState((state) =>
|
||||
state
|
||||
? {
|
||||
...state,
|
||||
completed: index + 1,
|
||||
failures: [
|
||||
...state.failures,
|
||||
{ name: namesById.get(id) ?? id, message: error.message || t('common.error') },
|
||||
],
|
||||
}
|
||||
: state,
|
||||
)
|
||||
},
|
||||
runItem,
|
||||
})
|
||||
|
||||
setOperationState(null)
|
||||
if (result.failed.length > 0) {
|
||||
setOperationState((state) => (state ? { ...state, finished: true, currentName: '' } : state))
|
||||
throw new Error(t('files.operationFailedSummary', { failed: result.failed.length, total: ids.length }))
|
||||
}
|
||||
setOperationState(null)
|
||||
if (result.cancelled) {
|
||||
toast.info(t('files.operationCancelled', { completed: result.completed, total: ids.length }))
|
||||
}
|
||||
@@ -83,6 +103,13 @@ function TrashPage() {
|
||||
setOperationState((state) => (state ? { ...state, cancelRequested: true } : state))
|
||||
}
|
||||
|
||||
function dismissOperation() {
|
||||
setOperationState(null)
|
||||
setConfirmDialog(null)
|
||||
setPendingDeleteIds([])
|
||||
setSelectedIds(new Set())
|
||||
}
|
||||
|
||||
async function runRestore(ids: string[]) {
|
||||
conflict.reset()
|
||||
const showApplyToAll = ids.length > 1
|
||||
@@ -254,7 +281,11 @@ function TrashPage() {
|
||||
)}
|
||||
</DialogHeader>
|
||||
{operationState ? (
|
||||
<OperationProgress operation={operationState} onCancel={requestOperationCancel} />
|
||||
<OperationProgress
|
||||
operation={operationState}
|
||||
onCancel={requestOperationCancel}
|
||||
onClose={dismissOperation}
|
||||
/>
|
||||
) : (
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setConfirmDialog(null)}>
|
||||
@@ -277,7 +308,13 @@ function TrashPage() {
|
||||
<DialogHeader>
|
||||
<DialogTitle>{operationState?.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{operationState && <OperationProgress operation={operationState} onCancel={requestOperationCancel} />}
|
||||
{operationState && (
|
||||
<OperationProgress
|
||||
operation={operationState}
|
||||
onCancel={requestOperationCancel}
|
||||
onClose={dismissOperation}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user