mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 10:27:08 +02:00
228 lines
6 KiB
React
228 lines
6 KiB
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'
|
||
|
|
import {
|
||
|
|
List,
|
||
|
|
ListItem,
|
||
|
|
ListItemIcon,
|
||
|
|
ListItemText,
|
||
|
|
Typography,
|
||
|
|
Box,
|
||
|
|
} from '@material-ui/core'
|
||
|
|
import { useGetList, useTranslate } from 'react-admin'
|
||
|
|
import PropTypes from 'prop-types'
|
||
|
|
import { makeStyles } from '@material-ui/core'
|
||
|
|
|
||
|
|
const useStyles = makeStyles((theme) => ({
|
||
|
|
root: {
|
||
|
|
width: '960px',
|
||
|
|
maxWidth: '100%',
|
||
|
|
},
|
||
|
|
headerContainer: {
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
marginBottom: theme.spacing(1),
|
||
|
|
paddingLeft: theme.spacing(1),
|
||
|
|
},
|
||
|
|
masterCheckbox: {
|
||
|
|
padding: '7px',
|
||
|
|
marginLeft: '-9px',
|
||
|
|
marginRight: theme.spacing(1),
|
||
|
|
},
|
||
|
|
libraryList: {
|
||
|
|
height: '120px',
|
||
|
|
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 = [],
|
||
|
|
isNewUser = false,
|
||
|
|
}) => {
|
||
|
|
const classes = useStyles()
|
||
|
|
const translate = useTranslate()
|
||
|
|
const [selectedLibraryIds, setSelectedLibraryIds] = useState([])
|
||
|
|
const [hasInitialized, setHasInitialized] = useState(false)
|
||
|
|
|
||
|
|
const { ids, data, isLoading } = useGetList(
|
||
|
|
'library',
|
||
|
|
{ page: 1, perPage: -1 },
|
||
|
|
{ field: 'name', order: 'ASC' },
|
||
|
|
)
|
||
|
|
|
||
|
|
const options = useMemo(
|
||
|
|
() => (ids && ids.map((id) => data[id])) || [],
|
||
|
|
[ids, data],
|
||
|
|
)
|
||
|
|
|
||
|
|
// 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, isNewUser, hasInitialized])
|
||
|
|
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleMasterCheckboxChange = () => {
|
||
|
|
const isAllSelected = selectedLibraryIds.length === options.length
|
||
|
|
const newSelection = isAllSelected ? [] : options.map((lib) => lib.id)
|
||
|
|
|
||
|
|
setSelectedLibraryIds(newSelection)
|
||
|
|
onChange(newSelection)
|
||
|
|
}
|
||
|
|
|
||
|
|
const selectedCount = selectedLibraryIds.length
|
||
|
|
const totalCount = options.length
|
||
|
|
const isAllSelected = selectedCount === totalCount && totalCount > 0
|
||
|
|
const isIndeterminate = selectedCount > 0 && selectedCount < totalCount
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={classes.root}>
|
||
|
|
{options.length > 1 && (
|
||
|
|
<Box className={classes.headerContainer}>
|
||
|
|
<Checkbox
|
||
|
|
checked={isAllSelected}
|
||
|
|
indeterminate={isIndeterminate}
|
||
|
|
onChange={handleMasterCheckboxChange}
|
||
|
|
size="small"
|
||
|
|
className={classes.masterCheckbox}
|
||
|
|
/>
|
||
|
|
<Typography variant="body2">
|
||
|
|
{translate('resources.user.message.selectAllLibraries')}
|
||
|
|
</Typography>
|
||
|
|
</Box>
|
||
|
|
)}
|
||
|
|
<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,
|
||
|
|
isNewUser: PropTypes.bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
LibraryListItem.propTypes = {
|
||
|
|
library: PropTypes.object.isRequired,
|
||
|
|
isSelected: PropTypes.bool.isRequired,
|
||
|
|
onToggle: PropTypes.func.isRequired,
|
||
|
|
}
|