mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-28 14:17:10 +01:00
feat(user): add library selection for user creation and editing
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
e9d6a8eccf
commit
db167f56b9
8 changed files with 554 additions and 6 deletions
141
ui/src/common/SelectLibraryInput.jsx
Normal file
141
ui/src/common/SelectLibraryInput.jsx
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import CheckBoxIcon from '@material-ui/icons/CheckBox'
|
||||
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Typography,
|
||||
} from '@material-ui/core'
|
||||
import { useGetList } from 'react-admin'
|
||||
import PropTypes from 'prop-types'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: '960px', // MD breakpoint width
|
||||
maxWidth: '100%',
|
||||
},
|
||||
libraryList: {
|
||||
height: '120px', // 3 rows of libraries
|
||||
overflow: 'auto',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
listItem: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
emptyMessage: {
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}))
|
||||
|
||||
const EmptyLibraryMessage = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<div className={classes.emptyMessage}>
|
||||
<Typography variant="body2">No libraries available</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const LibraryListItem = ({ library, isSelected, onToggle }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
className={classes.listItem}
|
||||
button
|
||||
onClick={() => onToggle(library)}
|
||||
dense
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
|
||||
checkedIcon={<CheckBoxIcon fontSize="small" />}
|
||||
checked={isSelected}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={library.name} />
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
export const SelectLibraryInput = ({ onChange, value = [] }) => {
|
||||
const classes = useStyles()
|
||||
const [selectedLibraryIds, setSelectedLibraryIds] = useState([])
|
||||
|
||||
const { ids, data } = useGetList(
|
||||
'library',
|
||||
{ page: 1, perPage: -1 },
|
||||
{ field: 'name', order: 'ASC' },
|
||||
{ smart: false },
|
||||
)
|
||||
|
||||
const options = (ids && ids.map((id) => data[id])) || []
|
||||
|
||||
// Update selectedLibraryIds when value prop changes (for editing mode)
|
||||
useEffect(() => {
|
||||
if (Array.isArray(value)) {
|
||||
const libraryIds = value.map((item) =>
|
||||
typeof item === 'object' ? item.id : item,
|
||||
)
|
||||
setSelectedLibraryIds(libraryIds)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const isLibrarySelected = (library) => selectedLibraryIds.includes(library.id)
|
||||
|
||||
const handleLibraryToggle = (library) => {
|
||||
const isSelected = selectedLibraryIds.includes(library.id)
|
||||
let newSelection
|
||||
|
||||
if (isSelected) {
|
||||
newSelection = selectedLibraryIds.filter((id) => id !== library.id)
|
||||
} else {
|
||||
newSelection = [...selectedLibraryIds, library.id]
|
||||
}
|
||||
|
||||
setSelectedLibraryIds(newSelection)
|
||||
onChange(newSelection)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<List className={classes.libraryList}>
|
||||
{options.length === 0 ? (
|
||||
<EmptyLibraryMessage />
|
||||
) : (
|
||||
options.map((library) => (
|
||||
<LibraryListItem
|
||||
key={library.id}
|
||||
library={library}
|
||||
isSelected={isLibrarySelected(library)}
|
||||
onToggle={handleLibraryToggle}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SelectLibraryInput.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.array,
|
||||
}
|
||||
|
||||
LibraryListItem.propTypes = {
|
||||
library: PropTypes.object.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
122
ui/src/common/SelectLibraryInput.test.jsx
Normal file
122
ui/src/common/SelectLibraryInput.test.jsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import * as React from 'react'
|
||||
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
|
||||
import { SelectLibraryInput } from './SelectLibraryInput'
|
||||
import { useGetList } from 'react-admin'
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
|
||||
// Mock the react-admin hook
|
||||
vi.mock('react-admin', () => ({
|
||||
useGetList: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('<SelectLibraryInput />', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the mock before each test
|
||||
mockOnChange.mockClear()
|
||||
})
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
it('should render empty message when no libraries available', () => {
|
||||
// Mock empty library response
|
||||
useGetList.mockReturnValue({
|
||||
ids: [],
|
||||
data: {},
|
||||
})
|
||||
|
||||
render(<SelectLibraryInput onChange={mockOnChange} value={[]} />)
|
||||
expect(screen.getByText('No libraries available')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should render libraries when available', () => {
|
||||
// Mock libraries
|
||||
const mockLibraries = {
|
||||
1: { id: '1', name: 'Library 1' },
|
||||
2: { id: '2', name: 'Library 2' },
|
||||
}
|
||||
useGetList.mockReturnValue({
|
||||
ids: ['1', '2'],
|
||||
data: mockLibraries,
|
||||
})
|
||||
|
||||
render(<SelectLibraryInput onChange={mockOnChange} value={[]} />)
|
||||
expect(screen.getByText('Library 1')).not.toBeNull()
|
||||
expect(screen.getByText('Library 2')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should toggle selection when a library is clicked', () => {
|
||||
// Mock libraries
|
||||
const mockLibraries = {
|
||||
1: { id: '1', name: 'Library 1' },
|
||||
2: { id: '2', name: 'Library 2' },
|
||||
}
|
||||
|
||||
// Test selecting an item
|
||||
useGetList.mockReturnValue({
|
||||
ids: ['1', '2'],
|
||||
data: mockLibraries,
|
||||
})
|
||||
render(<SelectLibraryInput onChange={mockOnChange} value={[]} />)
|
||||
|
||||
// First test - click to select
|
||||
fireEvent.click(screen.getAllByRole('button')[0]) // Click first list item
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['1'])
|
||||
|
||||
// Clean up to avoid DOM duplication
|
||||
cleanup()
|
||||
mockOnChange.mockClear()
|
||||
|
||||
// Test deselecting an item
|
||||
useGetList.mockReturnValue({
|
||||
ids: ['1', '2'],
|
||||
data: mockLibraries,
|
||||
})
|
||||
render(<SelectLibraryInput onChange={mockOnChange} value={['1']} />)
|
||||
|
||||
// Second test - click to deselect
|
||||
fireEvent.click(screen.getAllByRole('button')[0]) // Click first list item again
|
||||
expect(mockOnChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it('should correctly initialize with provided values', () => {
|
||||
// Mock libraries
|
||||
const mockLibraries = {
|
||||
1: { id: '1', name: 'Library 1' },
|
||||
2: { id: '2', name: 'Library 2' },
|
||||
}
|
||||
useGetList.mockReturnValue({
|
||||
ids: ['1', '2'],
|
||||
data: mockLibraries,
|
||||
})
|
||||
|
||||
// Initial value as array of IDs
|
||||
render(<SelectLibraryInput onChange={mockOnChange} value={['1']} />)
|
||||
|
||||
// Check that checkbox for Library 1 is checked
|
||||
const checkboxes = screen.getAllByRole('checkbox')
|
||||
expect(checkboxes[0].checked).toBe(true)
|
||||
expect(checkboxes[1].checked).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle value as array of objects', () => {
|
||||
// Mock libraries
|
||||
const mockLibraries = {
|
||||
1: { id: '1', name: 'Library 1' },
|
||||
2: { id: '2', name: 'Library 2' },
|
||||
}
|
||||
useGetList.mockReturnValue({
|
||||
ids: ['1', '2'],
|
||||
data: mockLibraries,
|
||||
})
|
||||
|
||||
// Initial value as array of objects with id property
|
||||
render(<SelectLibraryInput onChange={mockOnChange} value={[{ id: '2' }]} />)
|
||||
|
||||
// Check that checkbox for Library 2 is checked
|
||||
const checkboxes = screen.getAllByRole('checkbox')
|
||||
expect(checkboxes[0].checked).toBe(false)
|
||||
expect(checkboxes[1].checked).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -43,6 +43,60 @@ const callDeleteMany = (resource, params) => {
|
|||
}).then((response) => ({ data: response.json.ids || [] }))
|
||||
}
|
||||
|
||||
// Helper function to handle user-library associations
|
||||
const handleUserLibraryAssociation = async (userId, libraryIds) => {
|
||||
if (!libraryIds || libraryIds.length === 0) {
|
||||
return // Admin users or users without library assignments
|
||||
}
|
||||
|
||||
try {
|
||||
await httpClient(`${REST_URL}/user/${userId}/library`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ libraryIds }),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error setting user libraries:', error) //eslint-disable-line no-console
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced user creation that handles library associations
|
||||
const createUser = async (params) => {
|
||||
const { data } = params
|
||||
const { libraryIds, ...userData } = data
|
||||
|
||||
// First create the user
|
||||
const userResponse = await dataProvider.create('user', { data: userData })
|
||||
const userId = userResponse.data.id
|
||||
|
||||
// Then set library associations for non-admin users
|
||||
if (!userData.isAdmin && libraryIds && libraryIds.length > 0) {
|
||||
await handleUserLibraryAssociation(userId, libraryIds)
|
||||
}
|
||||
|
||||
return userResponse
|
||||
}
|
||||
|
||||
// Enhanced user update that handles library associations
|
||||
const updateUser = async (params) => {
|
||||
const { data } = params
|
||||
const { libraryIds, ...userData } = data
|
||||
const userId = params.id
|
||||
|
||||
// First update the user
|
||||
const userResponse = await dataProvider.update('user', {
|
||||
...params,
|
||||
data: userData,
|
||||
})
|
||||
|
||||
// Then handle library associations for non-admin users
|
||||
if (!userData.isAdmin && libraryIds !== undefined) {
|
||||
await handleUserLibraryAssociation(userId, libraryIds)
|
||||
}
|
||||
|
||||
return userResponse
|
||||
}
|
||||
|
||||
const wrapperDataProvider = {
|
||||
...dataProvider,
|
||||
getList: (resource, params) => {
|
||||
|
|
@ -62,6 +116,9 @@ const wrapperDataProvider = {
|
|||
return dataProvider.getManyReference(r, p)
|
||||
},
|
||||
update: (resource, params) => {
|
||||
if (resource === 'user') {
|
||||
return updateUser(params)
|
||||
}
|
||||
const [r, p] = mapResource(resource, params)
|
||||
return dataProvider.update(r, p)
|
||||
},
|
||||
|
|
@ -70,6 +127,9 @@ const wrapperDataProvider = {
|
|||
return dataProvider.updateMany(r, p)
|
||||
},
|
||||
create: (resource, params) => {
|
||||
if (resource === 'user') {
|
||||
return createUser(params)
|
||||
}
|
||||
const [r, p] = mapResource(resource, params)
|
||||
return dataProvider.create(r, p)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -149,19 +149,27 @@
|
|||
"changePassword": "Change Password?",
|
||||
"currentPassword": "Current Password",
|
||||
"newPassword": "New Password",
|
||||
"token": "Token"
|
||||
"token": "Token",
|
||||
"libraries": "Libraries"
|
||||
},
|
||||
"helperTexts": {
|
||||
"name": "Changes to your name will only be reflected on next login"
|
||||
"name": "Changes to your name will only be reflected on next login",
|
||||
"libraries": "Select which libraries this user can access"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "User created",
|
||||
"updated": "User updated",
|
||||
"deleted": "User deleted"
|
||||
},
|
||||
"validation": {
|
||||
"librariesRequired": "At least one library must be selected for non-admin users"
|
||||
},
|
||||
"message": {
|
||||
"listenBrainzToken": "Enter your ListenBrainz user token.",
|
||||
"clickHereForToken": "Click here to get your token"
|
||||
},
|
||||
"messages": {
|
||||
"adminAutoLibraries": "Admin users automatically have access to all libraries"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
|
|
|
|||
31
ui/src/user/LibrarySelectionField.jsx
Normal file
31
ui/src/user/LibrarySelectionField.jsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { useInput, useTranslate } from 'react-admin'
|
||||
import { Box, FormControl, FormLabel, Typography } from '@material-ui/core'
|
||||
import { SelectLibraryInput } from '../common/SelectLibraryInput.jsx'
|
||||
import React from 'react'
|
||||
|
||||
export const LibrarySelectionField = () => {
|
||||
const translate = useTranslate()
|
||||
const {
|
||||
input: { name, onChange, value },
|
||||
meta: { error, touched },
|
||||
} = useInput({ source: 'libraryIds' })
|
||||
|
||||
return (
|
||||
<FormControl error={!!(touched && error)} fullWidth margin="normal">
|
||||
<FormLabel component="legend" required>
|
||||
{translate('resources.user.fields.libraries')}
|
||||
</FormLabel>
|
||||
<Box mt={1} mb={1}>
|
||||
<SelectLibraryInput onChange={onChange} value={value || []} />
|
||||
</Box>
|
||||
{touched && error && (
|
||||
<Typography color="error" variant="caption">
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{translate('resources.user.helperTexts.libraries')}
|
||||
</Typography>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
109
ui/src/user/LibrarySelectionField.test.jsx
Normal file
109
ui/src/user/LibrarySelectionField.test.jsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import * as React from 'react'
|
||||
import { render, screen, cleanup } from '@testing-library/react'
|
||||
import { LibrarySelectionField } from './LibrarySelectionField'
|
||||
import { useInput, useTranslate } from 'react-admin'
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { SelectLibraryInput } from '../common/SelectLibraryInput'
|
||||
|
||||
// Mock the react-admin hooks
|
||||
vi.mock('react-admin', () => ({
|
||||
useInput: vi.fn(),
|
||||
useTranslate: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock the SelectLibraryInput component
|
||||
vi.mock('../common/SelectLibraryInput.jsx', () => ({
|
||||
SelectLibraryInput: vi.fn(() => <div data-testid="select-library-input" />),
|
||||
}))
|
||||
|
||||
describe('<LibrarySelectionField />', () => {
|
||||
const defaultProps = {
|
||||
input: {
|
||||
name: 'libraryIds',
|
||||
value: [],
|
||||
onChange: vi.fn(),
|
||||
},
|
||||
meta: {
|
||||
touched: false,
|
||||
error: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
const mockTranslate = vi.fn((key) => key)
|
||||
|
||||
beforeEach(() => {
|
||||
useInput.mockReturnValue(defaultProps)
|
||||
useTranslate.mockReturnValue(mockTranslate)
|
||||
SelectLibraryInput.mockClear()
|
||||
})
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
it('should render field label from translations', () => {
|
||||
render(<LibrarySelectionField />)
|
||||
expect(screen.getByText('resources.user.fields.libraries')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should render helper text from translations', () => {
|
||||
render(<LibrarySelectionField />)
|
||||
expect(
|
||||
screen.getByText('resources.user.helperTexts.libraries'),
|
||||
).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should render SelectLibraryInput with correct props', () => {
|
||||
render(<LibrarySelectionField />)
|
||||
expect(screen.getByTestId('select-library-input')).not.toBeNull()
|
||||
expect(SelectLibraryInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onChange: defaultProps.input.onChange,
|
||||
value: defaultProps.input.value,
|
||||
}),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
|
||||
it('should render error message when touched and has error', () => {
|
||||
useInput.mockReturnValue({
|
||||
...defaultProps,
|
||||
meta: {
|
||||
touched: true,
|
||||
error: 'This field is required',
|
||||
},
|
||||
})
|
||||
|
||||
render(<LibrarySelectionField />)
|
||||
expect(screen.getByText('This field is required')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not render error message when not touched', () => {
|
||||
useInput.mockReturnValue({
|
||||
...defaultProps,
|
||||
meta: {
|
||||
touched: false,
|
||||
error: 'This field is required',
|
||||
},
|
||||
})
|
||||
|
||||
render(<LibrarySelectionField />)
|
||||
expect(screen.queryByText('This field is required')).toBeNull()
|
||||
})
|
||||
|
||||
it('should initialize with empty array when value is null', () => {
|
||||
useInput.mockReturnValue({
|
||||
...defaultProps,
|
||||
input: {
|
||||
...defaultProps.input,
|
||||
value: null,
|
||||
},
|
||||
})
|
||||
|
||||
render(<LibrarySelectionField />)
|
||||
expect(SelectLibraryInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
value: [],
|
||||
}),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -2,17 +2,20 @@ import React, { useCallback } from 'react'
|
|||
import {
|
||||
BooleanInput,
|
||||
Create,
|
||||
TextInput,
|
||||
email,
|
||||
FormDataConsumer,
|
||||
PasswordInput,
|
||||
required,
|
||||
email,
|
||||
SimpleForm,
|
||||
useTranslate,
|
||||
TextInput,
|
||||
useMutation,
|
||||
useNotify,
|
||||
useRedirect,
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import { Typography } from '@material-ui/core'
|
||||
import { Title } from '../common'
|
||||
import { LibrarySelectionField } from './LibrarySelectionField.jsx'
|
||||
|
||||
const UserCreate = (props) => {
|
||||
const translate = useTranslate()
|
||||
|
|
@ -48,9 +51,24 @@ const UserCreate = (props) => {
|
|||
[mutate, notify, redirect],
|
||||
)
|
||||
|
||||
// Custom validation function
|
||||
const validateUserForm = (values) => {
|
||||
const errors = {}
|
||||
// Only require library selection for non-admin users
|
||||
if (
|
||||
!values.isAdmin &&
|
||||
(!values.libraryIds || values.libraryIds.length === 0)
|
||||
) {
|
||||
errors.libraryIds = translate(
|
||||
'resources.user.validation.librariesRequired',
|
||||
)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
return (
|
||||
<Create title={<Title subTitle={title} />} {...props}>
|
||||
<SimpleForm save={save} variant={'outlined'}>
|
||||
<SimpleForm save={save} validate={validateUserForm} variant={'outlined'}>
|
||||
<TextInput
|
||||
spellCheck={false}
|
||||
source="userName"
|
||||
|
|
@ -64,6 +82,25 @@ const UserCreate = (props) => {
|
|||
validate={[required()]}
|
||||
/>
|
||||
<BooleanInput source="isAdmin" defaultValue={false} />
|
||||
|
||||
{/* Conditional Library Selection */}
|
||||
<FormDataConsumer>
|
||||
{({ formData }) => (
|
||||
<>
|
||||
{!formData.isAdmin && <LibrarySelectionField />}
|
||||
|
||||
{formData.isAdmin && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 16, marginBottom: 16 }}
|
||||
>
|
||||
{translate('resources.user.messages.adminAutoLibraries')}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FormDataConsumer>
|
||||
</SimpleForm>
|
||||
</Create>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ import {
|
|||
FormDataConsumer,
|
||||
usePermissions,
|
||||
} from 'react-admin'
|
||||
import { Typography } from '@material-ui/core'
|
||||
import { Title } from '../common'
|
||||
import DeleteUserButton from './DeleteUserButton'
|
||||
import { LibrarySelectionField } from './LibrarySelectionField.jsx'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
toolbar: {
|
||||
|
|
@ -100,12 +102,28 @@ const UserEdit = (props) => {
|
|||
[mutate, notify, permissions, redirect, refresh],
|
||||
)
|
||||
|
||||
// Custom validation function
|
||||
const validateUserForm = (values) => {
|
||||
const errors = {}
|
||||
// Only require library selection for non-admin users
|
||||
if (
|
||||
!values.isAdmin &&
|
||||
(!values.libraryIds || values.libraryIds.length === 0)
|
||||
) {
|
||||
errors.libraryIds = translate(
|
||||
'resources.user.validation.librariesRequired',
|
||||
)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
return (
|
||||
<Edit title={<UserTitle />} undoable={false} {...props}>
|
||||
<SimpleForm
|
||||
variant={'outlined'}
|
||||
toolbar={<UserToolbar showDelete={canDelete} />}
|
||||
save={save}
|
||||
validate={validateUserForm}
|
||||
>
|
||||
{permissions === 'admin' && (
|
||||
<TextInput
|
||||
|
|
@ -139,6 +157,28 @@ const UserEdit = (props) => {
|
|||
{permissions === 'admin' && (
|
||||
<BooleanInput source="isAdmin" initialValue={false} />
|
||||
)}
|
||||
|
||||
{/* Conditional Library Selection for Admin Users Only */}
|
||||
{permissions === 'admin' && (
|
||||
<FormDataConsumer>
|
||||
{({ formData }) => (
|
||||
<>
|
||||
{!formData.isAdmin && <LibrarySelectionField />}
|
||||
|
||||
{formData.isAdmin && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 16, marginBottom: 16 }}
|
||||
>
|
||||
{translate('resources.user.messages.adminAutoLibraries')}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FormDataConsumer>
|
||||
)}
|
||||
|
||||
<DateField variant="body1" source="lastLoginAt" showTime />
|
||||
<DateField variant="body1" source="lastAccessAt" showTime />
|
||||
<DateField variant="body1" source="updatedAt" showTime />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue