feat(ui): require confirmation before delete actions

- add a reusable modal component and export it from the shared ui barrel

- gate all delete and remove flows behind explicit confirmation dialogs

- use a solid modal surface color token with fallback to avoid transparent body
This commit is contained in:
2026-04-10 23:46:20 +03:00
parent 7223371fae
commit d8ea2d0126
14 changed files with 458 additions and 41 deletions
@@ -11,6 +11,7 @@ import {
Card,
DescriptionList,
Input,
Modal,
Select,
Table,
Timestamp,
@@ -41,6 +42,10 @@ export function ScenarioDetailPage() {
const [draggedStepId, setDraggedStepId] = useState<string | null>(null);
const [dragOverStepId, setDragOverStepId] = useState<string | null>(null);
const [reorderingSteps, setReorderingSteps] = useState(false);
const [pendingDelete, setPendingDelete] = useState<
{ type: 'scenario' } | { type: 'step'; id: string } | { type: 'credential'; id: string } | null
>(null);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (!id) return;
@@ -148,6 +153,26 @@ export function ScenarioDetailPage() {
setScenarioCreds((prev) => prev.filter((sc) => sc.id !== scCredId));
};
const confirmDelete = async () => {
if (!pendingDelete) return;
setDeleting(true);
try {
if (pendingDelete.type === 'scenario') {
await handleDelete();
return;
}
if (pendingDelete.type === 'step') {
await handleDeleteStep(pendingDelete.id);
}
if (pendingDelete.type === 'credential') {
await handleRemoveCredential(pendingDelete.id);
}
setPendingDelete(null);
} finally {
setDeleting(false);
}
};
const stepColumns: TableColumn<ScenarioStep>[] = [
{
key: 'drag',
@@ -226,7 +251,7 @@ export function ScenarioDetailPage() {
title={t('steps.action_delete')}
onClick={(e) => {
e.stopPropagation();
handleDeleteStep(s.id);
setPendingDelete({ type: 'step', id: s.id });
}}
>
<Trash2 size={14} />
@@ -261,7 +286,7 @@ export function ScenarioDetailPage() {
title={t('scenarios.cred_action_remove')}
onClick={(e) => {
e.stopPropagation();
handleRemoveCredential(sc.id);
setPendingDelete({ type: 'credential', id: sc.id });
}}
>
<Trash2 size={14} />
@@ -297,7 +322,7 @@ export function ScenarioDetailPage() {
<Pencil size={14} />
{t('scenarios.action_edit')}
</Button>
<Button variant="danger" size="sm" onClick={handleDelete}>
<Button variant="danger" size="sm" onClick={() => setPendingDelete({ type: 'scenario' })}>
<Trash2 size={14} />
{t('scenarios.action_delete')}
</Button>
@@ -456,6 +481,32 @@ export function ScenarioDetailPage() {
</div>
</>
)}
<Modal
open={pendingDelete != null}
title={
pendingDelete?.type === 'step'
? t('steps.action_delete')
: pendingDelete?.type === 'credential'
? t('scenarios.cred_action_remove')
: t('scenarios.action_delete')
}
onClose={() => !deleting && setPendingDelete(null)}
footer={(
<>
<Button variant="secondary" onClick={() => setPendingDelete(null)} disabled={deleting}>
Cancel
</Button>
<Button variant="danger" onClick={confirmDelete} disabled={deleting}>
Confirm
</Button>
</>
)}
>
{pendingDelete?.type === 'step' && 'Are you sure you want to delete this step?'}
{pendingDelete?.type === 'credential' && 'Are you sure you want to remove this credential from scenario?'}
{pendingDelete?.type === 'scenario' && 'Are you sure you want to delete this scenario?'}
</Modal>
</div>
);
}