mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
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:
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user