mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 04:43:35 +00:00
refactor(automation): explain a refused deletion instead of dumping the error
Deleting an automation had no confirmation at all, and when the server refused because something still referenced it, the raw message landed in a stray row underneath the table. Deletion now confirms first, and names what it will take with it. Global triggers are ours to clean up, so the dialog offers to delete them along with the automation. A reference from a rundown event is not: editing rundown data from a settings screen would be surprising and hard to undo, so that stays a block, with the server's message and a pointer to the event editor. This matters more once the demo project ships with automations, since those are referenced by definition and every user who tries to remove one would otherwise meet the raw error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
This commit is contained in:
@@ -2,8 +2,6 @@ import { Automation, AutomationDTO, NormalisedAutomation, Trigger } from 'ontime
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { IoAdd, IoPencil, IoSparklesOutline, IoTrash } from 'react-icons/io5';
|
||||
|
||||
import { deleteAutomation } from '../../../../common/api/automation';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
@@ -15,6 +13,7 @@ import { summariseOutputs } from '../../../../common/utils/automationOutputs';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import AutomationForm from './AutomationForm';
|
||||
import { groupTriggersByAutomation } from './automationUtils';
|
||||
import DeleteAutomationDialog from './DeleteAutomationDialog';
|
||||
import RecipeLibraryModal from './recipes/RecipeLibraryModal';
|
||||
|
||||
import style from './AutomationsList.module.scss';
|
||||
@@ -37,7 +36,7 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
const { refetch } = useAutomationSettings();
|
||||
const [automationFormData, setAutomationFormData] = useState<AutomationDTO | null>(null);
|
||||
const [showRecipes, setShowRecipes] = useState(false);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Automation | null>(null);
|
||||
|
||||
/**
|
||||
* A recipe lands in the editor rather than only in the list.
|
||||
@@ -50,15 +49,9 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
setAutomationFormData(created);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
setDeleteError(null);
|
||||
await deleteAutomation(id);
|
||||
} catch (error) {
|
||||
setDeleteError(maybeAxiosError(error));
|
||||
} finally {
|
||||
refetch();
|
||||
}
|
||||
const handleDeleted = async () => {
|
||||
setDeleteTarget(null);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
const lifecyclesByAutomation = useMemo(() => groupTriggersByAutomation(triggers), [triggers]);
|
||||
@@ -82,6 +75,14 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
onInstalled={(_recipe, created) => handleRecipeInstalled(created)}
|
||||
/>
|
||||
)}
|
||||
{deleteTarget !== null && (
|
||||
<DeleteAutomationDialog
|
||||
automation={deleteTarget}
|
||||
blockingTriggers={triggers.filter((trigger) => trigger.automationId === deleteTarget.id)}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onDeleted={handleDeleted}
|
||||
/>
|
||||
)}
|
||||
<Panel.SubHeader>
|
||||
Manage automations
|
||||
<Panel.InlineElements relation='inner'>
|
||||
@@ -181,7 +182,7 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
<IconButton
|
||||
variant='ghosted-destructive'
|
||||
aria-label='Delete entry'
|
||||
onClick={() => handleDelete(automationId)}
|
||||
onClick={() => setDeleteTarget(automation)}
|
||||
>
|
||||
<IoTrash />
|
||||
</IconButton>
|
||||
@@ -190,13 +191,6 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{deleteError && (
|
||||
<tr>
|
||||
<td colSpan={6}>
|
||||
<Panel.Error>{deleteError}</Panel.Error>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Panel.Table>
|
||||
</Panel.Section>
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import type { Automation, Trigger } from 'ontime-types';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { deleteAutomation, deleteTrigger } from '../../../../common/api/automation';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
import Dialog from '../../../../common/components/dialog/Dialog';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import { getLifecycleLabel } from '../../../../common/constants/timerLifecycle';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
interface DeleteAutomationDialogProps {
|
||||
automation: Automation;
|
||||
/** global triggers pointing at this automation, they block the delete server side */
|
||||
blockingTriggers: Trigger[];
|
||||
onCancel: () => void;
|
||||
onDeleted: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The server refuses to delete an automation that is still referenced, and the panel used to
|
||||
* dump that refusal into a stray row under the table. Global triggers are ours to clean up, so
|
||||
* we offer to do it. A reference from a rundown event is not: editing rundown data from a
|
||||
* settings screen would be a surprising, hard to undo action, so that stays a block with an
|
||||
* explanation of where to go.
|
||||
*/
|
||||
export default function DeleteAutomationDialog({
|
||||
automation,
|
||||
blockingTriggers,
|
||||
onCancel,
|
||||
onDeleted,
|
||||
}: DeleteAutomationDialogProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setError(null);
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
for (const trigger of blockingTriggers) {
|
||||
await deleteTrigger(trigger.id);
|
||||
}
|
||||
await deleteAutomation(automation.id);
|
||||
onDeleted();
|
||||
} catch (error) {
|
||||
setError(maybeAxiosError(error));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen
|
||||
onClose={onCancel}
|
||||
showBackdrop
|
||||
showCloseButton
|
||||
title='Delete automation'
|
||||
bodyElements={
|
||||
<Panel.Section>
|
||||
<Panel.Paragraph>
|
||||
Delete <strong>{automation.title}</strong>? This cannot be undone.
|
||||
</Panel.Paragraph>
|
||||
|
||||
{blockingTriggers.length > 0 && (
|
||||
<Info type='warning'>
|
||||
<Info.Title>
|
||||
{blockingTriggers.length === 1
|
||||
? 'One trigger will be deleted with it'
|
||||
: `${blockingTriggers.length} triggers will be deleted with it`}
|
||||
</Info.Title>
|
||||
<Info.Body>
|
||||
{blockingTriggers.map((trigger) => `${trigger.title} (${getLifecycleLabel(trigger.trigger)})`).join(', ')}
|
||||
</Info.Body>
|
||||
</Info>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Info type='error'>
|
||||
<Info.Title>Could not delete this automation</Info.Title>
|
||||
<Info.Body>{error}</Info.Body>
|
||||
<Info.Footer>
|
||||
Automations attached to a single event have to be removed from that event first, in the event editor.
|
||||
</Info.Footer>
|
||||
</Info>
|
||||
)}
|
||||
</Panel.Section>
|
||||
}
|
||||
footerElements={
|
||||
<>
|
||||
<Button onClick={onCancel} disabled={isDeleting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='destructive' onClick={handleDelete} loading={isDeleting}>
|
||||
{blockingTriggers.length > 0 ? 'Delete triggers and automation' : 'Delete'}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user