Improve routine configuration: delete triggers, fix pause, add feedback
- Add trash icon button to delete triggers (full stack: service, route, API client, UI) - Fix pause/unpause bug where saving routine could revert status by excluding status from the save payload (status is managed via dedicated pause/resume buttons) - Add toast feedback for run, pause, and resume actions - Auto-switch to Runs tab after triggering a manual run - Add live update invalidation for routine/trigger/run activity events Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -172,6 +172,33 @@ export function routineRoutes(db: Db) {
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
router.delete("/routine-triggers/:id", async (req, res) => {
|
||||
const trigger = await svc.getTrigger(req.params.id as string);
|
||||
if (!trigger) {
|
||||
res.status(404).json({ error: "Routine trigger not found" });
|
||||
return;
|
||||
}
|
||||
const routine = await assertCanManageExistingRoutine(req, trigger.routineId);
|
||||
if (!routine) {
|
||||
res.status(404).json({ error: "Routine not found" });
|
||||
return;
|
||||
}
|
||||
await svc.deleteTrigger(trigger.id);
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId: routine.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "routine.trigger_deleted",
|
||||
entityType: "routine_trigger",
|
||||
entityId: trigger.id,
|
||||
details: { routineId: routine.id, kind: trigger.kind },
|
||||
});
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/routine-triggers/:id/rotate-secret",
|
||||
validate(rotateRoutineTriggerSecretSchema),
|
||||
|
||||
@@ -843,6 +843,13 @@ export function routineService(db: Db) {
|
||||
return (updated as RoutineTrigger | undefined) ?? null;
|
||||
},
|
||||
|
||||
deleteTrigger: async (id: string): Promise<boolean> => {
|
||||
const existing = await getTriggerById(id);
|
||||
if (!existing) return false;
|
||||
await db.delete(routineTriggers).where(eq(routineTriggers.id, id));
|
||||
return true;
|
||||
},
|
||||
|
||||
rotateTriggerSecret: async (
|
||||
id: string,
|
||||
actor: Actor,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const routinesApi = {
|
||||
api.post<RoutineTriggerResponse>(`/routines/${id}/triggers`, data),
|
||||
updateTrigger: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<RoutineTrigger>(`/routine-triggers/${id}`, data),
|
||||
deleteTrigger: (id: string) => api.delete<void>(`/routine-triggers/${id}`),
|
||||
rotateTriggerSecret: (id: string) =>
|
||||
api.post<RotateRoutineTriggerResponse>(`/routine-triggers/${id}/rotate-secret`, {}),
|
||||
run: (id: string, data?: Record<string, unknown>) =>
|
||||
|
||||
@@ -422,6 +422,11 @@ function invalidateActivityQueries(
|
||||
return;
|
||||
}
|
||||
|
||||
if (entityType === "routine" || entityType === "routine_trigger" || entityType === "routine_run") {
|
||||
queryClient.invalidateQueries({ queryKey: ["routines"] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (entityType === "company") {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
RefreshCw,
|
||||
Repeat,
|
||||
Save,
|
||||
Trash2,
|
||||
Webhook,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
@@ -115,10 +116,12 @@ function TriggerEditor({
|
||||
trigger,
|
||||
onSave,
|
||||
onRotate,
|
||||
onDelete,
|
||||
}: {
|
||||
trigger: RoutineTrigger;
|
||||
onSave: (id: string, patch: Record<string, unknown>) => void;
|
||||
onRotate: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState({
|
||||
label: trigger.label ?? "",
|
||||
@@ -241,6 +244,16 @@ function TriggerEditor({
|
||||
Rotate secret
|
||||
</Button>
|
||||
)}
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={() => onDelete(trigger.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
{trigger.lastResult && <span className="text-xs text-muted-foreground">Last: {trigger.lastResult}</span>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -405,11 +418,13 @@ export function RoutineDetail() {
|
||||
};
|
||||
|
||||
const saveRoutine = useMutation({
|
||||
mutationFn: () =>
|
||||
routinesApi.update(routineId!, {
|
||||
...editDraft,
|
||||
description: editDraft.description.trim() || null,
|
||||
}),
|
||||
mutationFn: () => {
|
||||
const { status: _status, ...payload } = editDraft;
|
||||
return routinesApi.update(routineId!, {
|
||||
...payload,
|
||||
description: payload.description.trim() || null,
|
||||
});
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
||||
@@ -429,6 +444,8 @@ export function RoutineDetail() {
|
||||
const runRoutine = useMutation({
|
||||
mutationFn: () => routinesApi.run(routineId!),
|
||||
onSuccess: async () => {
|
||||
pushToast({ title: "Routine run started", tone: "success" });
|
||||
setActiveTab("runs");
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.runs(routineId!) }),
|
||||
@@ -447,7 +464,11 @@ export function RoutineDetail() {
|
||||
|
||||
const updateRoutineStatus = useMutation({
|
||||
mutationFn: (status: string) => routinesApi.update(routineId!, { status }),
|
||||
onSuccess: async () => {
|
||||
onSuccess: async (_data, status) => {
|
||||
pushToast({
|
||||
title: status === "paused" ? "Routine paused" : "Routine resumed",
|
||||
tone: "success",
|
||||
});
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }),
|
||||
@@ -521,6 +542,24 @@ export function RoutineDetail() {
|
||||
},
|
||||
});
|
||||
|
||||
const deleteTrigger = useMutation({
|
||||
mutationFn: (id: string) => routinesApi.deleteTrigger(id),
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }),
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.activity(selectedCompanyId!, routineId!) }),
|
||||
]);
|
||||
},
|
||||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to delete trigger",
|
||||
body: error instanceof Error ? error.message : "Paperclip could not delete the trigger.",
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const rotateTrigger = useMutation({
|
||||
mutationFn: (id: string): Promise<RotateRoutineTriggerResponse> => routinesApi.rotateTriggerSecret(id),
|
||||
onSuccess: async (result) => {
|
||||
@@ -933,6 +972,7 @@ export function RoutineDetail() {
|
||||
trigger={trigger}
|
||||
onSave={(id, patch) => updateTrigger.mutate({ id, patch })}
|
||||
onRotate={(id) => rotateTrigger.mutate(id)}
|
||||
onDelete={(id) => deleteTrigger.mutate(id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user