fix(ui): don't crash the playlist list when rows lose their record (#6250)

* fix(ui): don't crash playlist list rows that lost their record

react-admin 3 evicts records fetched more than 10 minutes ago whenever
another getList for the same resource completes, but the list keeps its
cached ids. The Datagrid then renders those rows with an undefined record,
and the Public and Auto-import switches crashed reading record.id. This
happened when the playlist list was left open and the sidebar or the add
to playlist dialog reloaded a smaller set of playlists.

Both switches now render nothing when the row has no record; the next list
refresh fills the row in again.

* refactor(ui): merge playlist list toggles into one ToggleField

The Public and Auto-import switches were copies that differed only in the
field they flip. ToggleField now flips its source field, and
ToggleAutoImport just shows it for playlists that have a file path. The
tests render inside TestContext, so they use react-admin's real hooks
instead of mocks.
This commit is contained in:
Deluan Quintão 2026-09-29 13:23:36 -04:00 • committed by GitHub
commit 0e1893530b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 34 additions and 37 deletions

View file

@ -67,15 +67,15 @@ const PlaylistFilter = (props) => {
)
}
const TogglePublicInput = ({ resource, source }) => {
export const ToggleField = ({ resource, source }) => {
const record = useRecordContext()
const notify = useNotify()
const [togglePublic] = useUpdate(
const [toggle] = useUpdate(
resource,
record.id,
record?.id,
{
...record,
public: !record.public,
[source]: !record?.[source],
},
{
undoable: false,
@ -86,10 +86,12 @@ const TogglePublicInput = ({ resource, source }) => {
)
const handleClick = (e) => {
togglePublic()
toggle()
e.stopPropagation()
}
if (!record) return null
return (
<Switch
checked={record[source]}
@ -99,35 +101,9 @@ const TogglePublicInput = ({ resource, source }) => {
)
}
const ToggleAutoImport = ({ resource, source }) => {
export const ToggleAutoImport = (props) => {
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
return record?.path ? <ToggleField {...props} /> : null
}
const PlaylistListBulkActions = (props) => {
@ -169,9 +145,7 @@ const PlaylistList = (props) => {
updatedAt: isDesktop && (
<DateField source="updatedAt" sortByOrder={'DESC'} />
),
public: !isXsmall && (
<TogglePublicInput source="public" sortByOrder={'DESC'} />
),
public: !isXsmall && <ToggleField source="public" sortByOrder={'DESC'} />,
comment: <TextField source="comment" />,
sync: !isXsmall && (
<ToggleAutoImport source="sync" sortByOrder={'DESC'} />

View file

@ -1,7 +1,8 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { PlaylistLove } from './PlaylistList'
import { TestContext } from 'ra-test'
import { PlaylistLove, ToggleField, ToggleAutoImport } from './PlaylistList'
vi.mock('../config', () => ({
default: { enableFavourites: true },
@ -32,3 +33,25 @@ describe('<PlaylistLove />', () => {
})
})
})
// react-admin evicts records older than 10 minutes while the list still holds
// their ids, so rows can render with no record.
describe('playlist toggles without a record', () => {
it('<ToggleField /> renders nothing', () => {
const { container } = render(
<TestContext>
<ToggleField resource="playlist" source="public" />
</TestContext>,
)
expect(container.innerHTML).toBe('')
})
it('<ToggleAutoImport /> renders nothing', () => {
const { container } = render(
<TestContext>
<ToggleAutoImport resource="playlist" source="sync" />
</TestContext>,
)
expect(container.innerHTML).toBe('')
})
})