5c3568f758
* fix: make playlist name sorting case-insensitive Add collation NOCASE to playlist.name column to ensure case-insensitive sorting, matching the behavior of other tables like radio and user. This fixes the issue where uppercase playlist names would appear before lowercase names regardless of alphabetical order. The migration recreates the playlist table with the proper collation and recreates all associated indexes. Corresponding collation tests are added to verify the fix persists through future migrations. * fix: add default sorting to playlist names Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
190 lines
4.3 KiB
React
190 lines
4.3 KiB
React
import React, { useMemo } from 'react'
|
|
import {
|
|
Datagrid,
|
|
DateField,
|
|
EditButton,
|
|
Filter,
|
|
NumberField,
|
|
ReferenceInput,
|
|
SearchInput,
|
|
SelectInput,
|
|
TextField,
|
|
useUpdate,
|
|
useNotify,
|
|
useRecordContext,
|
|
BulkDeleteButton,
|
|
usePermissions,
|
|
} from 'react-admin'
|
|
import Switch from '@material-ui/core/Switch'
|
|
import { makeStyles } from '@material-ui/core/styles'
|
|
import { useMediaQuery } from '@material-ui/core'
|
|
import {
|
|
DurationField,
|
|
List,
|
|
Writable,
|
|
isWritable,
|
|
useSelectedFields,
|
|
useResourceRefresh,
|
|
} from '../common'
|
|
import PlaylistListActions from './PlaylistListActions'
|
|
import ChangePublicStatusButton from './ChangePublicStatusButton'
|
|
|
|
const useStyles = makeStyles((theme) => ({
|
|
button: {
|
|
color: theme.palette.type === 'dark' ? 'white' : undefined,
|
|
},
|
|
}))
|
|
|
|
const PlaylistFilter = (props) => {
|
|
const { permissions } = usePermissions()
|
|
return (
|
|
<Filter {...props} variant={'outlined'}>
|
|
<SearchInput source="q" alwaysOn />
|
|
{permissions === 'admin' && (
|
|
<ReferenceInput
|
|
source="owner_id"
|
|
label={'resources.playlist.fields.ownerName'}
|
|
reference="user"
|
|
perPage={0}
|
|
sort={{ field: 'name', order: 'ASC' }}
|
|
alwaysOn
|
|
>
|
|
<SelectInput optionText="name" />
|
|
</ReferenceInput>
|
|
)}
|
|
</Filter>
|
|
)
|
|
}
|
|
|
|
const TogglePublicInput = ({ resource, source }) => {
|
|
const record = useRecordContext()
|
|
const notify = useNotify()
|
|
const [togglePublic] = useUpdate(
|
|
resource,
|
|
record.id,
|
|
{
|
|
...record,
|
|
public: !record.public,
|
|
},
|
|
{
|
|
undoable: false,
|
|
onFailure: (error) => {
|
|
notify('ra.page.error', 'warning')
|
|
},
|
|
},
|
|
)
|
|
|
|
const handleClick = (e) => {
|
|
togglePublic()
|
|
e.stopPropagation()
|
|
}
|
|
|
|
return (
|
|
<Switch
|
|
checked={record[source]}
|
|
onClick={handleClick}
|
|
disabled={!isWritable(record.ownerId)}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const ToggleAutoImport = ({ resource, source }) => {
|
|
const record = useRecordContext()
|
|
const notify = useNotify()
|
|
const [ToggleAutoImport] = useUpdate(
|
|
resource,
|
|
record.id,
|
|
{
|
|
...record,
|
|
sync: !record.sync,
|
|
},
|
|
{
|
|
undoable: false,
|
|
onFailure: (error) => {
|
|
notify('ra.page.error', 'warning')
|
|
},
|
|
},
|
|
)
|
|
const handleClick = (e) => {
|
|
ToggleAutoImport()
|
|
e.stopPropagation()
|
|
}
|
|
|
|
return record.path ? (
|
|
<Switch
|
|
checked={record[source]}
|
|
onClick={handleClick}
|
|
disabled={!isWritable(record.ownerId)}
|
|
/>
|
|
) : null
|
|
}
|
|
|
|
const PlaylistListBulkActions = (props) => {
|
|
const classes = useStyles()
|
|
return (
|
|
<>
|
|
<ChangePublicStatusButton
|
|
public={true}
|
|
{...props}
|
|
className={classes.button}
|
|
/>
|
|
<ChangePublicStatusButton
|
|
public={false}
|
|
{...props}
|
|
className={classes.button}
|
|
/>
|
|
<BulkDeleteButton {...props} className={classes.button} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
const PlaylistList = (props) => {
|
|
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
|
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
|
useResourceRefresh('playlist')
|
|
|
|
const toggleableFields = useMemo(
|
|
() => ({
|
|
ownerName: isDesktop && <TextField source="ownerName" />,
|
|
songCount: !isXsmall && <NumberField source="songCount" />,
|
|
duration: <DurationField source="duration" />,
|
|
updatedAt: isDesktop && (
|
|
<DateField source="updatedAt" sortByOrder={'DESC'} />
|
|
),
|
|
public: !isXsmall && (
|
|
<TogglePublicInput source="public" sortByOrder={'DESC'} />
|
|
),
|
|
comment: <TextField source="comment" />,
|
|
sync: <ToggleAutoImport source="sync" sortByOrder={'DESC'} />,
|
|
}),
|
|
[isDesktop, isXsmall],
|
|
)
|
|
|
|
const columns = useSelectedFields({
|
|
resource: 'playlist',
|
|
columns: toggleableFields,
|
|
defaultOff: ['comment'],
|
|
})
|
|
|
|
return (
|
|
<List
|
|
{...props}
|
|
exporter={false}
|
|
sort={{ field: 'name', order: 'ASC' }}
|
|
filters={<PlaylistFilter />}
|
|
actions={<PlaylistListActions />}
|
|
bulkActionButtons={!isXsmall && <PlaylistListBulkActions />}
|
|
>
|
|
<Datagrid rowClick="show" isRowSelectable={(r) => isWritable(r?.ownerId)}>
|
|
<TextField source="name" />
|
|
{columns}
|
|
<Writable>
|
|
<EditButton />
|
|
</Writable>
|
|
</Datagrid>
|
|
</List>
|
|
)
|
|
}
|
|
|
|
export default PlaylistList
|