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