mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
fix(ui): load user libraries on app init so NowPlaying filters correctly
The NowPlaying panel filters entries by the user's accessible libraries via useSelectedLibraries(). That selector reads userLibraries from the store, but the data was only fetched by LibrarySelector, which is rendered solely when the sidebar is open. On first load with the sidebar closed, userLibraries was empty, so getNowPlaying was called without a musicFolderId and the server returned entries from all libraries until a later refresh. Extract the loading logic into a useUserLibraries hook and call it from Layout, so the libraries are always loaded regardless of sidebar state. LibrarySelector now reuses the same hook instead of duplicating the fetch. Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
736e339080
commit
dab37f6a89
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useDataProvider, useTranslate, useRefresh } from 'react-admin'
|
||||
import { useTranslate, useRefresh } from 'react-admin'
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
@ -15,8 +15,8 @@ import {
|
||||
makeStyles,
|
||||
} from '@material-ui/core'
|
||||
import { ExpandMore, ExpandLess, LibraryMusic } from '@material-ui/icons'
|
||||
import { setSelectedLibraries, setUserLibraries } from '../actions'
|
||||
import { useRefreshOnEvents } from './useRefreshOnEvents'
|
||||
import { setSelectedLibraries } from '../actions'
|
||||
import { useUserLibraries } from './useUserLibraries'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@ -70,7 +70,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
const LibrarySelector = () => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const dataProvider = useDataProvider()
|
||||
const translate = useTranslate()
|
||||
const refresh = useRefresh()
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
@ -80,34 +79,9 @@ const LibrarySelector = () => {
|
||||
(state) => state.library,
|
||||
)
|
||||
|
||||
// Load user's libraries when component mounts
|
||||
const loadUserLibraries = useCallback(async () => {
|
||||
const userId = localStorage.getItem('userId')
|
||||
if (userId) {
|
||||
try {
|
||||
const { data } = await dataProvider.getOne('user', { id: userId })
|
||||
const libraries = data.libraries || []
|
||||
dispatch(setUserLibraries(libraries))
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Could not load user libraries (this may be expected for non-admin users):',
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [dataProvider, dispatch])
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadUserLibraries()
|
||||
}, [loadUserLibraries])
|
||||
|
||||
// Reload user libraries when library changes occur
|
||||
useRefreshOnEvents({
|
||||
events: ['library', 'user'],
|
||||
onRefresh: loadUserLibraries,
|
||||
})
|
||||
// Keep the user's libraries loaded (also done at the Layout level so the data
|
||||
// is available even when this selector isn't rendered).
|
||||
useUserLibraries()
|
||||
|
||||
// Don't render if user has no libraries or only has one library
|
||||
if (!userLibraries.length || userLibraries.length === 1) {
|
||||
|
||||
@ -28,6 +28,7 @@ export * from './useGetHandleArtistClick'
|
||||
export * from './useInterval'
|
||||
export * from './useResourceRefresh'
|
||||
export * from './useRefreshOnEvents'
|
||||
export * from './useUserLibraries'
|
||||
export * from './useToggleLove'
|
||||
export * from './useTraceUpdate'
|
||||
export * from './Writable'
|
||||
|
||||
40
ui/src/common/useUserLibraries.js
Normal file
40
ui/src/common/useUserLibraries.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useDataProvider } from 'react-admin'
|
||||
import { setUserLibraries } from '../actions'
|
||||
import { useRefreshOnEvents } from './useRefreshOnEvents'
|
||||
|
||||
/**
|
||||
* Loads the current user's accessible libraries into the Redux store and keeps
|
||||
* them refreshed when library/user events occur. Mount this once high in the
|
||||
* tree (e.g. the Layout) so consumers like useSelectedLibraries always have the
|
||||
* data available, regardless of whether the sidebar/LibrarySelector is open.
|
||||
*/
|
||||
export const useUserLibraries = () => {
|
||||
const dispatch = useDispatch()
|
||||
const dataProvider = useDataProvider()
|
||||
|
||||
const loadUserLibraries = useCallback(async () => {
|
||||
const userId = localStorage.getItem('userId')
|
||||
if (!userId) return
|
||||
try {
|
||||
const { data } = await dataProvider.getOne('user', { id: userId })
|
||||
dispatch(setUserLibraries(data.libraries || []))
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Could not load user libraries (this may be expected for non-admin users):',
|
||||
error,
|
||||
)
|
||||
}
|
||||
}, [dataProvider, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
loadUserLibraries()
|
||||
}, [loadUserLibraries])
|
||||
|
||||
useRefreshOnEvents({
|
||||
events: ['library', 'user'],
|
||||
onRefresh: loadUserLibraries,
|
||||
})
|
||||
}
|
||||
65
ui/src/common/useUserLibraries.test.js
Normal file
65
ui/src/common/useUserLibraries.test.js
Normal file
@ -0,0 +1,65 @@
|
||||
import { renderHook } from '@testing-library/react-hooks'
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { useUserLibraries } from './useUserLibraries'
|
||||
|
||||
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
const mockDispatch = vi.fn()
|
||||
const mockGetOne = vi.fn()
|
||||
|
||||
vi.mock('react-redux', () => ({
|
||||
useDispatch: () => mockDispatch,
|
||||
}))
|
||||
|
||||
vi.mock('react-admin', () => ({
|
||||
useDataProvider: () => ({ getOne: mockGetOne }),
|
||||
}))
|
||||
|
||||
vi.mock('./useRefreshOnEvents', () => ({
|
||||
useRefreshOnEvents: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('useUserLibraries', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('loads the user libraries and dispatches them on mount', async () => {
|
||||
localStorage.setItem('userId', 'u-1')
|
||||
const libraries = [{ id: 1 }, { id: 2 }]
|
||||
mockGetOne.mockResolvedValue({ data: { libraries } })
|
||||
|
||||
renderHook(() => useUserLibraries())
|
||||
await flushPromises()
|
||||
|
||||
expect(mockGetOne).toHaveBeenCalledWith('user', { id: 'u-1' })
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ data: libraries }),
|
||||
)
|
||||
})
|
||||
|
||||
it('does not fetch when there is no userId', () => {
|
||||
renderHook(() => useUserLibraries())
|
||||
|
||||
expect(mockGetOne).not.toHaveBeenCalled()
|
||||
expect(mockDispatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles a failed fetch without dispatching', async () => {
|
||||
localStorage.setItem('userId', 'u-1')
|
||||
mockGetOne.mockRejectedValue(new Error('forbidden'))
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
renderHook(() => useUserLibraries())
|
||||
await flushPromises()
|
||||
|
||||
expect(mockGetOne).toHaveBeenCalled()
|
||||
expect(mockDispatch).not.toHaveBeenCalled()
|
||||
warn.mockRestore()
|
||||
})
|
||||
})
|
||||
@ -7,7 +7,7 @@ import Menu from './Menu'
|
||||
import AppBar from './AppBar'
|
||||
import Notification from './Notification'
|
||||
import useCurrentTheme from '../themes/useCurrentTheme'
|
||||
import { useSearchRefocus } from '../common'
|
||||
import { useSearchRefocus, useUserLibraries } from '../common'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: { paddingBottom: (props) => (props.addPadding ? '80px' : 0) },
|
||||
@ -19,6 +19,7 @@ const Layout = (props) => {
|
||||
const classes = useStyles({ addPadding: queue.length > 0 })
|
||||
const dispatch = useDispatch()
|
||||
useSearchRefocus()
|
||||
useUserLibraries()
|
||||
|
||||
const keyHandlers = {
|
||||
TOGGLE_MENU: useCallback(() => dispatch(toggleSidebar()), [dispatch]),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user