mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(client): confirm destructive Book Dock discards
Require explicit confirmation before row, sheet, or bulk discard deletes files from disk. Provide a separate deselect action for bulk selections. Closes #1007
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const props = defineProps<{
|
||||
selectionPayload: {
|
||||
fileIds?: number[]
|
||||
selectAll?: boolean
|
||||
excludedIds?: number[]
|
||||
status?: string
|
||||
needsReview?: boolean
|
||||
search?: string
|
||||
}
|
||||
selectionCount: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
discarded: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const discarding = ref(false)
|
||||
const error = ref(false)
|
||||
|
||||
function handleClose() {
|
||||
if (!discarding.value) emit('close')
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (discarding.value) return
|
||||
discarding.value = true
|
||||
error.value = false
|
||||
|
||||
try {
|
||||
const response = await api('/api/v1/book-dock/files/discard', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(props.selectionPayload),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
error.value = true
|
||||
return
|
||||
}
|
||||
|
||||
emit('discarded')
|
||||
} catch {
|
||||
error.value = true
|
||||
} finally {
|
||||
discarding.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmDialog
|
||||
open
|
||||
destructive
|
||||
:title="t('bookDock.discardDialog.title', { count: selectionCount })"
|
||||
:description="t('bookDock.discardDialog.description', { count: selectionCount })"
|
||||
:confirm-label="t('bookDock.discardDialog.confirm', { count: selectionCount })"
|
||||
:busy="discarding"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<p v-if="error" class="mt-3 text-sm text-destructive" role="alert">
|
||||
{{ t('bookDock.discardDialog.failed') }}
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
</template>
|
||||
@@ -30,11 +30,11 @@ const props = defineProps<{ file: BookDockFile }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
discarded: []
|
||||
discard: [BookDockFile]
|
||||
updated: [BookDockFile]
|
||||
}>()
|
||||
|
||||
const { saved, saveError, saveMetadata, setTarget, discardFile, coverUrl } = useBookDockDetail()
|
||||
const { saved, saveError, saveMetadata, setTarget, coverUrl } = useBookDockDetail()
|
||||
const { libraries, fetchLibraries: fetchLibs } = useLibraries()
|
||||
|
||||
const meta = computed(() => props.file.selectedMetadata ?? props.file.embeddedMetadata ?? ({} as BookDockMetadata))
|
||||
@@ -195,9 +195,8 @@ function formatDate(iso: string): string {
|
||||
return formatDateTime(new Date(iso))
|
||||
}
|
||||
|
||||
async function handleDiscard() {
|
||||
await discardFile(props.file.id)
|
||||
emit('discarded')
|
||||
function handleDiscard() {
|
||||
emit('discard', props.file)
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ArrowUpDown, Check, FolderPlus, Keyboard, Loader2, PenLine, RefreshCw, Trash2, Wand2 } from '@lucide/vue'
|
||||
import { ArrowUpDown, Check, FolderPlus, Keyboard, Loader2, PenLine, RefreshCw, Trash2, Wand2, X } from '@lucide/vue'
|
||||
import { Permission, type BookDockSummary } from '@bookorbit/types'
|
||||
import { formatBytes } from '@/lib/formatting'
|
||||
import { formatNumber } from '@/i18n/formatters'
|
||||
@@ -29,6 +29,7 @@ const emit = defineEmits<{
|
||||
finalize: []
|
||||
setDestination: []
|
||||
bulkEdit: []
|
||||
deselect: []
|
||||
bulkDiscard: []
|
||||
applyFetched: []
|
||||
retryFetch: []
|
||||
@@ -72,6 +73,9 @@ function emitSetDestination() {
|
||||
function emitBulkEdit() {
|
||||
emit('bulkEdit')
|
||||
}
|
||||
function emitDeselect() {
|
||||
emit('deselect')
|
||||
}
|
||||
function emitBulkDiscard() {
|
||||
emit('bulkDiscard')
|
||||
}
|
||||
@@ -174,6 +178,15 @@ function emitRetryFetch() {
|
||||
<PenLine class="size-3" aria-hidden="true" />
|
||||
{{ t('bookDock.bulkEditAction') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="book-dock-deselect"
|
||||
class="inline-flex h-7 items-center gap-1.5 rounded-lg bg-muted px-2.5 text-[11.5px] font-medium text-foreground transition-colors hover:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
@click="emitDeselect"
|
||||
>
|
||||
<X class="size-3" aria-hidden="true" />
|
||||
{{ t('bookDock.deselect') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="book-dock-bulk-discard"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import BookDockDiscardDialog from '../BookDockDiscardDialog.vue'
|
||||
|
||||
const apiMock = vi.fn<(...args: unknown[]) => Promise<Response>>()
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: (...args: unknown[]) => apiMock(...args),
|
||||
}))
|
||||
|
||||
const ConfirmDialogStub = defineComponent({
|
||||
name: 'ConfirmDialog',
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
confirmLabel: { type: String, required: true },
|
||||
busy: Boolean,
|
||||
},
|
||||
emits: ['confirm', 'cancel'],
|
||||
template: `
|
||||
<div>
|
||||
<h2>{{ title }}</h2>
|
||||
<p>{{ description }}</p>
|
||||
<slot />
|
||||
<button type="button" :disabled="busy" @click="$emit('cancel')">Cancel</button>
|
||||
<button type="button" :disabled="busy" @click="$emit('confirm')">{{ confirmLabel }}</button>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
type SelectionPayload = {
|
||||
fileIds?: number[]
|
||||
selectAll?: boolean
|
||||
excludedIds?: number[]
|
||||
status?: string
|
||||
search?: string
|
||||
}
|
||||
|
||||
function mountDialog(
|
||||
selectionCount = 2,
|
||||
selectionPayload: SelectionPayload = { selectAll: true, excludedIds: [9], status: 'ready', search: 'Dune' },
|
||||
) {
|
||||
return mount(BookDockDiscardDialog, {
|
||||
props: {
|
||||
selectionPayload,
|
||||
selectionCount,
|
||||
},
|
||||
global: {
|
||||
stubs: { ConfirmDialog: ConfirmDialogStub },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('BookDockDiscardDialog', () => {
|
||||
beforeEach(() => {
|
||||
apiMock.mockReset()
|
||||
})
|
||||
|
||||
it('warns that discarding permanently deletes the selected files from disk', () => {
|
||||
const wrapper = mountDialog()
|
||||
|
||||
expect(wrapper.text()).toContain('Discard the 2 selected files?')
|
||||
expect(wrapper.text()).toContain('This permanently deletes the 2 selected files from disk. This action cannot be undone.')
|
||||
expect(wrapper.text()).toContain('Delete 2 files')
|
||||
expect(apiMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses singular warning copy for a row discard', () => {
|
||||
const wrapper = mountDialog(1, { fileIds: [42] })
|
||||
|
||||
expect(wrapper.text()).toContain('Discard the selected file?')
|
||||
expect(wrapper.text()).toContain('This permanently deletes the selected file from disk. This action cannot be undone.')
|
||||
expect(wrapper.text()).toContain('Delete file')
|
||||
})
|
||||
|
||||
it('closes without deleting when cancelled', async () => {
|
||||
const wrapper = mountDialog()
|
||||
|
||||
const cancelButton = wrapper.findAll('button').find((button) => button.text() === 'Cancel')
|
||||
expect(cancelButton).toBeDefined()
|
||||
await cancelButton!.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('close')).toHaveLength(1)
|
||||
expect(apiMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('discards the captured selection only after confirmation', async () => {
|
||||
apiMock.mockResolvedValue({ ok: true } as Response)
|
||||
const wrapper = mountDialog()
|
||||
|
||||
const confirmButton = wrapper.findAll('button').find((button) => button.text() === 'Delete 2 files')
|
||||
expect(confirmButton).toBeDefined()
|
||||
await confirmButton!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(apiMock).toHaveBeenCalledWith('/api/v1/book-dock/files/discard', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ selectAll: true, excludedIds: [9], status: 'ready', search: 'Dune' }),
|
||||
})
|
||||
expect(wrapper.emitted('discarded')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('keeps the dialog open and reports a failed deletion', async () => {
|
||||
apiMock.mockResolvedValue({ ok: false } as Response)
|
||||
const wrapper = mountDialog()
|
||||
|
||||
const confirmButton = wrapper.findAll('button').find((button) => button.text() === 'Delete 2 files')
|
||||
await confirmButton!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.emitted('discarded')).toBeUndefined()
|
||||
expect(wrapper.get('[role="alert"]').text()).toBe('The selected files could not be deleted. Try again.')
|
||||
expect(wrapper.text()).toContain('Discard the 2 selected files?')
|
||||
})
|
||||
})
|
||||
@@ -55,4 +55,12 @@ describe('BookDockFileRow metadata refetch action', () => {
|
||||
|
||||
expect(wrapper.get('[data-testid="book-dock-row-retry"]').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('requests confirmation instead of deleting from the row', async () => {
|
||||
const wrapper = mountRow('ready')
|
||||
|
||||
await wrapper.get('[data-testid="book-dock-row-discard"]').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('discard')).toEqual([[expect.objectContaining({ id: 42 })]])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,7 +18,6 @@ vi.mock('../../composables/useBookDockDetail', () => ({
|
||||
saveError: ref(null),
|
||||
saveMetadata: mocks.saveMetadata,
|
||||
setTarget: vi.fn<(...args: unknown[]) => Promise<null>>().mockResolvedValue(null),
|
||||
discardFile: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
|
||||
coverUrl: (id: number) => `/api/v1/book-dock/files/${id}/cover`,
|
||||
}),
|
||||
}))
|
||||
@@ -248,4 +247,16 @@ describe('BookDockFileSheet metadata search defaults', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('requests confirmation before discarding the file', async () => {
|
||||
const dockFile = makeFile()
|
||||
const wrapper = mountSheet(dockFile)
|
||||
const discardButton = wrapper.findAll('button').find((button) => button.text().trim() === 'Discard')
|
||||
|
||||
expect(discardButton).toBeDefined()
|
||||
await discardButton!.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('discard')).toEqual([[dockFile]])
|
||||
expect(wrapper.emitted('close')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { BookDockSummary } from '@bookorbit/types'
|
||||
import BookDockListBar from '../BookDockListBar.vue'
|
||||
|
||||
vi.mock('@/features/auth/composables/usePermissions', () => ({
|
||||
usePermissions: () => ({
|
||||
hasPermission: () => true,
|
||||
isDemoRestrictedAccount: { value: false },
|
||||
}),
|
||||
}))
|
||||
|
||||
const summary: BookDockSummary = {
|
||||
pending: 0,
|
||||
working: 0,
|
||||
ready: 2,
|
||||
error: 0,
|
||||
needsReview: 0,
|
||||
readyToFile: 2,
|
||||
total: 2,
|
||||
paused: false,
|
||||
}
|
||||
|
||||
function mountListBar(selectionCount: number) {
|
||||
return shallowMount(BookDockListBar, {
|
||||
props: {
|
||||
pageCount: 2,
|
||||
total: 2,
|
||||
totalSizeBytes: null,
|
||||
allOnPageSelected: selectionCount === 2,
|
||||
selectAllMatching: false,
|
||||
selectionCount,
|
||||
fetchedCount: 0,
|
||||
errorCount: 0,
|
||||
sort: 'attention',
|
||||
summary,
|
||||
keyboardActive: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('BookDockListBar selection actions', () => {
|
||||
it('offers a distinct deselect action while files are selected', () => {
|
||||
const wrapper = mountListBar(2)
|
||||
|
||||
expect(wrapper.get('[data-testid="book-dock-deselect"]').text()).toBe('Deselect')
|
||||
expect(wrapper.get('[data-testid="book-dock-bulk-discard"]').text()).toBe('Discard')
|
||||
})
|
||||
|
||||
it('emits deselect without invoking discard', async () => {
|
||||
const wrapper = mountListBar(2)
|
||||
|
||||
await wrapper.get('[data-testid="book-dock-deselect"]').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('deselect')).toHaveLength(1)
|
||||
expect(wrapper.emitted('bulkDiscard')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not show selection actions when nothing is selected', () => {
|
||||
const wrapper = mountListBar(0)
|
||||
|
||||
expect(wrapper.find('[data-testid="book-dock-deselect"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-testid="book-dock-bulk-discard"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { useBookDockFiles } from '../useBookDockFiles'
|
||||
|
||||
describe('useBookDockFiles deleted selections', () => {
|
||||
it('removes a deleted file from an explicit selection', () => {
|
||||
const files = useBookDockFiles()
|
||||
files.total.value = 3
|
||||
files.toggleSelect(1)
|
||||
files.toggleSelect(2)
|
||||
|
||||
files.removeDeletedSelection(1)
|
||||
|
||||
expect(files.selectionCount.value).toBe(1)
|
||||
expect(files.isSelected(1)).toBe(false)
|
||||
expect(files.isSelected(2)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps all remaining matches selected after deleting an excluded row', () => {
|
||||
const files = useBookDockFiles()
|
||||
files.total.value = 3
|
||||
files.toggleSelectAll()
|
||||
files.toggleSelect(1)
|
||||
expect(files.selectionCount.value).toBe(2)
|
||||
|
||||
files.removeDeletedSelection(1)
|
||||
files.total.value = 2
|
||||
|
||||
expect(files.selectionCount.value).toBe(2)
|
||||
expect(files.isSelected(2)).toBe(true)
|
||||
expect(files.isSelected(3)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -68,14 +68,9 @@ export function useBookDockDetail() {
|
||||
return null
|
||||
}
|
||||
|
||||
async function discardFile(id: number) {
|
||||
await api(`/api/v1/book-dock/files/${id}`, { method: 'DELETE' })
|
||||
file.value = null
|
||||
}
|
||||
|
||||
function coverUrl(id: number): string {
|
||||
return `/api/v1/book-dock/files/${id}/cover`
|
||||
}
|
||||
|
||||
return { file, loading, saving, saved, saveError, loadFile, saveMetadata, debouncedSave, setTarget, discardFile, coverUrl }
|
||||
return { file, loading, saving, saved, saveError, loadFile, saveMetadata, debouncedSave, setTarget, coverUrl }
|
||||
}
|
||||
|
||||
@@ -155,6 +155,12 @@ export function useBookDockFiles() {
|
||||
lastToggledId.value = null
|
||||
}
|
||||
|
||||
function removeDeletedSelection(id: number) {
|
||||
selectedIds.value.delete(id)
|
||||
excludedIds.value.delete(id)
|
||||
if (lastToggledId.value === id) lastToggledId.value = null
|
||||
}
|
||||
|
||||
function isSelected(id: number): boolean {
|
||||
if (selectAll.value) return !excludedIds.value.has(id)
|
||||
return selectedIds.value.has(id)
|
||||
@@ -204,6 +210,7 @@ export function useBookDockFiles() {
|
||||
toggleSelect,
|
||||
toggleSelectAll,
|
||||
clearSelection,
|
||||
removeDeletedSelection,
|
||||
isSelected,
|
||||
selectAll,
|
||||
selectionCount,
|
||||
|
||||
@@ -5000,6 +5000,7 @@
|
||||
"apply": "Apply",
|
||||
"done": "Done",
|
||||
"discard": "Discard",
|
||||
"deselect": "Deselect",
|
||||
"finalize": "Finalize",
|
||||
"rescan": "Rescan",
|
||||
"uploadAction": "Upload",
|
||||
@@ -5009,6 +5010,12 @@
|
||||
"bulkEditAction": "Bulk Edit",
|
||||
"applyFetched": "Apply Fetched",
|
||||
"retryErrors": "Retry Errors",
|
||||
"discardDialog": {
|
||||
"title": "{count, plural, =0 {Discard no files?} one {Discard the selected file?} other {Discard the # selected files?}}",
|
||||
"description": "{count, plural, =0 {No files are selected.} one {This permanently deletes the selected file from disk. This action cannot be undone.} other {This permanently deletes the # selected files from disk. This action cannot be undone.}}",
|
||||
"confirm": "{count, plural, one {Delete file} other {Delete # files}}",
|
||||
"failed": "The selected files could not be deleted. Try again."
|
||||
},
|
||||
"commaSeparated": "comma-separated",
|
||||
"destinationLibrary": "Destination Library",
|
||||
"destinationFolder": "Destination Folder",
|
||||
|
||||
@@ -21,6 +21,7 @@ import BookDockFileSheet from '@/features/book-dock/components/BookDockFileSheet
|
||||
import BookDockFinalizeDialog from '@/features/book-dock/components/BookDockFinalizeDialog.vue'
|
||||
import BookDockBulkEditDialog from '@/features/book-dock/components/BookDockBulkEditDialog.vue'
|
||||
import BookDockSetDestinationDialog from '@/features/book-dock/components/BookDockSetDestinationDialog.vue'
|
||||
import BookDockDiscardDialog from '@/features/book-dock/components/BookDockDiscardDialog.vue'
|
||||
|
||||
type ApplyFetchedResult = {
|
||||
total: number
|
||||
@@ -29,6 +30,22 @@ type ApplyFetchedResult = {
|
||||
skippedEdited: number
|
||||
}
|
||||
|
||||
type BookDockSelectionPayload = {
|
||||
fileIds?: number[]
|
||||
selectAll?: boolean
|
||||
excludedIds?: number[]
|
||||
status?: string
|
||||
needsReview?: boolean
|
||||
search?: string
|
||||
}
|
||||
|
||||
type PendingDiscard = {
|
||||
selectionPayload: BookDockSelectionPayload
|
||||
selectionCount: number
|
||||
source: 'bulk' | 'row' | 'sheet'
|
||||
fileId?: number
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
@@ -46,6 +63,7 @@ const {
|
||||
toggleSelect,
|
||||
toggleSelectAll,
|
||||
clearSelection,
|
||||
removeDeletedSelection,
|
||||
isSelected,
|
||||
selectAll,
|
||||
selectionCount,
|
||||
@@ -64,6 +82,7 @@ const selectedFile = ref<BookDockFile | null>(null)
|
||||
const showFinalizeDialog = ref(false)
|
||||
const showBulkEditDialog = ref(false)
|
||||
const showSetDestinationDialog = ref(false)
|
||||
const pendingDiscard = ref<PendingDiscard | null>(null)
|
||||
const dragOver = ref(false)
|
||||
const newFilesDetected = ref(false)
|
||||
const namePreviewByFileId = ref<Record<number, string>>({})
|
||||
@@ -106,11 +125,6 @@ function closeSheet() {
|
||||
selectedFile.value = null
|
||||
}
|
||||
|
||||
function onDiscarded() {
|
||||
selectedFile.value = null
|
||||
refresh()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetchFiles()
|
||||
fetchSummary(true)
|
||||
@@ -183,9 +197,8 @@ function handleSort(field: SortField) {
|
||||
setSort(field)
|
||||
}
|
||||
|
||||
async function handleRowDiscard(file: BookDockFile) {
|
||||
await api(`/api/v1/book-dock/files/${file.id}`, { method: 'DELETE' })
|
||||
refresh()
|
||||
function handleRowDiscard(file: BookDockFile) {
|
||||
openSingleDiscard(file, 'row')
|
||||
}
|
||||
|
||||
async function handleRowRetry(file: BookDockFile) {
|
||||
@@ -204,14 +217,41 @@ function openFinalizeForFile(file: BookDockFile) {
|
||||
showFinalizeDialog.value = true
|
||||
}
|
||||
|
||||
async function handleBulkDiscard() {
|
||||
const payload = getSelectionPayload()
|
||||
await api('/api/v1/book-dock/files/discard', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
clearSelection()
|
||||
function openBulkDiscardDialog() {
|
||||
if (!hasSelection.value) return
|
||||
pendingDiscard.value = {
|
||||
selectionPayload: getSelectionPayload(),
|
||||
selectionCount: selectionCount.value,
|
||||
source: 'bulk',
|
||||
}
|
||||
}
|
||||
|
||||
function openSingleDiscard(file: BookDockFile, source: 'row' | 'sheet') {
|
||||
pendingDiscard.value = {
|
||||
selectionPayload: { fileIds: [file.id] },
|
||||
selectionCount: 1,
|
||||
source,
|
||||
fileId: file.id,
|
||||
}
|
||||
}
|
||||
|
||||
function handleSheetDiscard(file: BookDockFile) {
|
||||
openSingleDiscard(file, 'sheet')
|
||||
}
|
||||
|
||||
function closeDiscardDialog() {
|
||||
pendingDiscard.value = null
|
||||
}
|
||||
|
||||
function handleDiscarded() {
|
||||
const discarded = pendingDiscard.value
|
||||
if (!discarded) return
|
||||
|
||||
if (discarded.source === 'bulk') clearSelection()
|
||||
else if (discarded.fileId !== undefined) removeDeletedSelection(discarded.fileId)
|
||||
if (discarded.source === 'sheet') selectedFile.value = null
|
||||
|
||||
pendingDiscard.value = null
|
||||
refresh()
|
||||
}
|
||||
|
||||
@@ -582,7 +622,8 @@ onUnmounted(() => {
|
||||
@finalize="openFinalize"
|
||||
@set-destination="openSetDestination"
|
||||
@bulk-edit="openBulkEdit"
|
||||
@bulk-discard="handleBulkDiscard"
|
||||
@deselect="clearSelection"
|
||||
@bulk-discard="openBulkDiscardDialog"
|
||||
@apply-fetched="handleApplyFetched"
|
||||
@retry-fetch="handleRetryFetch"
|
||||
/>
|
||||
@@ -621,7 +662,7 @@ onUnmounted(() => {
|
||||
</main>
|
||||
|
||||
<Teleport to="body">
|
||||
<BookDockFileSheet v-if="selectedFile" :file="selectedFile" @close="closeSheet" @discarded="onDiscarded" @updated="onFileUpdated" />
|
||||
<BookDockFileSheet v-if="selectedFile" :file="selectedFile" @close="closeSheet" @discard="handleSheetDiscard" @updated="onFileUpdated" />
|
||||
</Teleport>
|
||||
|
||||
<BookDockFinalizeDialog
|
||||
@@ -647,6 +688,14 @@ onUnmounted(() => {
|
||||
@close="showSetDestinationDialog = false"
|
||||
@updated="onDestinationSet"
|
||||
/>
|
||||
|
||||
<BookDockDiscardDialog
|
||||
v-if="pendingDiscard"
|
||||
:selection-payload="pendingDiscard.selectionPayload"
|
||||
:selection-count="pendingDiscard.selectionCount"
|
||||
@close="closeDiscardDialog"
|
||||
@discarded="handleDiscarded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user