From 6e8c024d638707a27849d6e9816ed3e1b70a7e21 Mon Sep 17 00:00:00 2001 From: Tim Kaiser Date: Mon, 8 Jun 2026 19:04:59 +0200 Subject: [PATCH] fix(frontend): load root folders on mount so cold first add isn't falsely rejected The root folders store is only ever populated by an explicit load() call. Several views read rootFoldersStore.folders synchronously to gate UI, but never load it on a cold SPA start: - AddNewView's addToLibrary() guard reported "Root folder not configured" and redirected to /settings on the first add after a fresh page load; the redirect populated the store, so a back+retry then succeeded. - AudiobooksView's hasRootFolderConfigured drove a false "Root Folder Not Configured" empty-state for users whose library is empty. - AudiobookDetailView's path preview fell back to the legacy outputPath. Load the store on mount in each of these views (mirroring the existing LibraryImportView / UnmatchedFilesModal pattern). Also stop wiping already-loaded folders when a reload throws, so a transient API failure no longer masquerades as "no root folders configured". Adds a regression test asserting AddNewView fetches root folders on mount. --- fe/src/__tests__/AddNewView.spec.ts | 17 +++++++++++++++++ fe/src/stores/rootFolders.ts | 4 +++- fe/src/views/content/AddNewView.vue | 5 +++++ fe/src/views/library/AudiobookDetailView.vue | 4 ++++ fe/src/views/library/AudiobooksView.vue | 3 +++ 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/fe/src/__tests__/AddNewView.spec.ts b/fe/src/__tests__/AddNewView.spec.ts index d0a82fd3b..551141435 100644 --- a/fe/src/__tests__/AddNewView.spec.ts +++ b/fe/src/__tests__/AddNewView.spec.ts @@ -768,4 +768,21 @@ describe('AddNewView pagination', () => { expect(addBtn.text()).toContain('Added') expect(addBtn.attributes('disabled')).toBeDefined() }) + + // Regression: cold-add "root folder not configured" false negative. + // The addToLibrary() guard reads rootFoldersStore.folders synchronously, so + // the store must be populated on mount; otherwise the first add after a fresh + // page load wrongly redirects to /settings. + it('loads root folders on mount so the cold first add is not falsely rejected', async () => { + const apiModule = await import('@/services/api') + const getRootFolders = apiModule.apiService.getRootFolders as unknown as Mock + getRootFolders.mockClear() + getRootFolders.mockResolvedValue([{ id: 1, name: 'Books', path: '/books', isDefault: true }]) + + const router = createTestRouter() + mount(AddNewView, { global: { plugins: [createPinia(), router] } }) + await flushPromises() + + expect(getRootFolders).toHaveBeenCalled() + }) }) diff --git a/fe/src/stores/rootFolders.ts b/fe/src/stores/rootFolders.ts index ecab77a49..4f2f69069 100644 --- a/fe/src/stores/rootFolders.ts +++ b/fe/src/stores/rootFolders.ts @@ -38,7 +38,9 @@ export const useRootFoldersStore = defineStore('rootFolders', () => { } } catch (err) { logger.debug('Failed to load root folders:', err) - folders.value = [] + // Preserve any previously-loaded folders on transient failure: a failed + // reload must not masquerade as "no root folders configured" (folders + // starts as [], so a first-ever failure still yields an empty list). } finally { loading.value = false } diff --git a/fe/src/views/content/AddNewView.vue b/fe/src/views/content/AddNewView.vue index 40f6c77ce..c0fbd7e6f 100644 --- a/fe/src/views/content/AddNewView.vue +++ b/fe/src/views/content/AddNewView.vue @@ -2998,6 +2998,11 @@ onMounted(async () => { await configStore.loadApplicationSettings() await configStore.loadApiConfigurations() + // Ensure root folders are loaded before addToLibrary()'s guard reads them. + // Without this, the first add after a cold page load sees an empty store and + // falsely reports "Root folder not configured" until a redirect populates it. + if (rootFoldersStore.folders.length === 0) await rootFoldersStore.load() + const defaultRegion = normalizeSearchRegion(configStore.applicationSettings?.defaultSearchRegion) const defaultLanguage = normalizePreferredSearchLanguage( configStore.applicationSettings?.defaultSearchLanguage, diff --git a/fe/src/views/library/AudiobookDetailView.vue b/fe/src/views/library/AudiobookDetailView.vue index 14dbfc297..c00deac61 100644 --- a/fe/src/views/library/AudiobookDetailView.vue +++ b/fe/src/views/library/AudiobookDetailView.vue @@ -1171,6 +1171,10 @@ onMounted(async () => { syncActiveTabFromRoute() document.addEventListener('click', handleClickOutside) + // The path preview falls back to rootFoldersStore.defaultFolder; load it so a + // cold page load shows the correct destination rather than the legacy outputPath. + if (rootFoldersStore.folders.length === 0) await rootFoldersStore.load() + await loadAudiobook() // subscribe to scan job updates diff --git a/fe/src/views/library/AudiobooksView.vue b/fe/src/views/library/AudiobooksView.vue index f7b7fa4ec..ddbf663d6 100644 --- a/fe/src/views/library/AudiobooksView.vue +++ b/fe/src/views/library/AudiobooksView.vue @@ -1911,6 +1911,9 @@ onMounted(async () => { libraryStore.fetchLibrary(), configStore.loadApplicationSettings(), loadQualityProfiles(), + // hasRootFolderConfigured gates the empty-state CTA on this store; load it + // so an empty library doesn't falsely show "Root Folder Not Configured". + rootFoldersStore.folders.length === 0 ? rootFoldersStore.load() : Promise.resolve(), ]) // Load persisted view mode (if available) before layout calc