mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-27 21:57:17 +01:00
feat(library): enhance LibrarySelectionField to extract library IDs from record
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
c1365cd236
commit
0b61df1b90
3 changed files with 210 additions and 4 deletions
|
|
@ -1,22 +1,39 @@
|
|||
import { useInput, useTranslate } from 'react-admin'
|
||||
import { useInput, useTranslate, useRecordContext } from 'react-admin'
|
||||
import { Box, FormControl, FormLabel, Typography } from '@material-ui/core'
|
||||
import { SelectLibraryInput } from '../common/SelectLibraryInput.jsx'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
export const LibrarySelectionField = () => {
|
||||
const translate = useTranslate()
|
||||
const record = useRecordContext()
|
||||
|
||||
const {
|
||||
input: { name, onChange, value },
|
||||
meta: { error, touched },
|
||||
} = useInput({ source: 'libraryIds' })
|
||||
|
||||
// Extract library IDs from either 'libraries' array or 'libraryIds' array
|
||||
const libraryIds = useMemo(() => {
|
||||
// First check if form has libraryIds (create mode or already transformed)
|
||||
if (value && Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
// Then check if record has libraries array (edit mode from backend)
|
||||
if (record?.libraries && Array.isArray(record.libraries)) {
|
||||
return record.libraries.map((lib) => lib.id)
|
||||
}
|
||||
|
||||
return []
|
||||
}, [value, record])
|
||||
|
||||
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 || []} />
|
||||
<SelectLibraryInput onChange={onChange} value={libraryIds} />
|
||||
</Box>
|
||||
{touched && error && (
|
||||
<Typography color="error" variant="caption">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
import { render, screen, cleanup } from '@testing-library/react'
|
||||
import { LibrarySelectionField } from './LibrarySelectionField'
|
||||
import { useInput, useTranslate } from 'react-admin'
|
||||
import { useInput, useTranslate, useRecordContext } from 'react-admin'
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { SelectLibraryInput } from '../common/SelectLibraryInput'
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ import { SelectLibraryInput } from '../common/SelectLibraryInput'
|
|||
vi.mock('react-admin', () => ({
|
||||
useInput: vi.fn(),
|
||||
useTranslate: vi.fn(),
|
||||
useRecordContext: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock the SelectLibraryInput component
|
||||
|
|
@ -34,6 +35,7 @@ describe('<LibrarySelectionField />', () => {
|
|||
beforeEach(() => {
|
||||
useInput.mockReturnValue(defaultProps)
|
||||
useTranslate.mockReturnValue(mockTranslate)
|
||||
useRecordContext.mockReturnValue({})
|
||||
SelectLibraryInput.mockClear()
|
||||
})
|
||||
|
||||
|
|
@ -106,4 +108,61 @@ describe('<LibrarySelectionField />', () => {
|
|||
expect.anything(),
|
||||
)
|
||||
})
|
||||
|
||||
it('should extract library IDs from record libraries array when editing user', () => {
|
||||
// Mock a record with libraries array (from backend during edit)
|
||||
useRecordContext.mockReturnValue({
|
||||
id: 'user123',
|
||||
name: 'John Doe',
|
||||
libraries: [
|
||||
{ id: 1, name: 'Music Library 1', path: '/music1' },
|
||||
{ id: 3, name: 'Music Library 3', path: '/music3' },
|
||||
],
|
||||
})
|
||||
|
||||
// Mock input without libraryIds (edit mode scenario)
|
||||
useInput.mockReturnValue({
|
||||
...defaultProps,
|
||||
input: {
|
||||
...defaultProps.input,
|
||||
value: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
render(<LibrarySelectionField />)
|
||||
expect(SelectLibraryInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
value: [1, 3], // Should extract IDs from libraries array
|
||||
}),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
|
||||
it('should prefer libraryIds when both libraryIds and libraries are present', () => {
|
||||
// Mock a record with libraries array
|
||||
useRecordContext.mockReturnValue({
|
||||
id: 'user123',
|
||||
libraries: [
|
||||
{ id: 1, name: 'Music Library 1', path: '/music1' },
|
||||
{ id: 3, name: 'Music Library 3', path: '/music3' },
|
||||
],
|
||||
})
|
||||
|
||||
// Mock input with explicit libraryIds (create mode or already transformed)
|
||||
useInput.mockReturnValue({
|
||||
...defaultProps,
|
||||
input: {
|
||||
...defaultProps.input,
|
||||
value: [2, 4], // Different IDs than in libraries
|
||||
},
|
||||
})
|
||||
|
||||
render(<LibrarySelectionField />)
|
||||
expect(SelectLibraryInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
value: [2, 4], // Should prefer libraryIds over libraries
|
||||
}),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
130
ui/src/user/UserEdit.test.jsx
Normal file
130
ui/src/user/UserEdit.test.jsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import * as React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import UserEdit from './UserEdit'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
|
||||
const defaultUser = {
|
||||
id: 'user1',
|
||||
userName: 'testuser',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
isAdmin: false,
|
||||
libraries: [
|
||||
{ id: 1, name: 'Library 1', path: '/music1' },
|
||||
{ id: 2, name: 'Library 2', path: '/music2' },
|
||||
],
|
||||
lastLoginAt: '2023-01-01T12:00:00Z',
|
||||
lastAccessAt: '2023-01-02T12:00:00Z',
|
||||
updatedAt: '2023-01-03T12:00:00Z',
|
||||
createdAt: '2023-01-04T12:00:00Z',
|
||||
}
|
||||
|
||||
const adminUser = {
|
||||
...defaultUser,
|
||||
id: 'admin1',
|
||||
userName: 'admin',
|
||||
name: 'Admin User',
|
||||
isAdmin: true,
|
||||
}
|
||||
|
||||
// Mock React-Admin completely with simpler implementations
|
||||
vi.mock('react-admin', () => ({
|
||||
Edit: ({ children, title }) => (
|
||||
<div data-testid="edit-component">
|
||||
{title}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SimpleForm: ({ children }) => (
|
||||
<form data-testid="simple-form">{children}</form>
|
||||
),
|
||||
TextInput: ({ source }) => <input data-testid={`text-input-${source}`} />,
|
||||
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}`} />
|
||||
),
|
||||
Toolbar: ({ children }) => <div data-testid="toolbar">{children}</div>,
|
||||
SaveButton: () => <button data-testid="save-button">Save</button>,
|
||||
FormDataConsumer: ({ children }) => children({ formData: {} }),
|
||||
Typography: ({ children }) => <p>{children}</p>,
|
||||
required: () => () => null,
|
||||
email: () => () => null,
|
||||
useMutation: () => [vi.fn()],
|
||||
useNotify: () => vi.fn(),
|
||||
useRedirect: () => vi.fn(),
|
||||
useRefresh: () => vi.fn(),
|
||||
usePermissions: () => ({ permissions: 'admin' }),
|
||||
useTranslate: () => (key) => key,
|
||||
}))
|
||||
|
||||
vi.mock('./LibrarySelectionField.jsx', () => ({
|
||||
LibrarySelectionField: () => <div data-testid="library-selection-field" />,
|
||||
}))
|
||||
|
||||
vi.mock('./DeleteUserButton', () => ({
|
||||
__esModule: true,
|
||||
default: () => <button data-testid="delete-user-button">Delete</button>,
|
||||
}))
|
||||
|
||||
vi.mock('../common', () => ({
|
||||
Title: ({ subTitle }) => <div data-testid="title">{subTitle}</div>,
|
||||
}))
|
||||
|
||||
// Mock Material-UI
|
||||
vi.mock('@material-ui/core/styles', () => ({
|
||||
makeStyles: () => () => ({}),
|
||||
}))
|
||||
|
||||
vi.mock('@material-ui/core', () => ({
|
||||
Typography: ({ children }) => <p>{children}</p>,
|
||||
}))
|
||||
|
||||
describe('<UserEdit />', () => {
|
||||
it('should render the user edit form', () => {
|
||||
render(<UserEdit id="user1" permissions="admin" />)
|
||||
|
||||
// Check if the edit component renders
|
||||
expect(screen.getByTestId('edit-component')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('simple-form')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render text inputs for admin users', () => {
|
||||
render(<UserEdit id="user1" permissions="admin" />)
|
||||
|
||||
// Should render username input for admin
|
||||
expect(screen.getByTestId('text-input-userName')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('text-input-name')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('text-input-email')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render admin checkbox for admin permissions', () => {
|
||||
render(<UserEdit id="user1" permissions="admin" />)
|
||||
|
||||
// Should render isAdmin checkbox for admin users
|
||||
expect(screen.getByTestId('boolean-input-isAdmin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render date fields', () => {
|
||||
render(<UserEdit id="user1" permissions="admin" />)
|
||||
|
||||
expect(screen.getByTestId('date-field-lastLoginAt')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('date-field-lastAccessAt')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('date-field-updatedAt')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('date-field-createdAt')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render username input for non-admin users', () => {
|
||||
render(<UserEdit id="user1" permissions="user" />)
|
||||
|
||||
// Should not render username input for non-admin
|
||||
expect(screen.queryByTestId('text-input-userName')).not.toBeInTheDocument()
|
||||
// But should still render name and email
|
||||
expect(screen.getByTestId('text-input-name')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('text-input-email')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue