mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-29 14:37:49 +01:00
feat: add default library assignment for new users
Introduced a new column `default_new_users` in the library table to facilitate automatic assignment of default libraries to new regular users. When a new user is created, they will now be assigned to libraries marked as default, enhancing user experience by ensuring they have immediate access to essential resources. Additionally, updated the user repository logic to handle this new functionality and modified the user creation validation to reflect that library selection is optional for non-admin users. Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
0b2d728b21
commit
7fbf6c6382
13 changed files with 322 additions and 30 deletions
|
|
@ -36,6 +36,11 @@ func upAddUserLibraryTable(ctx context.Context, tx *sql.Tx) error {
|
|||
UPDATE library SET total_duration = (
|
||||
SELECT IFNULL(SUM(duration),0) from album where album.library_id = library.id and missing = 0
|
||||
);
|
||||
|
||||
-- Add default_new_users column to library table
|
||||
ALTER TABLE library ADD COLUMN default_new_users boolean DEFAULT false;
|
||||
-- Set library ID 1 (default library) as default for new users
|
||||
UPDATE library SET default_new_users = true WHERE id = 1;
|
||||
`)
|
||||
|
||||
return err
|
||||
|
|
@ -47,6 +52,7 @@ func downAddUserLibraryTable(ctx context.Context, tx *sql.Tx) error {
|
|||
DROP INDEX IF EXISTS idx_user_library_user_id;
|
||||
DROP TABLE IF EXISTS user_library;
|
||||
ALTER TABLE library DROP COLUMN IF EXISTS total_duration;
|
||||
ALTER TABLE library DROP COLUMN IF EXISTS default_new_users;
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type Library struct {
|
|||
TotalMissingFiles int `json:"totalMissingFiles" db:"total_missing_files"`
|
||||
TotalSize int64 `json:"totalSize" db:"total_size"`
|
||||
TotalDuration float64 `json:"totalDuration" db:"total_duration"`
|
||||
DefaultNewUsers bool `json:"defaultNewUsers" db:"default_new_users"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -90,10 +90,11 @@ func (r *libraryRepository) Put(l *model.Library) error {
|
|||
} else {
|
||||
// Try to update first
|
||||
cols := map[string]any{
|
||||
"name": l.Name,
|
||||
"path": l.Path,
|
||||
"remote_path": l.RemotePath,
|
||||
"updated_at": time.Now(),
|
||||
"name": l.Name,
|
||||
"path": l.Path,
|
||||
"remote_path": l.RemotePath,
|
||||
"default_new_users": l.DefaultNewUsers,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
sq := Update(r.tableName).SetMap(cols).Where(Eq{"id": l.ID})
|
||||
rowsAffected, updateErr := r.executeSQL(sq)
|
||||
|
|
|
|||
|
|
@ -148,6 +148,15 @@ func (r *userRepository) Put(u *model.User) error {
|
|||
if _, err := r.executeSQL(sql); err != nil {
|
||||
return fmt.Errorf("failed to assign all libraries to admin user: %w", err)
|
||||
}
|
||||
} else if count == 0 { // Only for new regular users
|
||||
// Auto-assign default libraries to new regular users
|
||||
sql := Expr(
|
||||
"INSERT OR IGNORE INTO user_library (user_id, library_id) SELECT ?, id FROM library WHERE default_new_users = true",
|
||||
u.ID,
|
||||
)
|
||||
if _, err := r.executeSQL(sql); err != nil {
|
||||
return fmt.Errorf("failed to assign default libraries to new user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -420,7 +420,7 @@ var _ = Describe("UserRepository", func() {
|
|||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID, 1))
|
||||
})
|
||||
|
||||
It("does not affect regular users", func() {
|
||||
It("assigns default libraries to regular users", func() {
|
||||
regularUser := model.User{
|
||||
ID: "regular-user-id-2",
|
||||
UserName: "regularuser2",
|
||||
|
|
@ -433,10 +433,12 @@ var _ = Describe("UserRepository", func() {
|
|||
err := repo.Put(®ularUser)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Regular user should have no library associations
|
||||
// Regular user should be assigned to default libraries (library ID 1 from migration)
|
||||
libraries, err := repo.GetUserLibraries(regularUser.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(libraries).To(HaveLen(0))
|
||||
Expect(libraries).To(HaveLen(1))
|
||||
Expect(libraries[0].ID).To(Equal(1))
|
||||
Expect(libraries[0].DefaultNewUsers).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -530,8 +532,8 @@ var _ = Describe("UserRepository", func() {
|
|||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID))
|
||||
})
|
||||
|
||||
It("returns empty Libraries array when user has no library associations", func() {
|
||||
// Create a user with no library associations
|
||||
It("returns default Libraries array for new regular users", func() {
|
||||
// Create a user with no explicit library associations - should get default libraries
|
||||
userWithoutLibs := model.User{
|
||||
ID: "no-libs-user",
|
||||
UserName: "nolibsuser",
|
||||
|
|
@ -548,7 +550,9 @@ var _ = Describe("UserRepository", func() {
|
|||
user, err := repo.Get(userWithoutLibs.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(user.Libraries).ToNot(BeNil())
|
||||
Expect(user.Libraries).To(HaveLen(0))
|
||||
// Regular users should be assigned to default libraries (library ID 1 from migration)
|
||||
Expect(user.Libraries).To(HaveLen(1))
|
||||
Expect(user.Libraries[0].ID).To(Equal(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useMemo } from 'react'
|
||||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import CheckBoxIcon from '@material-ui/icons/CheckBox'
|
||||
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'
|
||||
|
|
@ -82,29 +82,75 @@ const LibraryListItem = ({ library, isSelected, onToggle }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const SelectLibraryInput = ({ onChange, value = [] }) => {
|
||||
export const SelectLibraryInput = ({
|
||||
onChange,
|
||||
value = [],
|
||||
isNewUser = false,
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const translate = useTranslate()
|
||||
const [selectedLibraryIds, setSelectedLibraryIds] = useState([])
|
||||
const [hasInitialized, setHasInitialized] = useState(false)
|
||||
|
||||
const { ids, data } = useGetList(
|
||||
const { ids, data, isLoading } = useGetList(
|
||||
'library',
|
||||
{ page: 1, perPage: -1 },
|
||||
{ field: 'name', order: 'ASC' },
|
||||
{ smart: false },
|
||||
)
|
||||
|
||||
const options = (ids && ids.map((id) => data[id])) || []
|
||||
const options = useMemo(
|
||||
() => (ids && ids.map((id) => data[id])) || [],
|
||||
[ids, data],
|
||||
)
|
||||
|
||||
// Update selectedLibraryIds when value prop changes (for editing mode)
|
||||
// Reset initialization state when isNewUser changes
|
||||
useEffect(() => {
|
||||
if (isNewUser) {
|
||||
setHasInitialized(false)
|
||||
}
|
||||
}, [isNewUser])
|
||||
|
||||
// Pre-select default libraries for new users
|
||||
useEffect(() => {
|
||||
if (
|
||||
isNewUser &&
|
||||
!isLoading &&
|
||||
options.length > 0 &&
|
||||
!hasInitialized &&
|
||||
Array.isArray(value) &&
|
||||
value.length === 0
|
||||
) {
|
||||
const defaultLibraryIds = options
|
||||
.filter((lib) => lib.defaultNewUsers)
|
||||
.map((lib) => lib.id)
|
||||
|
||||
if (defaultLibraryIds.length > 0) {
|
||||
setSelectedLibraryIds(defaultLibraryIds)
|
||||
onChange(defaultLibraryIds)
|
||||
}
|
||||
|
||||
setHasInitialized(true)
|
||||
}
|
||||
}, [isNewUser, isLoading, options, hasInitialized, value, onChange])
|
||||
|
||||
// Update selectedLibraryIds when value prop changes (for editing mode and pre-selection)
|
||||
useEffect(() => {
|
||||
// For new users, only sync from value prop if it has actual data
|
||||
// This prevents empty initial state from overriding our pre-selection
|
||||
if (isNewUser && Array.isArray(value) && value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const libraryIds = value.map((item) =>
|
||||
typeof item === 'object' ? item.id : item,
|
||||
)
|
||||
setSelectedLibraryIds(libraryIds)
|
||||
} else if (value.length === 0) {
|
||||
// Handle case where value is explicitly set to empty array (for existing users)
|
||||
setSelectedLibraryIds([])
|
||||
}
|
||||
}, [value])
|
||||
}, [value, isNewUser, hasInitialized])
|
||||
|
||||
const isLibrarySelected = (library) => selectedLibraryIds.includes(library.id)
|
||||
|
||||
|
|
@ -172,6 +218,7 @@ export const SelectLibraryInput = ({ onChange, value = [] }) => {
|
|||
SelectLibraryInput.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.array,
|
||||
isNewUser: PropTypes.bool,
|
||||
}
|
||||
|
||||
LibraryListItem.propTypes = {
|
||||
|
|
|
|||
|
|
@ -259,4 +259,200 @@ describe('<SelectLibraryInput />', () => {
|
|||
// Note: Testing indeterminate property directly through JSDOM can be unreliable
|
||||
// The important behavior is that it's not checked when only some are selected
|
||||
})
|
||||
|
||||
describe('New User Default Library Selection', () => {
|
||||
// Helper function to create mock libraries with configurable default settings
|
||||
const createMockLibraries = (libraryConfigs) => {
|
||||
const libraries = {}
|
||||
const ids = []
|
||||
|
||||
libraryConfigs.forEach(({ id, name, defaultNewUsers }) => {
|
||||
libraries[id] = {
|
||||
id,
|
||||
name,
|
||||
...(defaultNewUsers !== undefined && { defaultNewUsers }),
|
||||
}
|
||||
ids.push(id)
|
||||
})
|
||||
|
||||
return { libraries, ids }
|
||||
}
|
||||
|
||||
// Helper function to setup useGetList mock
|
||||
const setupMockLibraries = (libraryConfigs, isLoading = false) => {
|
||||
const { libraries, ids } = createMockLibraries(libraryConfigs)
|
||||
useGetList.mockReturnValue({
|
||||
ids,
|
||||
data: libraries,
|
||||
isLoading,
|
||||
})
|
||||
return { libraries, ids }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnChange.mockClear()
|
||||
})
|
||||
|
||||
it('should pre-select default libraries for new users', () => {
|
||||
setupMockLibraries([
|
||||
{ id: '1', name: 'Library 1', defaultNewUsers: true },
|
||||
{ id: '2', name: 'Library 2', defaultNewUsers: false },
|
||||
{ id: '3', name: 'Library 3', defaultNewUsers: true },
|
||||
])
|
||||
|
||||
render(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={[]}
|
||||
isNewUser={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['1', '3'])
|
||||
})
|
||||
|
||||
it('should not pre-select default libraries if new user already has values', () => {
|
||||
setupMockLibraries([
|
||||
{ id: '1', name: 'Library 1', defaultNewUsers: true },
|
||||
{ id: '2', name: 'Library 2', defaultNewUsers: false },
|
||||
])
|
||||
|
||||
render(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={['2']} // Already has a value
|
||||
isNewUser={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not pre-select libraries while data is still loading', () => {
|
||||
setupMockLibraries(
|
||||
[{ id: '1', name: 'Library 1', defaultNewUsers: true }],
|
||||
true,
|
||||
) // isLoading = true
|
||||
|
||||
render(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={[]}
|
||||
isNewUser={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not pre-select anything if no libraries have defaultNewUsers flag', () => {
|
||||
setupMockLibraries([
|
||||
{ id: '1', name: 'Library 1', defaultNewUsers: false },
|
||||
{ id: '2', name: 'Library 2', defaultNewUsers: false },
|
||||
])
|
||||
|
||||
render(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={[]}
|
||||
isNewUser={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reset initialization state when isNewUser prop changes', () => {
|
||||
setupMockLibraries([
|
||||
{ id: '1', name: 'Library 1', defaultNewUsers: true },
|
||||
])
|
||||
|
||||
const { rerender } = render(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={[]}
|
||||
isNewUser={false} // Start as existing user
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
|
||||
// Change to new user
|
||||
rerender(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={[]}
|
||||
isNewUser={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['1'])
|
||||
})
|
||||
|
||||
it('should not override pre-selection when value prop is empty for new users', () => {
|
||||
setupMockLibraries([
|
||||
{ id: '1', name: 'Library 1', defaultNewUsers: true },
|
||||
{ id: '2', name: 'Library 2', defaultNewUsers: false },
|
||||
])
|
||||
|
||||
const { rerender } = render(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={[]}
|
||||
isNewUser={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['1'])
|
||||
mockOnChange.mockClear()
|
||||
|
||||
// Re-render with empty value prop (simulating form state update)
|
||||
rerender(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={[]} // Still empty
|
||||
isNewUser={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should sync from value prop for existing users even when empty', () => {
|
||||
setupMockLibraries([
|
||||
{ id: '1', name: 'Library 1', defaultNewUsers: true },
|
||||
])
|
||||
|
||||
render(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={[]} // Empty value for existing user
|
||||
isNewUser={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Check that no libraries are selected (checkboxes should be unchecked)
|
||||
const checkboxes = screen.getAllByRole('checkbox')
|
||||
// Only one checkbox since there's only one library and no master checkbox for single library
|
||||
expect(checkboxes[0].checked).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle libraries with missing defaultNewUsers property', () => {
|
||||
setupMockLibraries([
|
||||
{ id: '1', name: 'Library 1', defaultNewUsers: true },
|
||||
{ id: '2', name: 'Library 2' }, // Missing defaultNewUsers property
|
||||
{ id: '3', name: 'Library 3', defaultNewUsers: false },
|
||||
])
|
||||
|
||||
render(
|
||||
<SelectLibraryInput
|
||||
onChange={mockOnChange}
|
||||
value={[]}
|
||||
isNewUser={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['1'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@
|
|||
},
|
||||
"helperTexts": {
|
||||
"name": "Changes to your name will only be reflected on next login",
|
||||
"libraries": "Select which libraries this user can access"
|
||||
"libraries": "Select specific libraries for this user, or leave empty to use default libraries"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "User created",
|
||||
|
|
@ -296,6 +296,7 @@
|
|||
"totalMissingFiles": "Missing Files",
|
||||
"totalSize": "Total Size",
|
||||
"totalDuration": "Duration",
|
||||
"defaultNewUsers": "Default for New Users",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Updated"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
Create,
|
||||
SimpleForm,
|
||||
TextInput,
|
||||
BooleanInput,
|
||||
required,
|
||||
useTranslate,
|
||||
useMutation,
|
||||
|
|
@ -74,6 +75,7 @@ const LibraryCreate = (props) => {
|
|||
<SimpleForm save={save} variant={'outlined'}>
|
||||
<TextInput source="name" validate={[required()]} />
|
||||
<TextInput source="path" validate={[required()]} fullWidth />
|
||||
<BooleanInput source="defaultNewUsers" />
|
||||
</SimpleForm>
|
||||
</Create>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
Edit,
|
||||
FormWithRedirect,
|
||||
TextInput,
|
||||
BooleanInput,
|
||||
required,
|
||||
SaveButton,
|
||||
DateField,
|
||||
|
|
@ -98,16 +99,25 @@ const LibraryEdit = (props) => {
|
|||
|
||||
<TextInput
|
||||
source="name"
|
||||
label={translate('resources.library.fields.name')}
|
||||
validate={[required()]}
|
||||
variant="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
source="path"
|
||||
label={translate('resources.library.fields.path')}
|
||||
validate={[required()]}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
InputProps={{ readOnly: !canEditPath }} // Disable editing path for library 1
|
||||
/>
|
||||
<BooleanInput
|
||||
source="defaultNewUsers"
|
||||
label={translate(
|
||||
'resources.library.fields.defaultNewUsers',
|
||||
)}
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<Box mt="2em" />
|
||||
|
||||
|
|
@ -122,6 +132,7 @@ const LibraryEdit = (props) => {
|
|||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalSongs'}
|
||||
label={translate('resources.library.fields.totalSongs')}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
|
|
@ -131,6 +142,9 @@ const LibraryEdit = (props) => {
|
|||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalAlbums'}
|
||||
label={translate(
|
||||
'resources.library.fields.totalAlbums',
|
||||
)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
|
|
@ -143,6 +157,9 @@ const LibraryEdit = (props) => {
|
|||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalArtists'}
|
||||
label={translate(
|
||||
'resources.library.fields.totalArtists',
|
||||
)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
|
|
@ -152,6 +169,7 @@ const LibraryEdit = (props) => {
|
|||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalSize'}
|
||||
label={translate('resources.library.fields.totalSize')}
|
||||
format={formatBytes}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
|
|
@ -165,6 +183,9 @@ const LibraryEdit = (props) => {
|
|||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalDuration'}
|
||||
label={translate(
|
||||
'resources.library.fields.totalDuration',
|
||||
)}
|
||||
format={formatDuration2}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
|
|
@ -175,6 +196,9 @@ const LibraryEdit = (props) => {
|
|||
InputProps={{ readOnly: true }}
|
||||
resource={'library'}
|
||||
source={'totalMissingFiles'}
|
||||
label={translate(
|
||||
'resources.library.fields.totalMissingFiles',
|
||||
)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
SimpleList,
|
||||
TextField,
|
||||
NumberField,
|
||||
BooleanField,
|
||||
} from 'react-admin'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
import { List, DateField, useResourceRefresh } from '../common'
|
||||
|
|
@ -35,9 +36,9 @@ const LibraryList = (props) => {
|
|||
/>
|
||||
) : (
|
||||
<Datagrid rowClick="edit">
|
||||
<TextField source="id" />
|
||||
<TextField source="name" />
|
||||
<TextField source="path" />
|
||||
<BooleanField source="defaultNewUsers" />
|
||||
<NumberField source="totalSongs" label="Songs" />
|
||||
<NumberField source="totalAlbums" label="Albums" />
|
||||
<NumberField source="totalMissingFiles" label="Missing Files" />
|
||||
|
|
|
|||
|
|
@ -27,13 +27,20 @@ export const LibrarySelectionField = () => {
|
|||
return []
|
||||
}, [value, record])
|
||||
|
||||
// Determine if this is a new user (no ID means new record)
|
||||
const isNewUser = !record?.id
|
||||
|
||||
return (
|
||||
<FormControl error={!!(touched && error)} fullWidth margin="normal">
|
||||
<FormLabel component="legend" required>
|
||||
<FormLabel component="legend">
|
||||
{translate('resources.user.fields.libraries')}
|
||||
</FormLabel>
|
||||
<Box mt={1} mb={1}>
|
||||
<SelectLibraryInput onChange={onChange} value={libraryIds} />
|
||||
<SelectLibraryInput
|
||||
onChange={onChange}
|
||||
value={libraryIds}
|
||||
isNewUser={isNewUser}
|
||||
/>
|
||||
</Box>
|
||||
{touched && error && (
|
||||
<Typography color="error" variant="caption">
|
||||
|
|
|
|||
|
|
@ -54,15 +54,8 @@ const UserCreate = (props) => {
|
|||
// 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',
|
||||
)
|
||||
}
|
||||
// Library selection is optional for non-admin users since they will be auto-assigned to default libraries
|
||||
// No validation required for library selection
|
||||
return errors
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue