diff --git a/ui/src/dataProvider/wrapperDataProvider.js b/ui/src/dataProvider/wrapperDataProvider.js index 8b4a0cb6..268d3668 100644 --- a/ui/src/dataProvider/wrapperDataProvider.js +++ b/ui/src/dataProvider/wrapperDataProvider.js @@ -12,7 +12,21 @@ const isAdmin = () => { const getSelectedLibraries = () => { try { const state = JSON.parse(localStorage.getItem('state')) - return state?.library?.selectedLibraries || [] + const selectedLibraries = state?.library?.selectedLibraries || [] + const userLibraries = state?.library?.userLibraries || [] + + // Validate selected libraries against current user libraries + const userLibraryIds = userLibraries.map((lib) => lib.id) + const validatedSelection = selectedLibraries.filter((id) => + userLibraryIds.includes(id), + ) + + // If user has only one library, return empty array (no filter needed) + if (userLibraryIds.length === 1) { + return [] + } + + return validatedSelection } catch (err) { return [] } diff --git a/ui/src/reducers/libraryReducer.js b/ui/src/reducers/libraryReducer.js index 7cda10bc..ef613260 100644 --- a/ui/src/reducers/libraryReducer.js +++ b/ui/src/reducers/libraryReducer.js @@ -8,18 +8,39 @@ const initialState = { export const libraryReducer = (previousState = initialState, payload) => { const { type, data } = payload switch (type) { - case SET_USER_LIBRARIES: + case SET_USER_LIBRARIES: { + const newUserLibraryIds = data.map((lib) => lib.id) + + // Validate and filter selected libraries to only include IDs that exist in new user libraries + const validatedSelection = previousState.selectedLibraries.filter((id) => + newUserLibraryIds.includes(id), + ) + + // Determine the final selection: + // 1. If first time setting libraries (no previous user libraries), select all + // 2. If user now has only one library, reset to empty (no filter needed) + // 3. Otherwise, use validated selection (may be empty if all previous selections were invalid) + let finalSelection + if ( + previousState.selectedLibraries.length === 0 && + previousState.userLibraries.length === 0 + ) { + // First time: select all libraries + finalSelection = newUserLibraryIds + } else if (newUserLibraryIds.length === 1) { + // Single library: reset selection (empty means "all accessible") + finalSelection = [] + } else { + // Multiple libraries: use validated selection + finalSelection = validatedSelection + } + return { ...previousState, userLibraries: data, - // If this is the first time setting user libraries and no selection exists, - // default to all libraries - selectedLibraries: - previousState.selectedLibraries.length === 0 && - previousState.userLibraries.length === 0 - ? data.map((lib) => lib.id) - : previousState.selectedLibraries, + selectedLibraries: finalSelection, } + } case SET_SELECTED_LIBRARIES: return { ...previousState, diff --git a/ui/src/reducers/libraryReducer.test.js b/ui/src/reducers/libraryReducer.test.js new file mode 100644 index 00000000..b962c103 --- /dev/null +++ b/ui/src/reducers/libraryReducer.test.js @@ -0,0 +1,186 @@ +import { describe, it, expect } from 'vitest' +import { libraryReducer } from './libraryReducer' +import { SET_SELECTED_LIBRARIES, SET_USER_LIBRARIES } from '../actions' + +describe('libraryReducer', () => { + const mockLibraries = [ + { id: '1', name: 'Music Library' }, + { id: '2', name: 'Podcasts' }, + { id: '3', name: 'Audiobooks' }, + ] + + const initialState = { + userLibraries: [], + selectedLibraries: [], + } + + describe('SET_USER_LIBRARIES', () => { + it('should set user libraries and select all on first load', () => { + const action = { + type: SET_USER_LIBRARIES, + data: mockLibraries, + } + + const result = libraryReducer(initialState, action) + + expect(result.userLibraries).toEqual(mockLibraries) + expect(result.selectedLibraries).toEqual(['1', '2', '3']) + }) + + it('should reset selection to empty when user has only one library', () => { + const previousState = { + userLibraries: mockLibraries, + selectedLibraries: ['1', '2'], + } + + const action = { + type: SET_USER_LIBRARIES, + data: [mockLibraries[0]], // Only one library now + } + + const result = libraryReducer(previousState, action) + + expect(result.userLibraries).toEqual([mockLibraries[0]]) + expect(result.selectedLibraries).toEqual([]) // Reset for single library + }) + + it('should filter out invalid library IDs from selection', () => { + const previousState = { + userLibraries: mockLibraries, + selectedLibraries: ['1', '2', '3'], + } + + const action = { + type: SET_USER_LIBRARIES, + data: [mockLibraries[0], mockLibraries[1]], // Only libraries 1 and 2 remain + } + + const result = libraryReducer(previousState, action) + + expect(result.userLibraries).toEqual([mockLibraries[0], mockLibraries[1]]) + expect(result.selectedLibraries).toEqual(['1', '2']) // Library 3 removed + }) + + it('should keep valid selection when libraries change', () => { + const previousState = { + userLibraries: mockLibraries, + selectedLibraries: ['1'], + } + + const action = { + type: SET_USER_LIBRARIES, + data: mockLibraries, // Same libraries + } + + const result = libraryReducer(previousState, action) + + expect(result.userLibraries).toEqual(mockLibraries) + expect(result.selectedLibraries).toEqual(['1']) // Selection preserved + }) + + it('should handle selection becoming empty after filtering invalid IDs', () => { + const previousState = { + userLibraries: mockLibraries, + selectedLibraries: ['1', '2'], + } + + const newLibraries = [{ id: '4', name: 'New Library' }] + const action = { + type: SET_USER_LIBRARIES, + data: newLibraries, + } + + const result = libraryReducer(previousState, action) + + expect(result.userLibraries).toEqual(newLibraries) + expect(result.selectedLibraries).toEqual([]) // All selected IDs were invalid + }) + + it('should handle transition from multiple to single library with invalid selection', () => { + const previousState = { + userLibraries: mockLibraries, + selectedLibraries: ['2', '3'], // User had libraries 2 and 3 selected + } + + const action = { + type: SET_USER_LIBRARIES, + data: [mockLibraries[0]], // Now only has access to library 1 + } + + const result = libraryReducer(previousState, action) + + expect(result.userLibraries).toEqual([mockLibraries[0]]) + expect(result.selectedLibraries).toEqual([]) // Reset for single library + }) + + it('should handle empty library list', () => { + const previousState = { + userLibraries: mockLibraries, + selectedLibraries: ['1', '2'], + } + + const action = { + type: SET_USER_LIBRARIES, + data: [], + } + + const result = libraryReducer(previousState, action) + + expect(result.userLibraries).toEqual([]) + expect(result.selectedLibraries).toEqual([]) // All selections filtered out + }) + }) + + describe('SET_SELECTED_LIBRARIES', () => { + it('should update selected libraries', () => { + const previousState = { + userLibraries: mockLibraries, + selectedLibraries: ['1'], + } + + const action = { + type: SET_SELECTED_LIBRARIES, + data: ['2', '3'], + } + + const result = libraryReducer(previousState, action) + + expect(result.selectedLibraries).toEqual(['2', '3']) + expect(result.userLibraries).toEqual(mockLibraries) // Unchanged + }) + + it('should allow setting empty selection', () => { + const previousState = { + userLibraries: mockLibraries, + selectedLibraries: ['1', '2'], + } + + const action = { + type: SET_SELECTED_LIBRARIES, + data: [], + } + + const result = libraryReducer(previousState, action) + + expect(result.selectedLibraries).toEqual([]) + }) + }) + + describe('unknown action', () => { + it('should return previous state for unknown action', () => { + const previousState = { + userLibraries: mockLibraries, + selectedLibraries: ['1'], + } + + const action = { + type: 'UNKNOWN_ACTION', + data: null, + } + + const result = libraryReducer(previousState, action) + + expect(result).toBe(previousState) // Same reference + }) + }) +})