feat(ui): add a Get Info dialog for artists

This commit is contained in:
Deluan 2026-09-03 23:55:40 -04:00
parent 687646809f
commit fd46e594e1
8 changed files with 154 additions and 4 deletions

View 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

View 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()
})
})

View File

@ -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 />} />
</>
)
}

View File

@ -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>
)
}

View File

@ -272,7 +272,6 @@ export const ArtistContextMenu = (props) =>
props.record ? (
<ContextMenu
{...props}
hideInfo={true}
resource={'artist'}
songQueryParams={{
pagination: { page: 1, perPage: 200 },

View File

@ -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',
})
})
})
})

View File

@ -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()

View File

@ -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",