fix(library): expose rename setting in scoped library list

Return fileRenameEnabled for non-superuser library responses so the bulk rename selector includes configured libraries.

Closes #918
This commit is contained in:
neon
2026-08-08 04:26:17 +00:00
parent ddf285f568
commit 27040d5dee
4 changed files with 163 additions and 1 deletions
@@ -0,0 +1,141 @@
import { flushPromises, shallowMount } from '@vue/test-utils'
import { ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Library } from '@bookorbit/types'
const libraries = ref<Library[]>([])
const fetchLibraries = vi.fn<() => Promise<void>>()
const selectLibrary = vi.fn<(libraryId: number) => void>()
const loadPreview = vi.fn<() => Promise<void>>()
const bulk = {
selectedLibraryId: ref<number | null>(null),
page: ref(1),
pageSize: ref(50),
statusFilter: ref(undefined),
totalPages: ref(1),
previewItems: ref([]),
previewTotal: ref(0),
totalByStatus: ref({ will_rename: 0, unchanged: 0, collision: 0, no_pattern: 0, error: 0 }),
loading: ref(false),
previewError: ref<string | null>(null),
executing: ref(false),
executionStats: ref(null),
executionError: ref<string | null>(null),
selectLibrary,
loadPreview,
execute: vi.fn<() => Promise<void>>(),
cancelExecution: vi.fn<() => void>(),
setPage: vi.fn<(page: number) => void>(),
setStatusFilter: vi.fn<() => void>(),
}
vi.mock('@/features/library/composables/useLibraries', () => ({
useLibraries: () => ({ libraries, fetchLibraries }),
}))
vi.mock('../../composables/useBulkRename', () => ({
useBulkRename: () => bulk,
}))
vi.mock('@vueuse/core', () => ({
useElementSize: () => ({ width: ref(1024), height: ref(768) }),
}))
import BulkRenameView from './BulkRenameView.vue'
function makeLibrary(overrides: Partial<Library> = {}): Library {
return {
id: 1,
name: 'Enabled Library',
icon: null,
displayOrder: 0,
coverAspectRatio: '2/3',
watch: false,
autoScanCronExpression: null,
metadataPrecedence: [],
formatPriority: [],
allowedFormats: [],
organizationMode: 'book_per_file',
excludePatterns: [],
readingThreshold: 10,
markAsFinishedPercentComplete: 95,
fileNamingPattern: null,
fileWriteEnabled: false,
fileWriteWriteCover: false,
fileWriteEpubEnabled: false,
fileWriteEpubMaxFileSizeMb: 100,
fileWriteFb2Enabled: false,
fileWriteFb2MaxFileSizeMb: 100,
fileWritePdfEnabled: false,
fileWritePdfMaxFileSizeMb: 100,
fileWriteCbxEnabled: false,
fileWriteCbxMaxFileSizeMb: 500,
fileWriteKindleEnabled: false,
fileWriteKindleMaxFileSizeMb: 100,
fileWriteAudioEnabled: false,
fileWriteAudioMaxFileSizeMb: 500,
fileRenameEnabled: true,
folders: [],
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
...overrides,
}
}
function mountView() {
return shallowMount(BulkRenameView, {
global: {
stubs: {
BulkRenameConfirmDialog: true,
},
},
})
}
describe('BulkRenameView library eligibility', () => {
beforeEach(() => {
vi.clearAllMocks()
fetchLibraries.mockResolvedValue(undefined)
loadPreview.mockResolvedValue(undefined)
libraries.value = []
bulk.selectedLibraryId.value = null
})
it('lists every library with file rename enabled and excludes disabled libraries', async () => {
libraries.value = [
makeLibrary({ id: 1, name: 'First Enabled Library' }),
makeLibrary({ id: 2, name: 'Rename Disabled', fileRenameEnabled: false }),
makeLibrary({ id: 3, name: 'Second Enabled Library' }),
]
const wrapper = mountView()
await flushPromises()
const options = wrapper.findAll('select option').slice(1)
expect(options.map((option) => option.text())).toEqual(['First Enabled Library', 'Second Enabled Library'])
expect(wrapper.text()).not.toContain('No libraries have file rename enabled')
expect(fetchLibraries).toHaveBeenCalledOnce()
})
it('shows the empty state when every library has file rename disabled', async () => {
libraries.value = [makeLibrary({ id: 1, fileRenameEnabled: false }), makeLibrary({ id: 2, fileRenameEnabled: false })]
const wrapper = mountView()
await flushPromises()
expect(wrapper.findAll('select option')).toHaveLength(1)
expect(wrapper.text()).toContain('No libraries have file rename enabled')
})
it('selects an eligible library and loads its preview', async () => {
libraries.value = [makeLibrary({ id: 17 })]
const wrapper = mountView()
await flushPromises()
await wrapper.get('select').setValue('17')
expect(selectLibrary).toHaveBeenCalledWith(17)
expect(loadPreview).toHaveBeenCalledOnce()
})
})
@@ -6,6 +6,7 @@ vi.mock('drizzle-orm', () => ({
sql: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({ op: 'sql', text: strings.join(''), values })),
}));
import { libraries } from '../../db/schema';
import { LibraryRepository } from './library.repository';
describe('LibraryRepository', () => {
@@ -51,6 +52,24 @@ describe('LibraryRepository', () => {
expect(txUpdateSet).toHaveBeenNthCalledWith(2, { displayOrder: 6 });
});
it('findAllForUser includes file rename eligibility in the scoped library projection', async () => {
const orderBy = vi.fn().mockResolvedValue([]);
const groupBy = vi.fn().mockReturnValue({ orderBy });
const leftJoin = vi.fn().mockReturnValue({ groupBy });
const innerJoin = vi.fn().mockReturnValue({ leftJoin });
const from = vi.fn().mockReturnValue({ innerJoin });
db.select.mockReturnValue({ from });
await repo.findAllForUser(42);
expect(db.select).toHaveBeenCalledWith(
expect.objectContaining({
fileRenameEnabled: libraries.fileRenameEnabled,
}),
);
expect(orderBy).toHaveBeenCalledWith(libraries.displayOrder, libraries.name);
});
it('getStats aggregates counts, sizes, and format map', async () => {
const countChain = {
from: vi.fn().mockReturnValue({
@@ -39,6 +39,7 @@ export class LibraryRepository {
displayOrder: libraries.displayOrder,
coverAspectRatio: libraries.coverAspectRatio,
scanMode: libraries.scanMode,
fileRenameEnabled: libraries.fileRenameEnabled,
createdAt: libraries.createdAt,
updatedAt: libraries.updatedAt,
bookCount: sql<number>`count(${books.id})::int`,
@@ -95,7 +95,7 @@ describe('LibraryService', () => {
});
it('findAll uses scoped folder query for non-superusers', async () => {
libraryRepo.findAllForUser.mockResolvedValue([{ id: 10, name: 'A', coverAspectRatio: '1/1' }]);
libraryRepo.findAllForUser.mockResolvedValue([{ id: 10, name: 'A', coverAspectRatio: '1/1', fileRenameEnabled: true }]);
libraryRepo.findFoldersByLibraryIds.mockResolvedValue([{ id: 1, libraryId: 10, path: '/a', createdAt: new Date() }]);
const result = await service.findAll({ id: 7, isSuperuser: false, contentFilters: EMPTY_CONTENT_FILTER_RULES } as any);
@@ -105,6 +105,7 @@ describe('LibraryService', () => {
expect(libraryRepo.findAllFolders).not.toHaveBeenCalled();
expect(result[0].folders).toEqual([{ id: 1, path: '/a', createdAt: expect.any(Date) }]);
expect(result[0].coverAspectRatio).toBe('1/1');
expect(result[0].fileRenameEnabled).toBe(true);
});
it('passes contentFilters to findAllForUser for non-superuser and skips for superuser', async () => {