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:
dotta
2026-03-19 16:45:08 -05:00
parent bdeaaeac9c
commit 5caf43349b
5 changed files with 86 additions and 6 deletions

View File

@@ -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),

View File

@@ -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,

View File

@@ -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>) =>

View File

@@ -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 });
}

View File

@@ -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>