Enhance show deletion functionality to support media removal option
Updated the DeleteShowCommand and its handler to include an optional parameter for media deletion, allowing users to remove associated media files when deleting a show. Refactored related components and API calls to accommodate this new feature, ensuring a seamless user experience. Additionally, updated localization strings to reflect the new functionality and adjusted tests to verify the correct behavior of the deletion process with and without media.
This commit is contained in:
@@ -55,7 +55,11 @@ export function InterstitialsPanel() {
|
||||
},
|
||||
onError,
|
||||
})
|
||||
const deleteClipMutation = useMutation({ mutationFn: deleteShow, onSuccess: invalidate, onError })
|
||||
const deleteClipMutation = useMutation({
|
||||
mutationFn: (id: string) => deleteShow(id),
|
||||
onSuccess: invalidate,
|
||||
onError,
|
||||
})
|
||||
const deleteBlockMutation = useMutation({
|
||||
mutationFn: deleteCollection,
|
||||
onSuccess: invalidate,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ShowSummaryDto } from '@/shared/api/types'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/shared/ui/dialog'
|
||||
|
||||
/**
|
||||
* Подтверждение удаления шоу с выбором, что делать с его файлами.
|
||||
*
|
||||
* Галочка нужна потому, что медиа переживает шоу: серия ссылается на ассет по значению, внешнего
|
||||
* ключа между ними нет, и после удаления файлы остались бы в библиотеке ничьими, занимая место.
|
||||
* Спросить об этом здесь дешевле, чем потом искать осиротевшие гигабайты в списке медиа.
|
||||
*/
|
||||
export function DeleteShowDialog({
|
||||
show,
|
||||
pending,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: Readonly<{
|
||||
show: ShowSummaryDto
|
||||
pending: boolean
|
||||
onConfirm: (withMedia: boolean) => void
|
||||
onClose: () => void
|
||||
}>) {
|
||||
const { t } = useTranslation()
|
||||
const [withMedia, setWithMedia] = useState(false)
|
||||
const hasMedia = show.episodeCount > 0
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admin.shows.deleteTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('admin.shows.deleteHint', { name: show.name })}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{hasMedia && (
|
||||
<label className="flex items-start gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1"
|
||||
checked={withMedia}
|
||||
onChange={(e) => setWithMedia(e.target.checked)}
|
||||
/>
|
||||
<span>
|
||||
{t('admin.shows.deleteWithMedia', { count: show.episodeCount })}
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
{t('admin.shows.deleteWithMediaHint')}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button size="sm" variant="outline" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={pending}
|
||||
onClick={() => onConfirm(withMedia)}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SHOW_AUDIENCES,
|
||||
type ShowAudience,
|
||||
type ShowKind,
|
||||
type ShowSummaryDto,
|
||||
} from '@/shared/api/types'
|
||||
import { useApiError } from '@/shared/lib/use-api-error'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
@@ -19,6 +20,7 @@ import { sortRows, useTableSort } from '@/shared/lib/table-sort'
|
||||
import { SortHeader } from '@/shared/ui/sortable'
|
||||
import { listGenres } from '@/features/admin/genres/api'
|
||||
import { createShow, deleteShow, listShows } from './api'
|
||||
import { DeleteShowDialog } from './DeleteShowDialog'
|
||||
import { BulkTagBar } from './BulkTagBar'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
@@ -110,7 +112,17 @@ export function ShowsPanel() {
|
||||
},
|
||||
onError,
|
||||
})
|
||||
const deleteMutation = useMutation({ mutationFn: deleteShow, onSuccess: invalidate, onError })
|
||||
// Шоу к удалению: сначала спрашиваем про файлы, и только потом удаляем.
|
||||
const [toDelete, setToDelete] = useState<ShowSummaryDto | null>(null)
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: ({ id, withMedia }: { id: string; withMedia: boolean }) =>
|
||||
deleteShow(id, withMedia),
|
||||
onSuccess: () => {
|
||||
setToDelete(null)
|
||||
invalidate()
|
||||
},
|
||||
onError,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -309,11 +321,7 @@ export function ShowsPanel() {
|
||||
<td className="px-4 py-2 text-muted-foreground">{show.seasonCount}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{show.episodeCount}</td>
|
||||
<td className="px-4 py-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => deleteMutation.mutate(show.id)}
|
||||
>
|
||||
<Button size="sm" variant="destructive" onClick={() => setToDelete(show)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</td>
|
||||
@@ -324,6 +332,15 @@ export function ShowsPanel() {
|
||||
</div>
|
||||
|
||||
<Pager page={page} totalPages={totalPages} onChange={setPage} />
|
||||
|
||||
{toDelete && (
|
||||
<DeleteShowDialog
|
||||
show={toDelete}
|
||||
pending={deleteMutation.isPending}
|
||||
onConfirm={(withMedia) => deleteMutation.mutate({ id: toDelete.id, withMedia })}
|
||||
onClose={() => setToDelete(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,8 +79,10 @@ export function getShowUsage(id: string) {
|
||||
return apiRequest<ShowUsageDto>(`/admin/shows/${id}/usage`)
|
||||
}
|
||||
|
||||
export function deleteShow(id: string) {
|
||||
return apiRequest<void>(`/admin/shows/${id}`, { method: 'DELETE' })
|
||||
/** `withMedia` — снести заодно файлы шоу: иначе они переживут его осиротевшими. */
|
||||
export function deleteShow(id: string, withMedia = false) {
|
||||
const query = withMedia ? '?withMedia=true' : ''
|
||||
return apiRequest<void>(`/admin/shows/${id}${query}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export function addEpisode(showId: string, mediaAssetId: string) {
|
||||
|
||||
@@ -412,6 +412,11 @@ export const en = {
|
||||
genre: 'Genre',
|
||||
allGenres: 'All genres',
|
||||
allKinds: 'All kinds',
|
||||
deleteTitle: 'Delete the show?',
|
||||
deleteHint: '“{{name}}” will be removed from the library. This cannot be undone.',
|
||||
deleteWithMedia: 'Delete its media too: {{count}} file(s)',
|
||||
deleteWithMediaHint:
|
||||
'Otherwise the files stay in the media list with no owner and keep taking space.',
|
||||
otherGenres: 'Other genres',
|
||||
bulk: {
|
||||
selected: 'Selected: {{count}}',
|
||||
|
||||
@@ -409,6 +409,11 @@ export const ru = {
|
||||
genre: 'Жанр',
|
||||
allGenres: 'Все жанры',
|
||||
allKinds: 'Все типы',
|
||||
deleteTitle: 'Удалить шоу?',
|
||||
deleteHint: '«{{name}}» будет удалено из библиотеки. Отменить это нельзя.',
|
||||
deleteWithMedia: 'Также удалить медиа: файлов {{count}}',
|
||||
deleteWithMediaHint:
|
||||
'Иначе файлы останутся в списке медиа ничьими и продолжат занимать место.',
|
||||
otherGenres: 'Остальные жанры',
|
||||
bulk: {
|
||||
selected: 'Отмечено: {{count}}',
|
||||
|
||||
Reference in New Issue
Block a user