mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 14:39:06 +00:00
refactor: add confirmation dialogs for destructive actions
This commit is contained in:
committed by
Carlos Valente
parent
3838bdc54d
commit
2d9909577e
@@ -1,5 +1,10 @@
|
|||||||
import { CustomViewSummary } from 'ontime-types';
|
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 * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import CustomViewsListItem from './CustomViewsListItem';
|
import CustomViewsListItem from './CustomViewsListItem';
|
||||||
|
|
||||||
@@ -13,21 +18,71 @@ interface CustomViewsListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomViewsList({ views, onOpenUpload, onMutate, onError }: 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 (
|
return (
|
||||||
<Panel.Table>
|
<>
|
||||||
<thead>
|
<Panel.Table>
|
||||||
<tr>
|
<thead>
|
||||||
<th>Name</th>
|
<tr>
|
||||||
<th>URL</th>
|
<th>Name</th>
|
||||||
<th className={style.actionsHeader} />
|
<th>URL</th>
|
||||||
</tr>
|
<th className={style.actionsHeader} />
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{views.length === 0 && <Panel.TableEmpty handleClick={onOpenUpload} label='No custom views yet' />}
|
<tbody>
|
||||||
{views.map((view, index) => (
|
{views.length === 0 && <Panel.TableEmpty handleClick={onOpenUpload} label='No custom views yet' />}
|
||||||
<CustomViewsListItem key={view.slug} slug={view.slug} index={index} onMutate={onMutate} onError={onError} />
|
{views.map((view, index) => (
|
||||||
))}
|
<CustomViewsListItem
|
||||||
</tbody>
|
key={view.slug}
|
||||||
</Panel.Table>
|
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 { 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 { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||||
import { handleLinks } from '../../../../common/utils/linkUtils';
|
import { handleLinks } from '../../../../common/utils/linkUtils';
|
||||||
@@ -12,11 +12,11 @@ import style from './CustomViews.module.scss';
|
|||||||
interface CustomViewsListItemProps {
|
interface CustomViewsListItemProps {
|
||||||
slug: string;
|
slug: string;
|
||||||
index: number;
|
index: number;
|
||||||
onMutate: () => void;
|
onDelete: (slug: string) => void;
|
||||||
onError: (message: 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 = () => {
|
const handlePreview = () => {
|
||||||
handleLinks(`external/${encodeURIComponent(slug)}/`);
|
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 (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{slug}</td>
|
<td>{slug}</td>
|
||||||
@@ -62,7 +53,7 @@ export default function CustomViewsListItem({ slug, index, onMutate, onError }:
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='ghosted-destructive'
|
variant='ghosted-destructive'
|
||||||
onClick={handleDelete}
|
onClick={() => onDelete(slug)}
|
||||||
aria-label='Delete custom view'
|
aria-label='Delete custom view'
|
||||||
data-testid={`custom-view__delete_${index}`}
|
data-testid={`custom-view__delete_${index}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import {
|
|||||||
renameProject,
|
renameProject,
|
||||||
} from '../../../../common/api/db';
|
} from '../../../../common/api/db';
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||||
|
import Dialog from '../../../../common/components/dialog/Dialog';
|
||||||
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -50,6 +52,7 @@ export default function ProjectListItem({
|
|||||||
}: ProjectListItemProps) {
|
}: ProjectListItemProps) {
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isDeleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
|
||||||
const handleSubmitAction = (actionType: 'rename' | 'duplicate') => {
|
const handleSubmitAction = (actionType: 'rename' | 'duplicate') => {
|
||||||
return async (values: ProjectFormValues) => {
|
return async (values: ProjectFormValues) => {
|
||||||
@@ -99,6 +102,11 @@ export default function ProjectListItem({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitDelete = async () => {
|
||||||
|
await handleDelete(filename);
|
||||||
|
setDeleteOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleEditMode = (editMode: EditMode, filename: string | null) => {
|
const handleToggleEditMode = (editMode: EditMode, filename: string | null) => {
|
||||||
setSubmitError(null);
|
setSubmitError(null);
|
||||||
onToggleEditMode(editMode, filename);
|
onToggleEditMode(editMode, filename);
|
||||||
@@ -141,7 +149,7 @@ export default function ProjectListItem({
|
|||||||
current={current}
|
current={current}
|
||||||
filename={filename}
|
filename={filename}
|
||||||
onChangeEditMode={handleToggleEditMode}
|
onChangeEditMode={handleToggleEditMode}
|
||||||
onDelete={handleDelete}
|
onDelete={() => setDeleteOpen(true)}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
isDisabled={loading || showMergeForm}
|
isDisabled={loading || showMergeForm}
|
||||||
onMerge={(filename) => handleToggleEditMode('merge', filename)}
|
onMerge={(filename) => handleToggleEditMode('merge', filename)}
|
||||||
@@ -157,6 +165,24 @@ export default function ProjectListItem({
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
<Dialog
|
||||||
|
isOpen={isDeleteOpen}
|
||||||
|
onClose={() => setDeleteOpen(false)}
|
||||||
|
title='Delete project'
|
||||||
|
showBackdrop
|
||||||
|
showCloseButton
|
||||||
|
bodyElements='You will permanently remove this project file. Are you sure?'
|
||||||
|
footerElements={
|
||||||
|
<>
|
||||||
|
<Button size='large' onClick={() => setDeleteOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant='destructive' size='large' onClick={submitDelete} loading={loading}>
|
||||||
|
Delete project
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -166,7 +192,7 @@ interface ActionMenuProps {
|
|||||||
filename: string;
|
filename: string;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
onChangeEditMode: (editMode: EditMode, filename: string) => void;
|
onChangeEditMode: (editMode: EditMode, filename: string) => void;
|
||||||
onDelete: (filename: string) => Promise<void>;
|
onDelete: () => void;
|
||||||
onLoad: (filename: string) => Promise<void>;
|
onLoad: (filename: string) => Promise<void>;
|
||||||
onMerge: (filename: string) => void;
|
onMerge: (filename: string) => void;
|
||||||
}
|
}
|
||||||
@@ -208,7 +234,7 @@ function ActionMenu(props: ActionMenuProps) {
|
|||||||
{ type: 'item', icon: IoCopyOutline, label: 'Duplicate', onClick: handleDuplicate },
|
{ type: 'item', icon: IoCopyOutline, label: 'Duplicate', onClick: handleDuplicate },
|
||||||
{ type: 'item', icon: IoDocumentOutline, label: 'Download', onClick: handleDownload },
|
{ type: 'item', icon: IoDocumentOutline, label: 'Download', onClick: handleDownload },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ type: 'item', icon: IoTrash, label: 'Delete', onClick: () => onDelete(filename), disabled: current },
|
{ type: 'item', icon: IoTrash, label: 'Delete', onClick: onDelete, disabled: current },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<IoEllipsisHorizontal />
|
<IoEllipsisHorizontal />
|
||||||
|
|||||||
Reference in New Issue
Block a user