Files
navidrome/ui/src/playlist/PlaylistList.jsx
T
Deluan Quintão 5c3568f758 fix(ui): make playlist name sorting case-insensitive (#4845)
* 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>
2026-01-05 19:05:11 -05:00

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