mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
fix(ui): add Rescan button to plugin list empty state (#5471)
* feat(ui): add Rescan button to plugin list empty state When no plugins are installed and the folder watcher fails to detect new plugins, users had no way to trigger a rescan. Extract RescanButton into a shared component and render it in a custom empty state for the plugin list. * refactor(ui): address review feedback for plugin empty state - Pass label translation key directly to RA Button (auto-translates) - Use within() from Testing Library instead of querySelector for scoped queries with better error messages
This commit is contained in:
parent
219ceebd2c
commit
6ebe742f5f
@ -2,6 +2,7 @@ import React, { useMemo, useState, useCallback } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Datagrid,
|
||||
Empty,
|
||||
TextField,
|
||||
TopToolbar,
|
||||
useNotify,
|
||||
@ -10,7 +11,13 @@ import {
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useMediaQuery, Tooltip, Chip, Typography } from '@material-ui/core'
|
||||
import {
|
||||
useMediaQuery,
|
||||
Tooltip,
|
||||
Chip,
|
||||
Typography,
|
||||
Box,
|
||||
} from '@material-ui/core'
|
||||
import { MdError, MdRefresh } from 'react-icons/md'
|
||||
import { List, DateField, SimpleList, useResourceRefresh } from '../common'
|
||||
import { httpClient } from '../dataProvider'
|
||||
@ -72,8 +79,7 @@ const ManifestField = ({ source }) => {
|
||||
return <Typography variant="body2">{manifest[source] || '-'}</Typography>
|
||||
}
|
||||
|
||||
const PluginListActions = () => {
|
||||
const translate = useTranslate()
|
||||
const RescanButton = () => {
|
||||
const notify = useNotify()
|
||||
const refresh = useRefresh()
|
||||
const [loading, setLoading] = useState(false)
|
||||
@ -92,20 +98,37 @@ const PluginListActions = () => {
|
||||
})
|
||||
}, [notify, refresh])
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleRescan}
|
||||
disabled={loading}
|
||||
label="resources.plugin.actions.rescan"
|
||||
data-testid="rescan-button"
|
||||
>
|
||||
<MdRefresh />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const PluginListActions = () => {
|
||||
return (
|
||||
<TopToolbar>
|
||||
<Button
|
||||
onClick={handleRescan}
|
||||
disabled={loading}
|
||||
label={translate('resources.plugin.actions.rescan')}
|
||||
data-testid="rescan-button"
|
||||
>
|
||||
<MdRefresh />
|
||||
</Button>
|
||||
<RescanButton />
|
||||
</TopToolbar>
|
||||
)
|
||||
}
|
||||
|
||||
const PluginEmpty = () => {
|
||||
return (
|
||||
<>
|
||||
<Empty />
|
||||
<Box textAlign="center" mt={2}>
|
||||
<RescanButton />
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PluginList = (props) => {
|
||||
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
||||
const translate = useTranslate()
|
||||
@ -118,6 +141,7 @@ const PluginList = (props) => {
|
||||
exporter={false}
|
||||
bulkActionButtons={false}
|
||||
actions={<PluginListActions />}
|
||||
empty={<PluginEmpty />}
|
||||
>
|
||||
{isXsmall ? (
|
||||
<SimpleList
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import React from 'react'
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
@ -34,6 +40,7 @@ vi.mock('react-admin', async () => {
|
||||
TopToolbar: ({ children }) => (
|
||||
<div data-testid="top-toolbar">{children}</div>
|
||||
),
|
||||
Empty: () => <div data-testid="ra-empty">No resources</div>,
|
||||
Datagrid: ({ children }) => <div data-testid="datagrid">{children}</div>,
|
||||
TextField: ({ source }) => <span data-testid={`text-${source}`} />,
|
||||
}
|
||||
@ -42,9 +49,10 @@ vi.mock('react-admin', async () => {
|
||||
// Mock common components
|
||||
vi.mock('../common', async () => {
|
||||
return {
|
||||
List: ({ children, actions, ...props }) => (
|
||||
List: ({ children, actions, empty, ...props }) => (
|
||||
<div data-testid="list">
|
||||
{actions}
|
||||
{empty && <div data-testid="empty-state">{empty}</div>}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
@ -94,14 +102,16 @@ describe('PluginList', () => {
|
||||
expect(screen.getByTestId('datagrid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the rescan button', () => {
|
||||
it('renders the rescan button in the toolbar', () => {
|
||||
render(<PluginList />)
|
||||
expect(screen.getByTestId('rescan-button')).toBeInTheDocument()
|
||||
const toolbar = screen.getByTestId('top-toolbar')
|
||||
expect(within(toolbar).getByTestId('rescan-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls rescan endpoint when rescan button is clicked', async () => {
|
||||
render(<PluginList />)
|
||||
const rescanButton = screen.getByTestId('rescan-button')
|
||||
const toolbar = screen.getByTestId('top-toolbar')
|
||||
const rescanButton = within(toolbar).getByTestId('rescan-button')
|
||||
|
||||
fireEvent.click(rescanButton)
|
||||
|
||||
@ -114,7 +124,8 @@ describe('PluginList', () => {
|
||||
|
||||
it('calls refresh after successful rescan', async () => {
|
||||
render(<PluginList />)
|
||||
const rescanButton = screen.getByTestId('rescan-button')
|
||||
const toolbar = screen.getByTestId('top-toolbar')
|
||||
const rescanButton = within(toolbar).getByTestId('rescan-button')
|
||||
|
||||
fireEvent.click(rescanButton)
|
||||
|
||||
@ -127,7 +138,8 @@ describe('PluginList', () => {
|
||||
mockHttpClient.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
render(<PluginList />)
|
||||
const rescanButton = screen.getByTestId('rescan-button')
|
||||
const toolbar = screen.getByTestId('top-toolbar')
|
||||
const rescanButton = within(toolbar).getByTestId('rescan-button')
|
||||
|
||||
fireEvent.click(rescanButton)
|
||||
|
||||
@ -137,4 +149,25 @@ describe('PluginList', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('renders a rescan button in the empty state', () => {
|
||||
render(<PluginList />)
|
||||
const emptyState = screen.getByTestId('empty-state')
|
||||
expect(emptyState).toBeInTheDocument()
|
||||
expect(within(emptyState).getByTestId('rescan-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('empty state rescan button triggers rescan', async () => {
|
||||
render(<PluginList />)
|
||||
const emptyState = screen.getByTestId('empty-state')
|
||||
const rescanButton = within(emptyState).getByTestId('rescan-button')
|
||||
|
||||
fireEvent.click(rescanButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHttpClient).toHaveBeenCalledWith('/api/plugin/rescan', {
|
||||
method: 'POST',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user