mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
fix(metadata): refetch Book Dock metadata on demand
Allow ready Book Dock rows to refresh provider matches without depending on the failed-only bulk retry path. Preserve bounded queueing, ownership checks, and stale metadata cleanup. Fixes #1118
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(),
|
||||
}))
|
||||
|
||||
import { api } from '@/lib/api'
|
||||
import { refetchBookDockMetadata } from './book-dock.api'
|
||||
|
||||
const mockApi = vi.mocked(api)
|
||||
|
||||
describe('book-dock.api', () => {
|
||||
beforeEach(() => {
|
||||
mockApi.mockReset()
|
||||
})
|
||||
|
||||
it('posts to the dedicated per-file metadata refetch endpoint', async () => {
|
||||
mockApi.mockResolvedValue({ ok: true } as Response)
|
||||
|
||||
await expect(refetchBookDockMetadata(42)).resolves.toBe(true)
|
||||
|
||||
expect(mockApi).toHaveBeenCalledWith('/api/v1/book-dock/files/42/refetch-metadata', {
|
||||
method: 'POST',
|
||||
})
|
||||
})
|
||||
|
||||
it('reports a rejected refetch response', async () => {
|
||||
mockApi.mockResolvedValue({ ok: false } as Response)
|
||||
|
||||
await expect(refetchBookDockMetadata(42)).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
export async function refetchBookDockMetadata(fileId: number): Promise<boolean> {
|
||||
const response = await api(`/api/v1/book-dock/files/${fileId}/refetch-metadata`, {
|
||||
method: 'POST',
|
||||
})
|
||||
return response.ok
|
||||
}
|
||||
@@ -58,6 +58,7 @@ const confidence = computed(() => props.file.confidence)
|
||||
const hasScore = computed(() => confidence.value !== null && confidence.value !== undefined && confidence.value > 0)
|
||||
const weakMatch = computed(() => hasScore.value && (confidence.value as number) < 85)
|
||||
const noMatchYet = computed(() => props.file.status === 'ready' && !hasScore.value)
|
||||
const canRefetch = computed(() => props.file.status === 'ready' || props.file.status === 'error')
|
||||
|
||||
const edgeClass = computed(() => {
|
||||
if (props.file.status === 'error' || props.conflict) return 'border-l-red-500/60'
|
||||
@@ -213,9 +214,10 @@ function onRetry() {
|
||||
<button
|
||||
type="button"
|
||||
data-testid="book-dock-row-retry"
|
||||
class="grid size-7 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
class="grid size-7 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-muted-foreground"
|
||||
:title="t('bookDock.layout.row.refetch')"
|
||||
:aria-label="t('bookDock.layout.row.refetch')"
|
||||
:disabled="!canRefetch"
|
||||
@click="onRetry"
|
||||
>
|
||||
<RotateCw class="size-3.5" />
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { BookDockFile } from '@bookorbit/types'
|
||||
import BookDockFileRow from '../BookDockFileRow.vue'
|
||||
import en from '@/locales/en.json'
|
||||
|
||||
function file(status: BookDockFile['status']): BookDockFile {
|
||||
return {
|
||||
id: 42,
|
||||
fileName: 'book.epub',
|
||||
fileSize: 1024,
|
||||
format: 'epub',
|
||||
status,
|
||||
embeddedMetadata: { title: 'Book' },
|
||||
selectedMetadata: null,
|
||||
fetchedMetadata: null,
|
||||
targetLibraryId: null,
|
||||
targetFolderId: null,
|
||||
confidence: null,
|
||||
fetchedMetadataSources: null,
|
||||
errorMessage: null,
|
||||
metadataEditedAt: null,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
}
|
||||
}
|
||||
|
||||
function mountRow(status: BookDockFile['status']) {
|
||||
const i18n = createI18n({ legacy: false, locale: 'en', messages: { en } })
|
||||
return shallowMount(BookDockFileRow, {
|
||||
props: {
|
||||
file: file(status),
|
||||
selected: false,
|
||||
expanded: false,
|
||||
focused: false,
|
||||
libraryName: null,
|
||||
targetLabel: 'Unassigned',
|
||||
},
|
||||
global: { plugins: [i18n] },
|
||||
})
|
||||
}
|
||||
|
||||
describe('BookDockFileRow metadata refetch action', () => {
|
||||
it.each(['ready', 'error'] as const)('emits retry for a %s file', async (status) => {
|
||||
const wrapper = mountRow(status)
|
||||
|
||||
await wrapper.get('[data-testid="book-dock-row-retry"]').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('retry')).toEqual([[expect.objectContaining({ id: 42, status })]])
|
||||
})
|
||||
|
||||
it.each(['pending', 'extracting', 'fetching'] as const)('disables refetch while a file is %s', (status) => {
|
||||
const wrapper = mountRow(status)
|
||||
|
||||
expect(wrapper.get('[data-testid="book-dock-row-retry"]').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,7 @@ import { useBookDockSummary } from '@/features/book-dock/composables/useBookDock
|
||||
import { useBookDockStatistics } from '@/features/book-dock/composables/useBookDockStatistics'
|
||||
import { useBookDockUpload, SUPPORTED_FORMATS, SUPPORTED_FORMATS_ACCEPT } from '@/features/book-dock/composables/useBookDockUpload'
|
||||
import { useBookDockConflicts } from '@/features/book-dock/composables/useBookDockConflicts'
|
||||
import { refetchBookDockMetadata } from '@/features/book-dock/api/book-dock.api'
|
||||
import BookDockActionCluster from '@/features/book-dock/components/BookDockActionCluster.vue'
|
||||
import BookDockFilterBar from '@/features/book-dock/components/BookDockFilterBar.vue'
|
||||
import BookDockFileListView from '@/features/book-dock/components/BookDockFileListView.vue'
|
||||
@@ -188,12 +189,7 @@ async function handleRowDiscard(file: BookDockFile) {
|
||||
}
|
||||
|
||||
async function handleRowRetry(file: BookDockFile) {
|
||||
await api('/api/v1/book-dock/files/retry-fetch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ fileIds: [file.id] }),
|
||||
})
|
||||
refresh()
|
||||
if (await refetchBookDockMetadata(file.id)) refresh()
|
||||
}
|
||||
|
||||
function openDestinationForFile(file: BookDockFile) {
|
||||
|
||||
Reference in New Issue
Block a user