mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-18 07:30:29 +00:00
feat(ui): add a Get Info dialog for artists
This commit is contained in:
parent
687646809f
commit
fd46e594e1
82
ui/src/artist/ArtistInfo.jsx
Normal file
82
ui/src/artist/ArtistInfo.jsx
Normal file
@ -0,0 +1,82 @@
|
||||
import Table from '@material-ui/core/Table'
|
||||
import TableBody from '@material-ui/core/TableBody'
|
||||
import { humanize, underscore } from 'inflection'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import {
|
||||
DateField,
|
||||
TextField,
|
||||
useRecordContext,
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { ArtworkInfo, SizeField } from '../common'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
tableCell: {
|
||||
width: '17.5%',
|
||||
},
|
||||
value: {
|
||||
whiteSpace: 'pre-line',
|
||||
},
|
||||
})
|
||||
|
||||
const ArtistInfo = (props) => {
|
||||
const classes = useStyles()
|
||||
const translate = useTranslate()
|
||||
const record = useRecordContext(props)
|
||||
const data = {
|
||||
name: <TextField source={'name'} />,
|
||||
sortArtistName: <TextField source={'sortArtistName'} />,
|
||||
mbzArtistId: <TextField source={'mbzArtistId'} />,
|
||||
albumCount: <TextField source={'albumCount'} />,
|
||||
songCount: <TextField source={'songCount'} />,
|
||||
size: <SizeField source={'size'} />,
|
||||
playCount: <TextField source={'playCount'} />,
|
||||
externalInfoUpdatedAt: (
|
||||
<DateField source={'externalInfoUpdatedAt'} showTime />
|
||||
),
|
||||
updatedAt: <DateField source={'updatedAt'} showTime />,
|
||||
}
|
||||
|
||||
const optionalFields = [
|
||||
'sortArtistName',
|
||||
'mbzArtistId',
|
||||
'externalInfoUpdatedAt',
|
||||
]
|
||||
optionalFields.forEach((field) => {
|
||||
!record[field] && delete data[field]
|
||||
})
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table aria-label="artist details" size="small">
|
||||
<TableBody>
|
||||
{Object.keys(data).map((key) => {
|
||||
return (
|
||||
<TableRow key={`${record.id}-${key}`}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
className={classes.tableCell}
|
||||
>
|
||||
{translate(`resources.artist.fields.${key}`, {
|
||||
_: humanize(underscore(key)),
|
||||
})}
|
||||
:
|
||||
</TableCell>
|
||||
<TableCell align="left" className={classes.value}>
|
||||
{data[key]}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
<ArtworkInfo resource="artist" id={record.id} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArtistInfo
|
||||
39
ui/src/artist/ArtistInfo.test.jsx
Normal file
39
ui/src/artist/ArtistInfo.test.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { RecordContextProvider } from 'react-admin'
|
||||
import ArtistInfo from './ArtistInfo'
|
||||
|
||||
vi.mock('../common', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
ArtworkInfo: () => (
|
||||
<tr>
|
||||
<td>artwork-section</td>
|
||||
</tr>
|
||||
),
|
||||
}))
|
||||
|
||||
const renderWith = (record) =>
|
||||
render(
|
||||
<RecordContextProvider value={record}>
|
||||
<ArtistInfo />
|
||||
</RecordContextProvider>,
|
||||
)
|
||||
|
||||
describe('<ArtistInfo />', () => {
|
||||
it('renders the artist fields', () => {
|
||||
renderWith({ id: 'ar-1', name: 'Radiohead', albumCount: 9, songCount: 120 })
|
||||
expect(screen.getByText('Radiohead')).toBeInTheDocument()
|
||||
expect(screen.getByText('9')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('drops optional fields that are empty', () => {
|
||||
renderWith({ id: 'ar-1', name: 'Radiohead' })
|
||||
expect(screen.queryByText(/MusicBrainz/i)).toBeNull()
|
||||
})
|
||||
|
||||
it('renders the artwork section', () => {
|
||||
renderWith({ id: 'ar-1', name: 'Radiohead' })
|
||||
expect(screen.getByText('artwork-section')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -35,6 +35,8 @@ import ArtistSimpleList from './ArtistSimpleList'
|
||||
import { DraggableTypes } from '../consts'
|
||||
import en from '../i18n/en.json'
|
||||
import { formatBytes } from '../utils/index.js'
|
||||
import ArtistInfo from './ArtistInfo'
|
||||
import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
contextHeader: {
|
||||
@ -219,6 +221,7 @@ const ArtistList = (props) => {
|
||||
>
|
||||
<ArtistListView {...props} />
|
||||
</List>
|
||||
<ExpandInfoDialog content={<ArtistInfo />} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import { LoveButton, RatingField, ImageUploadOverlay } from '../common'
|
||||
import Lightbox from 'react-image-lightbox'
|
||||
import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
|
||||
import AlbumInfo from '../album/AlbumInfo'
|
||||
import ArtistInfo from './ArtistInfo'
|
||||
import subsonic from '../subsonic'
|
||||
import { SafeHTML } from '../common/SafeHTML'
|
||||
import { Artwork } from '../common/Artwork'
|
||||
@ -160,7 +161,9 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => {
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
<ExpandInfoDialog content={<AlbumInfo />} />
|
||||
<ExpandInfoDialog
|
||||
content={{ album: <AlbumInfo />, artist: <ArtistInfo /> }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -272,7 +272,6 @@ export const ArtistContextMenu = (props) =>
|
||||
props.record ? (
|
||||
<ContextMenu
|
||||
{...props}
|
||||
hideInfo={true}
|
||||
resource={'artist'}
|
||||
songQueryParams={{
|
||||
pagination: { page: 1, perPage: 200 },
|
||||
|
||||
@ -133,8 +133,6 @@ describe('ContextMenus', () => {
|
||||
})
|
||||
|
||||
describe('info dialog', () => {
|
||||
// ArtistContextMenu hardcodes hideInfo, so it never renders a Get Info item; Album
|
||||
// uses the same action and proves resource is threaded through the dispatch.
|
||||
it('dispatches the record and resource when opening the info dialog', () => {
|
||||
const record = { id: 'al1', name: 'Album', songCount: 1 }
|
||||
renderMenu(AlbumContextMenu, record)
|
||||
@ -145,5 +143,16 @@ describe('ContextMenus', () => {
|
||||
resource: 'album',
|
||||
})
|
||||
})
|
||||
|
||||
it('dispatches the record and resource when opening the artist info dialog', () => {
|
||||
const record = { id: 'ar1', name: 'Artist', stats: {} }
|
||||
renderMenu(ArtistContextMenu, record)
|
||||
fireEvent.click(screen.getByText('resources.album.actions.info'))
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
type: EXTENDED_INFO_OPEN,
|
||||
record,
|
||||
resource: 'artist',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -36,6 +36,17 @@ describe('ExpandInfoDialog', () => {
|
||||
expect(screen.queryByText('Album Info')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Guards the artist detail page, which maps both resources to one dialog: an album
|
||||
// card's Get Info must resolve to AlbumInfo, not the page's own ArtistInfo.
|
||||
it('resolves the album entry, not artist, when the resource is album', () => {
|
||||
renderDialog(
|
||||
{ album: <div>Album Info</div>, artist: <div>Artist Info</div> },
|
||||
'album',
|
||||
)
|
||||
expect(screen.getByText('Album Info')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Artist Info')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders nothing when the map has no entry for the resource', () => {
|
||||
renderDialog({ album: <div>Album Info</div> }, 'artist')
|
||||
expect(screen.queryByText('Album Info')).not.toBeInTheDocument()
|
||||
|
||||
@ -110,10 +110,14 @@
|
||||
"name": "Artist |||| Artists",
|
||||
"fields": {
|
||||
"name": "Name",
|
||||
"sortArtistName": "Sort Name",
|
||||
"mbzArtistId": "MusicBrainz Artist Id",
|
||||
"albumCount": "Album Count",
|
||||
"songCount": "Song Count",
|
||||
"size": "Size",
|
||||
"playCount": "Plays",
|
||||
"externalInfoUpdatedAt": "External Info Updated At",
|
||||
"updatedAt": "Updated At",
|
||||
"rating": "Rating",
|
||||
"genre": "Genre",
|
||||
"role": "Role",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user