diff --git a/ui/src/artist/ArtistInfo.jsx b/ui/src/artist/ArtistInfo.jsx
new file mode 100644
index 000000000..ad3f483d1
--- /dev/null
+++ b/ui/src/artist/ArtistInfo.jsx
@@ -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: ,
+ sortArtistName: ,
+ mbzArtistId: ,
+ albumCount: ,
+ songCount: ,
+ size: ,
+ playCount: ,
+ externalInfoUpdatedAt: (
+
+ ),
+ updatedAt: ,
+ }
+
+ const optionalFields = [
+ 'sortArtistName',
+ 'mbzArtistId',
+ 'externalInfoUpdatedAt',
+ ]
+ optionalFields.forEach((field) => {
+ !record[field] && delete data[field]
+ })
+
+ return (
+
+
+
+ {Object.keys(data).map((key) => {
+ return (
+
+
+ {translate(`resources.artist.fields.${key}`, {
+ _: humanize(underscore(key)),
+ })}
+ :
+
+
+ {data[key]}
+
+
+ )
+ })}
+
+
+
+
+ )
+}
+
+export default ArtistInfo
diff --git a/ui/src/artist/ArtistInfo.test.jsx b/ui/src/artist/ArtistInfo.test.jsx
new file mode 100644
index 000000000..b1b12bbba
--- /dev/null
+++ b/ui/src/artist/ArtistInfo.test.jsx
@@ -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: () => (
+
+ | artwork-section |
+
+ ),
+}))
+
+const renderWith = (record) =>
+ render(
+
+
+ ,
+ )
+
+describe('', () => {
+ 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()
+ })
+})
diff --git a/ui/src/artist/ArtistList.jsx b/ui/src/artist/ArtistList.jsx
index f4147221e..b34deaefe 100644
--- a/ui/src/artist/ArtistList.jsx
+++ b/ui/src/artist/ArtistList.jsx
@@ -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) => {
>
+ } />
>
)
}
diff --git a/ui/src/artist/DesktopArtistDetails.jsx b/ui/src/artist/DesktopArtistDetails.jsx
index 9269dc8d7..a0cddd258 100644
--- a/ui/src/artist/DesktopArtistDetails.jsx
+++ b/ui/src/artist/DesktopArtistDetails.jsx
@@ -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 }) => {
/>
)}
- } />
+ , artist: }}
+ />
)
}
diff --git a/ui/src/common/ContextMenus.jsx b/ui/src/common/ContextMenus.jsx
index 57e672b2d..0ff23955d 100644
--- a/ui/src/common/ContextMenus.jsx
+++ b/ui/src/common/ContextMenus.jsx
@@ -272,7 +272,6 @@ export const ArtistContextMenu = (props) =>
props.record ? (
{
})
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',
+ })
+ })
})
})
diff --git a/ui/src/dialogs/ExpandInfoDialog.test.jsx b/ui/src/dialogs/ExpandInfoDialog.test.jsx
index 9ce5d06e5..d450b8e0e 100644
--- a/ui/src/dialogs/ExpandInfoDialog.test.jsx
+++ b/ui/src/dialogs/ExpandInfoDialog.test.jsx
@@ -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: Album Info
, artist: Artist Info
},
+ '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: Album Info
}, 'artist')
expect(screen.queryByText('Album Info')).not.toBeInTheDocument()
diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index 9635cadd1..e141e6785 100644
--- a/ui/src/i18n/en.json
+++ b/ui/src/i18n/en.json
@@ -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",