feat(ui): add remove all missing files functionality (#4108)

* Add remove all missing files feature

* test: update mediafile_repository tests for missing files deletion

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2025-05-22 22:28:10 -04:00
committed by GitHub
parent 992c78376c
commit 3ccc02f375
8 changed files with 119 additions and 17 deletions
+3 -3
View File
@@ -36,9 +36,9 @@ const mapResource = (resource, params) => {
}
const callDeleteMany = (resource, params) => {
const ids = params.ids.map((id) => `id=${id}`)
const idsParam = ids.join('&')
return httpClient(`${REST_URL}/${resource}?${idsParam}`, {
const ids = (params.ids || []).map((id) => `id=${id}`)
const query = ids.length > 0 ? `?${ids.join('&')}` : ''
return httpClient(`${REST_URL}/${resource}${query}`, {
method: 'DELETE',
}).then((response) => ({ data: response.json.ids || [] }))
}
+4 -1
View File
@@ -241,7 +241,8 @@
"updatedAt": "Disappeared on"
},
"actions": {
"remove": "Remove"
"remove": "Remove",
"remove_all": "Remove All"
},
"notifications": {
"removed": "Missing file(s) removed"
@@ -403,6 +404,8 @@
"delete_user_content": "Are you sure you want to delete this user and all their data (including playlists and preferences)?",
"remove_missing_title": "Remove missing files",
"remove_missing_content": "Are you sure you want to remove the selected missing files from the database? This will remove permanently any references to them, including their play counts and ratings.",
"remove_all_missing_title": "Remove all missing files",
"remove_all_missing_content": "Are you sure you want to remove all missing files from the database? This will permanently remove any references to them, including their play counts and ratings.",
"notifications_blocked": "You have blocked Notifications for this site in your browser's settings",
"notifications_not_available": "This browser does not support desktop notifications or you are not accessing Navidrome over https",
"lastfmLinkSuccess": "Last.fm successfully linked and scrobbling enabled",
+18 -5
View File
@@ -29,13 +29,14 @@ const useStyles = makeStyles(
)
const DeleteMissingFilesButton = (props) => {
const { selectedIds, className } = props
const { selectedIds, className, deleteAll = false } = props
const [open, setOpen] = useState(false)
const unselectAll = useUnselectAll()
const refresh = useRefresh()
const notify = useNotify()
const [deleteMany, { loading }] = useDeleteMany('missing', selectedIds, {
const ids = deleteAll ? [] : selectedIds
const [deleteMany, { loading }] = useDeleteMany('missing', ids, {
onSuccess: () => {
notify('resources.missing.notifications.removed')
refresh()
@@ -57,7 +58,11 @@ const DeleteMissingFilesButton = (props) => {
<>
<Button
onClick={handleClick}
label="ra.action.remove"
label={
deleteAll
? 'resources.missing.actions.remove_all'
: 'ra.action.remove'
}
key="button"
className={clsx('ra-delete-button', classes.deleteButton, className)}
>
@@ -66,8 +71,16 @@ const DeleteMissingFilesButton = (props) => {
<Confirm
isOpen={open}
loading={loading}
title="message.remove_missing_title"
content="message.remove_missing_content"
title={
deleteAll
? 'message.remove_all_missing_title'
: 'message.remove_missing_title'
}
content={
deleteAll
? 'message.remove_all_missing_content'
: 'message.remove_missing_content'
}
onConfirm={handleConfirm}
onClose={handleDialogClose}
/>
@@ -0,0 +1,42 @@
import React from 'react'
import { render } from '@testing-library/react'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import DeleteMissingFilesButton from './DeleteMissingFilesButton.jsx'
import * as RA from 'react-admin'
vi.mock('react-admin', async () => {
const actual = await vi.importActual('react-admin')
return {
...actual,
Button: ({ children, onClick, label }) => (
<button onClick={onClick}>{label || children}</button>
),
Confirm: ({ isOpen }) => (isOpen ? <div data-testid="confirm" /> : null),
useNotify: vi.fn(),
useDeleteMany: vi.fn(() => [vi.fn(), { loading: false }]),
useRefresh: vi.fn(),
useUnselectAll: vi.fn(),
}
})
describe('DeleteMissingFilesButton', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('uses remove_all label when deleteAll is true', () => {
const { getByRole } = render(<DeleteMissingFilesButton deleteAll />)
expect(getByRole('button').textContent).toBe(
'resources.missing.actions.remove_all',
)
})
it('calls useDeleteMany with empty ids when deleteAll is true', () => {
render(<DeleteMissingFilesButton deleteAll />)
expect(RA.useDeleteMany).toHaveBeenCalledWith(
'missing',
[],
expect.any(Object),
)
})
})
+2
View File
@@ -8,6 +8,7 @@ import {
} from 'react-admin'
import jsonExport from 'jsonexport/dist'
import DeleteMissingFilesButton from './DeleteMissingFilesButton.jsx'
import MissingListActions from './MissingListActions.jsx'
const exporter = (files) => {
const filesToExport = files.map((file) => {
@@ -35,6 +36,7 @@ const MissingFilesList = (props) => {
{...props}
sort={{ field: 'updated_at', order: 'DESC' }}
exporter={exporter}
actions={<MissingListActions />}
bulkActionButtons={<BulkActionButtons />}
perPage={50}
pagination={<MissingPagination />}
+12
View File
@@ -0,0 +1,12 @@
import React from 'react'
import { TopToolbar, ExportButton } from 'react-admin'
import DeleteMissingFilesButton from './DeleteMissingFilesButton.jsx'
const MissingListActions = (props) => (
<TopToolbar {...props}>
<ExportButton />
<DeleteMissingFilesButton deleteAll />
</TopToolbar>
)
export default MissingListActions