feat(library): enhance LibraryCreate and LibraryEdit components with additional props and styling

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-06-24 20:31:14 -04:00
commit 20968ef0b9
6 changed files with 86 additions and 27 deletions

View file

@ -1,4 +1,7 @@
import React from 'react'
import DeleteIcon from '@material-ui/icons/Delete'
import { makeStyles, alpha } from '@material-ui/core/styles'
import clsx from 'clsx'
import {
useNotify,
useDeleteWithConfirmController,
@ -7,9 +10,30 @@ import {
useTranslate,
useRedirect,
} from 'react-admin'
import DeleteIcon from '@material-ui/icons/Delete'
const DeleteLibraryButton = ({ record, resource, basePath, ...props }) => {
const useStyles = makeStyles(
(theme) => ({
deleteButton: {
color: theme.palette.error.main,
'&:hover': {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
'@media (hover: none)': {
backgroundColor: 'transparent',
},
},
},
}),
{ name: 'RaDeleteWithConfirmButton' },
)
const DeleteLibraryButton = ({
record,
resource,
basePath,
className,
...props
}) => {
const translate = useTranslate()
const notify = useNotify()
const redirect = useRedirect()
@ -29,12 +53,14 @@ const DeleteLibraryButton = ({ record, resource, basePath, ...props }) => {
onSuccess,
})
const classes = useStyles(props)
return (
<>
<Button
label="ra.action.delete"
onClick={handleDialogOpen}
disabled={loading}
className={clsx('ra-delete-button', classes.deleteButton, className)}
{...props}
>
<DeleteIcon />

View file

@ -73,7 +73,7 @@ const LibraryCreate = (props) => {
<Create title={<Title subTitle={title} />} {...props}>
<SimpleForm save={save} variant={'outlined'}>
<TextInput source="name" validate={[required()]} />
<TextInput source="path" validate={[required()]} />
<TextInput source="path" validate={[required()]} fullWidth />
</SimpleForm>
</Create>
)

View file

@ -11,30 +11,39 @@ import {
useNotify,
useRedirect,
NumberInput,
Toolbar,
} from 'react-admin'
import { Typography, Box, Toolbar } from '@material-ui/core'
import { Typography, Box } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import DeleteLibraryButton from './DeleteLibraryButton'
import { Title } from '../common'
import { formatBytes, formatDuration2, formatNumber } from '../utils/index.js'
const useStyles = makeStyles({
toolbar: {
display: 'flex',
justifyContent: 'space-between',
},
})
const LibraryTitle = ({ record }) => {
const translate = useTranslate()
const resourceName = translate('resources.library.name', { smart_count: 1 })
return (
<Title subTitle={`${resourceName} ${record ? `\"${record.name}\"` : ''}`} />
<Title subTitle={`${resourceName} ${record ? `"${record.name}"` : ''}`} />
)
}
const CustomToolbar = (props) => (
<Toolbar {...props}>
<Box display="flex" justifyContent="space-between" width="100%">
<SaveButton
handleSubmitWithRedirect={props.handleSubmitWithRedirect}
saving={props.saving}
pristine={props.pristine}
const CustomToolbar = ({ showDelete, ...props }) => (
<Toolbar {...props} classes={useStyles()}>
<SaveButton disabled={props.pristine} />
{showDelete && (
<DeleteLibraryButton
record={props.record}
resource="library"
basePath="/library"
/>
<DeleteLibraryButton />
</Box>
)}
</Toolbar>
)
@ -44,6 +53,9 @@ const LibraryEdit = (props) => {
const notify = useNotify()
const redirect = useRedirect()
// Library ID 1 is protected (main library)
const canDelete = props.id !== '1'
const save = useCallback(
async (values) => {
try {
@ -83,8 +95,19 @@ const LibraryEdit = (props) => {
{translate('resources.library.sections.basic')}
</Typography>
<TextInput source="name" validate={[required()]} />
<TextInput source="path" validate={[required()]} fullWidth />
<TextInput
source="name"
validate={[required()]}
variant="outlined"
InputProps={{ readOnly: !canDelete }} // Disable editing path for library 1
/>
<TextInput
source="path"
validate={[required()]}
fullWidth
variant="outlined"
InputProps={{ readOnly: !canDelete }} // Disable editing path for library 1
/>
<Box mt="2em" />
@ -96,18 +119,20 @@ const LibraryEdit = (props) => {
<Box display="flex">
<Box flex={1} mr="0.5em">
<NumberInput
disabled
InputProps={{ readOnly: true }}
resource={'library'}
source={'totalSongs'}
fullWidth
variant="outlined"
/>
</Box>
<Box flex={1} ml="0.5em">
<NumberInput
disabled
InputProps={{ readOnly: true }}
resource={'library'}
source={'totalAlbums'}
fullWidth
variant="outlined"
/>
</Box>
</Box>
@ -115,19 +140,21 @@ const LibraryEdit = (props) => {
<Box display="flex">
<Box flex={1} mr="0.5em">
<NumberInput
disabled
InputProps={{ readOnly: true }}
resource={'library'}
source={'totalArtists'}
fullWidth
variant="outlined"
/>
</Box>
<Box flex={1} ml="0.5em">
<TextInput
disabled
InputProps={{ readOnly: true }}
resource={'library'}
source={'totalSize'}
format={formatBytes}
fullWidth
variant="outlined"
/>
</Box>
</Box>
@ -135,19 +162,21 @@ const LibraryEdit = (props) => {
<Box display="flex">
<Box flex={1} mr="0.5em">
<TextInput
disabled
InputProps={{ readOnly: true }}
resource={'library'}
source={'totalDuration'}
format={formatDuration2}
fullWidth
variant="outlined"
/>
</Box>
<Box flex={1} ml="0.5em">
<TextInput
disabled
InputProps={{ readOnly: true }}
resource={'library'}
source={'totalMissingFiles'}
fullWidth
variant="outlined"
/>
</Box>
</Box>
@ -208,6 +237,8 @@ const LibraryEdit = (props) => {
handleSubmitWithRedirect={formProps.handleSubmitWithRedirect}
pristine={formProps.pristine}
saving={formProps.saving}
record={formProps.record}
showDelete={canDelete}
/>
</form>
)}

View file

@ -1,8 +1,10 @@
import { MdLibraryMusic } from 'react-icons/md'
import LibraryList from './LibraryList'
import LibraryEdit from './LibraryEdit'
import LibraryCreate from './LibraryCreate'
export default {
icon: MdLibraryMusic,
list: LibraryList,
edit: LibraryEdit,
create: LibraryCreate,

View file

@ -35,7 +35,7 @@ export const formatDuration2 = (totalSeconds) => {
const seconds = Math.floor(totalSeconds % 60)
const parts = []
if (days > 0) {
// When days are present, show only d h m (3 levels max)
parts.push(`${days}d`)
@ -57,7 +57,7 @@ export const formatDuration2 = (totalSeconds) => {
parts.push(`${seconds}s`)
}
}
return parts.join(' ')
}

View file

@ -110,12 +110,12 @@ describe('formatDuration2', () => {
it('formats days with maximum 3 levels (d h m)', () => {
expect(formatDuration2(86400)).toEqual('1d')
expect(formatDuration2(86461)).toEqual('1d 1m') // seconds dropped when days present
expect(formatDuration2(90061)).toEqual('1d 1h 1m') // seconds dropped when days present
expect(formatDuration2(86461)).toEqual('1d 1m') // seconds dropped when days present
expect(formatDuration2(90061)).toEqual('1d 1h 1m') // seconds dropped when days present
expect(formatDuration2(172800)).toEqual('2d')
expect(formatDuration2(176400)).toEqual('2d 1h')
expect(formatDuration2(176460)).toEqual('2d 1h 1m')
expect(formatDuration2(176461)).toEqual('2d 1h 1m') // seconds dropped when days present
expect(formatDuration2(176461)).toEqual('2d 1h 1m') // seconds dropped when days present
})
})