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);
|
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(
|
router.post(
|
||||||
"/routine-triggers/:id/rotate-secret",
|
"/routine-triggers/:id/rotate-secret",
|
||||||
validate(rotateRoutineTriggerSecretSchema),
|
validate(rotateRoutineTriggerSecretSchema),
|
||||||
|
|||||||
@@ -843,6 +843,13 @@ export function routineService(db: Db) {
|
|||||||
return (updated as RoutineTrigger | undefined) ?? null;
|
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 (
|
rotateTriggerSecret: async (
|
||||||
id: string,
|
id: string,
|
||||||
actor: Actor,
|
actor: Actor,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const routinesApi = {
|
|||||||
api.post<RoutineTriggerResponse>(`/routines/${id}/triggers`, data),
|
api.post<RoutineTriggerResponse>(`/routines/${id}/triggers`, data),
|
||||||
updateTrigger: (id: string, data: Record<string, unknown>) =>
|
updateTrigger: (id: string, data: Record<string, unknown>) =>
|
||||||
api.patch<RoutineTrigger>(`/routine-triggers/${id}`, data),
|
api.patch<RoutineTrigger>(`/routine-triggers/${id}`, data),
|
||||||
|
deleteTrigger: (id: string) => api.delete<void>(`/routine-triggers/${id}`),
|
||||||
rotateTriggerSecret: (id: string) =>
|
rotateTriggerSecret: (id: string) =>
|
||||||
api.post<RotateRoutineTriggerResponse>(`/routine-triggers/${id}/rotate-secret`, {}),
|
api.post<RotateRoutineTriggerResponse>(`/routine-triggers/${id}/rotate-secret`, {}),
|
||||||
run: (id: string, data?: Record<string, unknown>) =>
|
run: (id: string, data?: Record<string, unknown>) =>
|
||||||
|
|||||||
@@ -422,6 +422,11 @@ function invalidateActivityQueries(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entityType === "routine" || entityType === "routine_trigger" || entityType === "routine_run") {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["routines"] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (entityType === "company") {
|
if (entityType === "company") {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Repeat,
|
Repeat,
|
||||||
Save,
|
Save,
|
||||||
|
Trash2,
|
||||||
Webhook,
|
Webhook,
|
||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -115,10 +116,12 @@ function TriggerEditor({
|
|||||||
trigger,
|
trigger,
|
||||||
onSave,
|
onSave,
|
||||||
onRotate,
|
onRotate,
|
||||||
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
trigger: RoutineTrigger;
|
trigger: RoutineTrigger;
|
||||||
onSave: (id: string, patch: Record<string, unknown>) => void;
|
onSave: (id: string, patch: Record<string, unknown>) => void;
|
||||||
onRotate: (id: string) => void;
|
onRotate: (id: string) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState({
|
const [draft, setDraft] = useState({
|
||||||
label: trigger.label ?? "",
|
label: trigger.label ?? "",
|
||||||
@@ -241,6 +244,16 @@ function TriggerEditor({
|
|||||||
Rotate secret
|
Rotate secret
|
||||||
</Button>
|
</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>}
|
{trigger.lastResult && <span className="text-xs text-muted-foreground">Last: {trigger.lastResult}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -405,11 +418,13 @@ export function RoutineDetail() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveRoutine = useMutation({
|
const saveRoutine = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () => {
|
||||||
routinesApi.update(routineId!, {
|
const { status: _status, ...payload } = editDraft;
|
||||||
...editDraft,
|
return routinesApi.update(routineId!, {
|
||||||
description: editDraft.description.trim() || null,
|
...payload,
|
||||||
}),
|
description: payload.description.trim() || null,
|
||||||
|
});
|
||||||
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
||||||
@@ -429,6 +444,8 @@ export function RoutineDetail() {
|
|||||||
const runRoutine = useMutation({
|
const runRoutine = useMutation({
|
||||||
mutationFn: () => routinesApi.run(routineId!),
|
mutationFn: () => routinesApi.run(routineId!),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
pushToast({ title: "Routine run started", tone: "success" });
|
||||||
|
setActiveTab("runs");
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.runs(routineId!) }),
|
queryClient.invalidateQueries({ queryKey: queryKeys.routines.runs(routineId!) }),
|
||||||
@@ -447,7 +464,11 @@ export function RoutineDetail() {
|
|||||||
|
|
||||||
const updateRoutineStatus = useMutation({
|
const updateRoutineStatus = useMutation({
|
||||||
mutationFn: (status: string) => routinesApi.update(routineId!, { status }),
|
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([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(routineId!) }),
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }),
|
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({
|
const rotateTrigger = useMutation({
|
||||||
mutationFn: (id: string): Promise<RotateRoutineTriggerResponse> => routinesApi.rotateTriggerSecret(id),
|
mutationFn: (id: string): Promise<RotateRoutineTriggerResponse> => routinesApi.rotateTriggerSecret(id),
|
||||||
onSuccess: async (result) => {
|
onSuccess: async (result) => {
|
||||||
@@ -933,6 +972,7 @@ export function RoutineDetail() {
|
|||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
onSave={(id, patch) => updateTrigger.mutate({ id, patch })}
|
onSave={(id, patch) => updateTrigger.mutate({ id, patch })}
|
||||||
onRotate={(id) => rotateTrigger.mutate(id)}
|
onRotate={(id) => rotateTrigger.mutate(id)}
|
||||||
|
onDelete={(id) => deleteTrigger.mutate(id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user