mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
feat(ui): show read-only values in edit forms as dimmed, themable inputs (#6238)
This commit is contained in:
parent
ab5869123d
commit
612c8290f9
10 changed files with 306 additions and 160 deletions
99
ui/src/common/ReadOnlyFields.jsx
Normal file
99
ui/src/common/ReadOnlyFields.jsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import get from 'lodash/get'
|
||||
import { FieldTitle, useRecordContext } from 'react-admin'
|
||||
import { TextField } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useDateLocale } from '../i18n/useDateLocale'
|
||||
import {
|
||||
formatBytes,
|
||||
formatDateTime,
|
||||
formatDuration2,
|
||||
formatNumber,
|
||||
} from '../utils/formatters'
|
||||
import { isDateSet } from '../utils/validations'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme) => ({
|
||||
inputRoot: {
|
||||
'&:hover $notchedOutline': {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
},
|
||||
notchedOutline: {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
}),
|
||||
{ name: 'NDReadOnlyField' },
|
||||
)
|
||||
|
||||
const identity = (v) => v
|
||||
|
||||
// Renders a record value as a dimmed, non-editable input, so it lines up with the inputs in a form
|
||||
export const ReadOnlyTextField = ({
|
||||
source,
|
||||
label,
|
||||
resource,
|
||||
className,
|
||||
fullWidth,
|
||||
format = identity,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles(props)
|
||||
const record = useRecordContext(props)
|
||||
const value = get(record, source)
|
||||
|
||||
return (
|
||||
<TextField
|
||||
id={source}
|
||||
className={className}
|
||||
label={<FieldTitle label={label} source={source} resource={resource} />}
|
||||
value={value == null ? '' : format(value)}
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
fullWidth={fullWidth}
|
||||
focused={false}
|
||||
helperText=" "
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
classes: {
|
||||
root: classes.inputRoot,
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
}}
|
||||
inputProps={{ tabIndex: -1 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
ReadOnlyTextField.propTypes = {
|
||||
source: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
record: PropTypes.object,
|
||||
resource: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
classes: PropTypes.object,
|
||||
fullWidth: PropTypes.bool,
|
||||
format: PropTypes.func,
|
||||
}
|
||||
|
||||
export const ReadOnlyDateField = (props) => {
|
||||
const locale = useDateLocale()
|
||||
const format = (v) => (isDateSet(v) ? formatDateTime(v, locale) : '')
|
||||
return <ReadOnlyTextField format={format} {...props} />
|
||||
}
|
||||
|
||||
export const ReadOnlyNumberField = (props) => {
|
||||
const locale = useDateLocale()
|
||||
return (
|
||||
<ReadOnlyTextField format={(v) => formatNumber(v, locale)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export const ReadOnlySizeField = (props) => (
|
||||
<ReadOnlyTextField format={formatBytes} {...props} />
|
||||
)
|
||||
|
||||
export const ReadOnlyDurationField = (props) => (
|
||||
<ReadOnlyTextField format={formatDuration2} {...props} />
|
||||
)
|
||||
121
ui/src/common/ReadOnlyFields.test.jsx
Normal file
121
ui/src/common/ReadOnlyFields.test.jsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import {
|
||||
ReadOnlyDateField,
|
||||
ReadOnlyDurationField,
|
||||
ReadOnlyNumberField,
|
||||
ReadOnlySizeField,
|
||||
ReadOnlyTextField,
|
||||
} from './ReadOnlyFields'
|
||||
|
||||
vi.mock('react-admin', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
useLocale: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ReadOnlyFields', () => {
|
||||
const record = {
|
||||
id: '1',
|
||||
client: 'NavidromeUI',
|
||||
createdAt: '2026-09-17T14:30:00Z',
|
||||
lastVisitedAt: '0001-01-01T00:00:00Z',
|
||||
count: 1234567,
|
||||
zero: 0,
|
||||
size: 1536000,
|
||||
duration: 3725,
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(navigator, 'languages', 'get').mockReturnValue([])
|
||||
const { useLocale } = await import('react-admin')
|
||||
vi.mocked(useLocale).mockReturnValue('de')
|
||||
})
|
||||
|
||||
const renderField = (Field, props) =>
|
||||
render(<Field record={record} resource="player" {...props} />)
|
||||
|
||||
describe('<ReadOnlyTextField>', () => {
|
||||
it('shows the record value with the translated field label', () => {
|
||||
renderField(ReadOnlyTextField, { source: 'client' })
|
||||
const input = screen.getByLabelText('resources.player.fields.client')
|
||||
expect(input).toHaveValue('NavidromeUI')
|
||||
})
|
||||
|
||||
it('cannot be edited or reached with the Tab key', () => {
|
||||
renderField(ReadOnlyTextField, { source: 'client' })
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveAttribute('readonly')
|
||||
expect(input).toHaveAttribute('tabindex', '-1')
|
||||
})
|
||||
|
||||
it('shows an empty value when the record has no value', () => {
|
||||
renderField(ReadOnlyTextField, { source: 'userName' })
|
||||
expect(screen.getByRole('textbox')).toHaveValue('')
|
||||
})
|
||||
|
||||
it('uses an explicit label when given', () => {
|
||||
renderField(ReadOnlyTextField, { source: 'client', label: 'Custom' })
|
||||
expect(screen.getByLabelText('Custom')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies a custom format', () => {
|
||||
renderField(ReadOnlyTextField, {
|
||||
source: 'client',
|
||||
format: (v) => v.toUpperCase(),
|
||||
})
|
||||
expect(screen.getByRole('textbox')).toHaveValue('NAVIDROMEUI')
|
||||
})
|
||||
|
||||
it('exposes theme-overridable class names', () => {
|
||||
const { container } = renderField(ReadOnlyTextField, {
|
||||
source: 'client',
|
||||
})
|
||||
expect(
|
||||
container.querySelector('[class*="NDReadOnlyField-inputRoot"]'),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
container.querySelector('[class*="NDReadOnlyField-notchedOutline"]'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('<ReadOnlyDateField>', () => {
|
||||
it('formats the date using the selected language', () => {
|
||||
renderField(ReadOnlyDateField, { source: 'createdAt' })
|
||||
expect(screen.getByRole('textbox').value).toMatch(/^17\.9\.2026, /)
|
||||
})
|
||||
|
||||
it('shows an empty value when the date is not set', () => {
|
||||
renderField(ReadOnlyDateField, { source: 'lastVisitedAt' })
|
||||
expect(screen.getByRole('textbox')).toHaveValue('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('<ReadOnlyNumberField>', () => {
|
||||
it('formats the number using the selected language', () => {
|
||||
renderField(ReadOnlyNumberField, { source: 'count' })
|
||||
expect(screen.getByRole('textbox')).toHaveValue('1.234.567')
|
||||
})
|
||||
|
||||
it('shows zero', () => {
|
||||
renderField(ReadOnlyNumberField, { source: 'zero' })
|
||||
expect(screen.getByRole('textbox')).toHaveValue('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('<ReadOnlySizeField>', () => {
|
||||
it('formats bytes as a human-readable size', () => {
|
||||
renderField(ReadOnlySizeField, { source: 'size' })
|
||||
expect(screen.getByRole('textbox')).toHaveValue('1.46 MB')
|
||||
})
|
||||
})
|
||||
|
||||
describe('<ReadOnlyDurationField>', () => {
|
||||
it('formats seconds as a human-readable duration', () => {
|
||||
renderField(ReadOnlyDurationField, { source: 'duration' })
|
||||
expect(screen.getByRole('textbox')).toHaveValue('1h 2m 5s')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -15,6 +15,7 @@ export * from './perPageStore'
|
|||
export * from './PlayButton'
|
||||
export * from './QuickFilter'
|
||||
export * from './RangeField'
|
||||
export * from './ReadOnlyFields'
|
||||
export * from './ShuffleAllButton'
|
||||
export * from './SimpleList'
|
||||
export * from './SizeField'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
BooleanInput,
|
||||
required,
|
||||
SaveButton,
|
||||
DateField,
|
||||
useTranslate,
|
||||
useMutation,
|
||||
useNotify,
|
||||
|
|
@ -16,8 +15,13 @@ import {
|
|||
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'
|
||||
import {
|
||||
ReadOnlyDateField,
|
||||
ReadOnlyDurationField,
|
||||
ReadOnlyNumberField,
|
||||
ReadOnlySizeField,
|
||||
Title,
|
||||
} from '../common'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
toolbar: {
|
||||
|
|
@ -26,6 +30,8 @@ const useStyles = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
const readOnlyProps = { resource: 'library', fullWidth: true }
|
||||
|
||||
const LibraryTitle = ({ record }) => {
|
||||
const translate = useTranslate()
|
||||
const resourceName = translate('resources.library.name', { smart_count: 1 })
|
||||
|
|
@ -125,132 +131,40 @@ const LibraryEdit = (props) => {
|
|||
{translate('resources.library.sections.statistics')}
|
||||
</Typography>
|
||||
|
||||
<Box display="flex">
|
||||
<Box flex={1} mr="0.5em">
|
||||
<TextInput
|
||||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalSongs'}
|
||||
label={translate('resources.library.fields.totalSongs')}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={1} ml="0.5em">
|
||||
<TextInput
|
||||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalAlbums'}
|
||||
label={translate(
|
||||
'resources.library.fields.totalAlbums',
|
||||
)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box display="flex">
|
||||
<Box flex={1} mr="0.5em">
|
||||
<TextInput
|
||||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalArtists'}
|
||||
label={translate(
|
||||
'resources.library.fields.totalArtists',
|
||||
)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={1} ml="0.5em">
|
||||
<TextInput
|
||||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalSize'}
|
||||
label={translate('resources.library.fields.totalSize')}
|
||||
format={(v) => formatBytes(v, 2)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box display="flex">
|
||||
<Box flex={1} mr="0.5em">
|
||||
<TextInput
|
||||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalDuration'}
|
||||
label={translate(
|
||||
'resources.library.fields.totalDuration',
|
||||
)}
|
||||
format={formatDuration2}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={1} ml="0.5em">
|
||||
<TextInput
|
||||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalMissingFiles'}
|
||||
label={translate(
|
||||
'resources.library.fields.totalMissingFiles',
|
||||
)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Timestamps Section */}
|
||||
<Box mb="1em">
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
gutterBottom
|
||||
>
|
||||
{translate('resources.library.fields.lastScanAt')}
|
||||
</Typography>
|
||||
<DateField
|
||||
variant="body1"
|
||||
source="lastScanAt"
|
||||
showTime
|
||||
record={formProps.record}
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns="1fr 1fr"
|
||||
gridColumnGap="1em"
|
||||
>
|
||||
<ReadOnlyNumberField
|
||||
source="totalSongs"
|
||||
{...readOnlyProps}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box mb="1em">
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
gutterBottom
|
||||
>
|
||||
{translate('resources.library.fields.updatedAt')}
|
||||
</Typography>
|
||||
<DateField
|
||||
variant="body1"
|
||||
source="updatedAt"
|
||||
showTime
|
||||
record={formProps.record}
|
||||
<ReadOnlyNumberField
|
||||
source="totalAlbums"
|
||||
{...readOnlyProps}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box mb="2em">
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
gutterBottom
|
||||
>
|
||||
{translate('resources.library.fields.createdAt')}
|
||||
</Typography>
|
||||
<DateField
|
||||
variant="body1"
|
||||
source="createdAt"
|
||||
showTime
|
||||
record={formProps.record}
|
||||
<ReadOnlyNumberField
|
||||
source="totalArtists"
|
||||
{...readOnlyProps}
|
||||
/>
|
||||
<ReadOnlySizeField source="totalSize" {...readOnlyProps} />
|
||||
<ReadOnlyDurationField
|
||||
source="totalDuration"
|
||||
{...readOnlyProps}
|
||||
/>
|
||||
<ReadOnlyNumberField
|
||||
source="totalMissingFiles"
|
||||
{...readOnlyProps}
|
||||
/>
|
||||
<Box gridColumn="1 / -1">
|
||||
<ReadOnlyDateField
|
||||
source="lastScanAt"
|
||||
{...readOnlyProps}
|
||||
/>
|
||||
</Box>
|
||||
<ReadOnlyDateField source="updatedAt" {...readOnlyProps} />
|
||||
<ReadOnlyDateField source="createdAt" {...readOnlyProps} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
TextField,
|
||||
Edit,
|
||||
SimpleForm,
|
||||
useTranslate,
|
||||
|
|
@ -9,7 +8,7 @@ import {
|
|||
Toolbar,
|
||||
} from 'react-admin'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { Title } from '../common'
|
||||
import { ReadOnlyTextField, Title } from '../common'
|
||||
import ApiKeyInput from './ApiKeyInput'
|
||||
import { playerInputs } from './playerInputs'
|
||||
|
||||
|
|
@ -45,8 +44,8 @@ const PlayerEdit = (props) => (
|
|||
<Edit title={<PlayerTitle />} mutationMode="pessimistic" {...props}>
|
||||
<SimpleForm variant={'outlined'} toolbar={<PlayerEditToolbar />}>
|
||||
{playerInputs()}
|
||||
<TextField source="client" />
|
||||
<TextField source="userName" />
|
||||
<ReadOnlyTextField source="client" />
|
||||
<ReadOnlyTextField source="userName" />
|
||||
<ApiKeyInput source="apiKey" />
|
||||
</SimpleForm>
|
||||
</Edit>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
FormDataConsumer,
|
||||
SimpleForm,
|
||||
TextInput,
|
||||
TextField,
|
||||
BooleanInput,
|
||||
required,
|
||||
useTranslate,
|
||||
|
|
@ -11,13 +10,14 @@ import {
|
|||
ReferenceInput,
|
||||
SelectInput,
|
||||
} from 'react-admin'
|
||||
import { isWritable, Title } from '../common'
|
||||
import { isWritable, ReadOnlyTextField, Title } from '../common'
|
||||
|
||||
const SyncFragment = ({ formData, variant, ...rest }) => {
|
||||
if (!formData.path) return null
|
||||
return (
|
||||
<>
|
||||
{formData.path && <BooleanInput source="sync" {...rest} />}
|
||||
{formData.path && <TextField source="path" {...rest} />}
|
||||
<BooleanInput source="sync" {...rest} />
|
||||
<ReadOnlyTextField source="path" {...rest} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -56,10 +56,10 @@ const PlaylistEditForm = (props) => {
|
|||
/>
|
||||
</ReferenceInput>
|
||||
) : (
|
||||
<TextField source="ownerName" />
|
||||
<ReadOnlyTextField source="ownerName" />
|
||||
)}
|
||||
<BooleanInput source="public" disabled={!isWritable(record.ownerId)} />
|
||||
<FormDataConsumer>
|
||||
<FormDataConsumer fullWidth>
|
||||
{(formDataProps) => <SyncFragment {...formDataProps} />}
|
||||
</FormDataConsumer>
|
||||
</SimpleForm>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
DateField,
|
||||
Edit,
|
||||
required,
|
||||
SimpleForm,
|
||||
|
|
@ -9,7 +8,12 @@ import {
|
|||
import { CardMedia } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { urlValidate } from '../utils/validations'
|
||||
import { Title, ImageUploadOverlay, useImageLoadingState } from '../common'
|
||||
import {
|
||||
Title,
|
||||
ImageUploadOverlay,
|
||||
ReadOnlyDateField,
|
||||
useImageLoadingState,
|
||||
} from '../common'
|
||||
import subsonic from '../subsonic'
|
||||
import config from '../config'
|
||||
import { RADIO_PLACEHOLDER_IMAGE } from '../consts'
|
||||
|
|
@ -65,8 +69,8 @@ const RadioEdit = (props) => {
|
|||
fullWidth
|
||||
validate={[urlValidate]}
|
||||
/>
|
||||
<DateField variant="body1" source="updatedAt" showTime />
|
||||
<DateField variant="body1" source="createdAt" showTime />
|
||||
<ReadOnlyDateField source="updatedAt" />
|
||||
<ReadOnlyDateField source="createdAt" />
|
||||
</SimpleForm>
|
||||
</Edit>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ import {
|
|||
DateTimeInput,
|
||||
BooleanInput,
|
||||
Edit,
|
||||
NumberField,
|
||||
SimpleForm,
|
||||
TextInput,
|
||||
} from 'react-admin'
|
||||
import { sharePlayerUrl } from '../utils'
|
||||
import { Link } from '@material-ui/core'
|
||||
import { DateField } from '../common'
|
||||
import {
|
||||
ReadOnlyDateField,
|
||||
ReadOnlyNumberField,
|
||||
ReadOnlyTextField,
|
||||
} from '../common'
|
||||
import config from '../config'
|
||||
|
||||
export const ShareEdit = (props) => {
|
||||
|
|
@ -16,20 +19,26 @@ export const ShareEdit = (props) => {
|
|||
const url = sharePlayerUrl(id)
|
||||
return (
|
||||
<Edit {...props}>
|
||||
<SimpleForm {...rest}>
|
||||
<Link source="URL" href={url} target="_blank" rel="noopener noreferrer">
|
||||
<SimpleForm variant={'outlined'} {...rest}>
|
||||
<Link
|
||||
source="URL"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="inherit"
|
||||
>
|
||||
{url}
|
||||
</Link>
|
||||
<TextInput source="description" />
|
||||
{config.enableDownloads && <BooleanInput source="downloadable" />}
|
||||
<DateTimeInput source="expiresAt" />
|
||||
<TextInput source="contents" disabled />
|
||||
<TextInput source="format" disabled />
|
||||
<TextInput source="maxBitRate" disabled />
|
||||
<TextInput source="username" disabled />
|
||||
<NumberField source="visitCount" disabled />
|
||||
<DateField source="lastVisitedAt" disabled showTime />
|
||||
<DateField source="createdAt" disabled showTime />
|
||||
<ReadOnlyTextField source="contents" />
|
||||
<ReadOnlyTextField source="format" />
|
||||
<ReadOnlyTextField source="maxBitRate" />
|
||||
<ReadOnlyTextField source="username" />
|
||||
<ReadOnlyNumberField source="visitCount" />
|
||||
<ReadOnlyDateField source="lastVisitedAt" />
|
||||
<ReadOnlyDateField source="createdAt" />
|
||||
</SimpleForm>
|
||||
</Edit>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import {
|
||||
TextInput,
|
||||
BooleanInput,
|
||||
DateField,
|
||||
PasswordInput,
|
||||
Edit,
|
||||
required,
|
||||
|
|
@ -21,7 +20,7 @@ import {
|
|||
useRecordContext,
|
||||
} from 'react-admin'
|
||||
import { Typography } from '@material-ui/core'
|
||||
import { Title } from '../common'
|
||||
import { ReadOnlyDateField, Title } from '../common'
|
||||
import DeleteUserButton from './DeleteUserButton'
|
||||
import { LibrarySelectionField } from './LibrarySelectionField.jsx'
|
||||
import { validateUserForm } from './userValidation'
|
||||
|
|
@ -183,10 +182,10 @@ const UserEdit = (props) => {
|
|||
helperText={translate('resources.user.helperTexts.scrobbleFilter')}
|
||||
/>
|
||||
|
||||
<DateField variant="body1" source="lastLoginAt" showTime />
|
||||
<DateField variant="body1" source="lastAccessAt" showTime />
|
||||
<DateField variant="body1" source="updatedAt" showTime />
|
||||
<DateField variant="body1" source="createdAt" showTime />
|
||||
<ReadOnlyDateField source="lastLoginAt" />
|
||||
<ReadOnlyDateField source="lastAccessAt" />
|
||||
<ReadOnlyDateField source="updatedAt" />
|
||||
<ReadOnlyDateField source="createdAt" />
|
||||
</SimpleForm>
|
||||
</Edit>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -51,9 +51,6 @@ vi.mock('react-admin', () => ({
|
|||
BooleanInput: ({ source }) => (
|
||||
<input type="checkbox" data-testid={`boolean-input-${source}`} />
|
||||
),
|
||||
DateField: ({ source }) => (
|
||||
<div data-testid={`date-field-${source}`}>Date</div>
|
||||
),
|
||||
PasswordInput: ({ source }) => (
|
||||
<input type="password" data-testid={`password-input-${source}`} />
|
||||
),
|
||||
|
|
@ -82,6 +79,9 @@ vi.mock('./DeleteUserButton', () => ({
|
|||
|
||||
vi.mock('../common', () => ({
|
||||
Title: ({ subTitle }) => <div data-testid="title">{subTitle}</div>,
|
||||
ReadOnlyDateField: ({ source }) => (
|
||||
<div data-testid={`date-field-${source}`}>Date</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Material-UI
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue