refactor: add confirmation dialogs for destructive actions

This commit is contained in:
Carlos Valente
2026-03-25 21:34:22 +01:00
committed by Carlos Valente
parent 3838bdc54d
commit 2d9909577e
3 changed files with 103 additions and 31 deletions
@@ -1,5 +1,10 @@
import { CustomViewSummary } from 'ontime-types';
import { useState } from 'react';
import { deleteCustomView } from '../../../../common/api/customViews';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import Dialog from '../../../../common/components/dialog/Dialog';
import * as Panel from '../../panel-utils/PanelUtils';
import CustomViewsListItem from './CustomViewsListItem';
@@ -13,21 +18,71 @@ interface CustomViewsListProps {
}
export default function CustomViewsList({ views, onOpenUpload, onMutate, onError }: CustomViewsListProps) {
const [targetSlug, setTargetSlug] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const openDelete = (slug: string) => {
setTargetSlug(slug);
};
const submitDelete = async () => {
if (!targetSlug) {
return;
}
try {
setIsDeleting(true);
await deleteCustomView(targetSlug);
onMutate();
} catch (error) {
onError(maybeAxiosError(error));
} finally {
setIsDeleting(false);
setTargetSlug(null);
}
};
return (
<Panel.Table>
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th className={style.actionsHeader} />
</tr>
</thead>
<tbody>
{views.length === 0 && <Panel.TableEmpty handleClick={onOpenUpload} label='No custom views yet' />}
{views.map((view, index) => (
<CustomViewsListItem key={view.slug} slug={view.slug} index={index} onMutate={onMutate} onError={onError} />
))}
</tbody>
</Panel.Table>
<>
<Panel.Table>
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th className={style.actionsHeader} />
</tr>
</thead>
<tbody>
{views.length === 0 && <Panel.TableEmpty handleClick={onOpenUpload} label='No custom views yet' />}
{views.map((view, index) => (
<CustomViewsListItem
key={view.slug}
slug={view.slug}
index={index}
onDelete={openDelete}
onError={onError}
/>
))}
</tbody>
</Panel.Table>
<Dialog
isOpen={targetSlug !== null}
onClose={() => setTargetSlug(null)}
title='Delete custom view'
showBackdrop
showCloseButton
bodyElements='You will permanently delete this file. Are you sure?'
footerElements={
<>
<Button size='large' onClick={() => setTargetSlug(null)}>
Cancel
</Button>
<Button variant='destructive' size='large' onClick={submitDelete} loading={isDeleting}>
Delete custom view
</Button>
</>
}
/>
</>
);
}
@@ -1,6 +1,6 @@
import { IoDownloadOutline, IoOpenOutline, IoTrash } from 'react-icons/io5';
import { deleteCustomView, downloadCustomView } from '../../../../common/api/customViews';
import { downloadCustomView } from '../../../../common/api/customViews';
import { maybeAxiosError } from '../../../../common/api/utils';
import IconButton from '../../../../common/components/buttons/IconButton';
import { handleLinks } from '../../../../common/utils/linkUtils';
@@ -12,11 +12,11 @@ import style from './CustomViews.module.scss';
interface CustomViewsListItemProps {
slug: string;
index: number;
onMutate: () => void;
onDelete: (slug: string) => void;
onError: (message: string) => void;
}
export default function CustomViewsListItem({ slug, index, onMutate, onError }: CustomViewsListItemProps) {
export default function CustomViewsListItem({ slug, index, onDelete, onError }: CustomViewsListItemProps) {
const handlePreview = () => {
handleLinks(`external/${encodeURIComponent(slug)}/`);
};
@@ -29,15 +29,6 @@ export default function CustomViewsListItem({ slug, index, onMutate, onError }:
}
};
const handleDelete = async () => {
try {
await deleteCustomView(slug);
onMutate();
} catch (error) {
onError(maybeAxiosError(error));
}
};
return (
<tr>
<td>{slug}</td>
@@ -62,7 +53,7 @@ export default function CustomViewsListItem({ slug, index, onMutate, onError }:
</IconButton>
<IconButton
variant='ghosted-destructive'
onClick={handleDelete}
onClick={() => onDelete(slug)}
aria-label='Delete custom view'
data-testid={`custom-view__delete_${index}`}
>