fix(books): restore metadata editing for multiple selected books

Routes multiple book selection to the edit tab instead of details.
Fixes a bug where deactivated kept-alive sources would passively
clobber the active navigation context by tracking component
activation state. Converts route param computed refs to shallow
refs to prevent reactivity issues during navigation.

Closes #455
This commit is contained in:
neon
2026-06-29 00:49:09 +00:00
parent e0a2c94854
commit 246389b050
6 changed files with 70 additions and 10 deletions
@@ -206,7 +206,7 @@ describe('useBookTableShell', () => {
handleEditIndividually() handleEditIndividually()
expect(mocks.setBookContext).toHaveBeenCalledWith([10, 30], 2) expect(mocks.setBookContext).toHaveBeenCalledWith([10, 30], 2)
expect(mocks.routerPush).toHaveBeenCalledWith({ name: 'book-detail', params: { bookId: 10 }, query: { tab: 'details' } }) expect(mocks.routerPush).toHaveBeenCalledWith({ name: 'book-detail', params: { bookId: 10 }, query: { tab: 'edit' } })
expect(mocks.exitSelectionMode).toHaveBeenCalledOnce() expect(mocks.exitSelectionMode).toHaveBeenCalledOnce()
}) })
@@ -1,4 +1,4 @@
import { defineComponent, ref } from 'vue' import { defineComponent, nextTick, ref } from 'vue'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest' import { afterEach, describe, expect, it, vi } from 'vitest'
import type { BookCard } from '@bookorbit/types' import type { BookCard } from '@bookorbit/types'
@@ -112,6 +112,49 @@ describe('useBookViewContext', () => {
expect(loadMore).toHaveBeenCalledTimes(1) expect(loadMore).toHaveBeenCalledTimes(1)
}) })
it('does not let a deactivated kept-alive source passively clobber the active navigation context', async () => {
const nav = useBookNavigation()
const showSource = ref(true)
const slots = ref<BookSlot[]>([makeBook(1), makeBook(2), makeBook(3)])
const total = ref(3)
const loadMore = vi.fn<() => Promise<void>>(async () => {})
const Source = defineComponent({
setup() {
useBookViewContext(slots, total, loadMore)
return {}
},
template: '<div />',
})
const Harness = defineComponent({
components: { Source },
setup() {
return { showSource }
},
template: '<KeepAlive><Source v-if="showSource" /></KeepAlive>',
})
const wrapper = mount(Harness)
await nextTick()
nav.setBookContext([10, 20], 2)
expect(nav.currentIndex(20)).toBe(1)
showSource.value = false
await nextTick()
slots.value = []
total.value = 0
await nextTick()
expect(nav.bookIds.value).toEqual([10, 20])
expect(nav.currentIndex(20)).toBe(1)
expect(nav.total.value).toBe(2)
wrapper.unmount()
})
it('reports the absolute slot index after a jump rail loads a far block', async () => { it('reports the absolute slot index after a jump rail loads a far block', async () => {
const nav = useBookNavigation() const nav = useBookNavigation()
// First block (rows 0-1) plus a far block (rows 5000-5001) loaded; the // First block (rows 0-1) plus a far block (rows 5000-5001) loaded; the
@@ -85,7 +85,7 @@ export function useBookTableShell({ books, querySelection }: BookTableShellOptio
const orderedIds = books.value.filter((book) => selection.selectedIds.value.has(book.id)).map((book) => book.id) const orderedIds = books.value.filter((book) => selection.selectedIds.value.has(book.id)).map((book) => book.id)
if (orderedIds.length === 0) return if (orderedIds.length === 0) return
setBookContext(orderedIds, orderedIds.length) setBookContext(orderedIds, orderedIds.length)
router.push({ name: 'book-detail', params: { bookId: orderedIds[0] }, query: { tab: 'details' } }) router.push({ name: 'book-detail', params: { bookId: orderedIds[0] }, query: { tab: 'edit' } })
selection.exitSelectionMode() selection.exitSelectionMode()
} }
@@ -1,19 +1,36 @@
import { onMounted, watch, type Ref } from 'vue' import { onActivated, onDeactivated, onMounted, watch, type Ref } from 'vue'
import { useBookNavigation } from './useBookNavigation' import { useBookNavigation } from './useBookNavigation'
import type { BookSlot } from './useBookWindow' import type { BookSlot } from './useBookWindow'
export function useBookViewContext(slots: Ref<BookSlot[]>, total: Ref<number>, loadMore: () => Promise<unknown> | unknown) { export function useBookViewContext(slots: Ref<BookSlot[]>, total: Ref<number>, loadMore: () => Promise<unknown> | unknown) {
const { setBookSlotContext, registerLoadMore } = useBookNavigation() const { setBookSlotContext, registerLoadMore } = useBookNavigation()
let active = true
const syncBookContext = () => { const syncBookContext = () => {
if (!active) return
setBookSlotContext(slots.value, total.value) setBookSlotContext(slots.value, total.value)
} }
watch([slots, total], () => syncBookContext(), { immediate: true }) watch([slots, total], () => syncBookContext(), { immediate: true })
onMounted(() => { function registerContextLoader() {
registerLoadMore(async () => { registerLoadMore(async () => {
await loadMore() await loadMore()
setBookSlotContext(slots.value, total.value)
})
}
onMounted(() => {
registerContextLoader()
})
onActivated(() => {
active = true
registerContextLoader()
syncBookContext() syncBookContext()
}) })
onDeactivated(() => {
active = false
}) })
} }
+2 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { AlertTriangle, CheckSquare, FileSpreadsheet, FolderOpen, Layers, Pencil, Search, SlidersHorizontal, Square, X } from '@lucide/vue' import { AlertTriangle, CheckSquare, FileSpreadsheet, FolderOpen, Layers, Pencil, Search, SlidersHorizontal, Square, X } from '@lucide/vue'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
@@ -46,7 +46,7 @@ const router = useRouter()
const { viewMode, effectiveViewMode } = useEffectiveViewMode() const { viewMode, effectiveViewMode } = useEffectiveViewMode()
const { hasPermission, isDemoRestrictedAccount } = usePermissions() const { hasPermission, isDemoRestrictedAccount } = usePermissions()
const collectionId = computed(() => Number(route.params.id)) const collectionId = shallowRef(Number(route.params.id))
const { tableDensity } = useDisplaySettings() const { tableDensity } = useDisplaySettings()
const { allSavedViews, saveView, renameView, deleteView, duplicateView, toggleFavorite, importViews } = useSavedViews('collection', collectionId) const { allSavedViews, saveView, renameView, deleteView, duplicateView, toggleFavorite, importViews } = useSavedViews('collection', collectionId)
const coverAspectRatio = computed(() => DEFAULT_COVER_ASPECT_RATIO) const coverAspectRatio = computed(() => DEFAULT_COVER_ASPECT_RATIO)
+2 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { import {
AlertTriangle, AlertTriangle,
@@ -57,7 +57,7 @@ const { viewMode, effectiveViewMode } = useEffectiveViewMode()
const { hasPermission, isDemoRestrictedAccount } = usePermissions() const { hasPermission, isDemoRestrictedAccount } = usePermissions()
const { smartScopeFilterExpanded } = useDisplaySettings() const { smartScopeFilterExpanded } = useDisplaySettings()
const smartScopeId = computed(() => Number(route.params.id)) const smartScopeId = shallowRef(Number(route.params.id))
const coverAspectRatio = computed(() => DEFAULT_COVER_ASPECT_RATIO) const coverAspectRatio = computed(() => DEFAULT_COVER_ASPECT_RATIO)
const { coverSize, gridGap } = useViewDisplaySettings('smartScope', smartScopeId, coverAspectRatio) const { coverSize, gridGap } = useViewDisplaySettings('smartScope', smartScopeId, coverAspectRatio)
const { tableDensity } = useDisplaySettings() const { tableDensity } = useDisplaySettings()