diff --git a/ui/src/user/LibrarySelectionField.jsx b/ui/src/user/LibrarySelectionField.jsx index b18d04951..c2bf9eaba 100644 --- a/ui/src/user/LibrarySelectionField.jsx +++ b/ui/src/user/LibrarySelectionField.jsx @@ -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 ( {translate('resources.user.fields.libraries')} - + {touched && error && ( diff --git a/ui/src/user/LibrarySelectionField.test.jsx b/ui/src/user/LibrarySelectionField.test.jsx index 849c8ba17..9777bab99 100644 --- a/ui/src/user/LibrarySelectionField.test.jsx +++ b/ui/src/user/LibrarySelectionField.test.jsx @@ -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('', () => { beforeEach(() => { useInput.mockReturnValue(defaultProps) useTranslate.mockReturnValue(mockTranslate) + useRecordContext.mockReturnValue({}) SelectLibraryInput.mockClear() }) @@ -106,4 +108,61 @@ describe('', () => { 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() + 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() + expect(SelectLibraryInput).toHaveBeenCalledWith( + expect.objectContaining({ + value: [2, 4], // Should prefer libraryIds over libraries + }), + expect.anything(), + ) + }) }) diff --git a/ui/src/user/UserEdit.test.jsx b/ui/src/user/UserEdit.test.jsx new file mode 100644 index 000000000..75a9a1ada --- /dev/null +++ b/ui/src/user/UserEdit.test.jsx @@ -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 }) => ( +
+ {title} + {children} +
+ ), + SimpleForm: ({ children }) => ( +
{children}
+ ), + TextInput: ({ source }) => , + BooleanInput: ({ source }) => ( + + ), + DateField: ({ source }) => ( +
Date
+ ), + PasswordInput: ({ source }) => ( + + ), + Toolbar: ({ children }) =>
{children}
, + SaveButton: () => , + FormDataConsumer: ({ children }) => children({ formData: {} }), + Typography: ({ children }) =>

{children}

, + 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: () =>
, +})) + +vi.mock('./DeleteUserButton', () => ({ + __esModule: true, + default: () => , +})) + +vi.mock('../common', () => ({ + Title: ({ subTitle }) =>
{subTitle}
, +})) + +// Mock Material-UI +vi.mock('@material-ui/core/styles', () => ({ + makeStyles: () => () => ({}), +})) + +vi.mock('@material-ui/core', () => ({ + Typography: ({ children }) =>

{children}

, +})) + +describe('', () => { + it('should render the user edit form', () => { + render() + + // 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() + + // 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() + + // Should render isAdmin checkbox for admin users + expect(screen.getByTestId('boolean-input-isAdmin')).toBeInTheDocument() + }) + + it('should render date fields', () => { + render() + + 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() + + // 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() + }) +})