Skip to content

Commit ef695a0

Browse files
committed
Enable deleting run templates
1 parent ecd1165 commit ef695a0

3 files changed

Lines changed: 195 additions & 46 deletions

File tree

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx

Lines changed: 172 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { conform, useForm } from "@conform-to/react";
22
import { parse } from "@conform-to/zod";
3-
import { BeakerIcon, StarIcon, RectangleStackIcon } from "@heroicons/react/20/solid";
3+
import { BeakerIcon, StarIcon, RectangleStackIcon, TrashIcon } from "@heroicons/react/20/solid";
44
import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
55
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
66
import { typedjson, useTypedLoaderData } from "remix-typedjson";
@@ -62,9 +62,10 @@ import { MachinePresetName } from "@trigger.dev/core/v3";
6262
import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource";
6363
import { Callout } from "~/components/primitives/Callout";
6464
import { TaskRunTemplateService } from "~/v3/services/taskRunTemplate.server";
65-
import { RunTemplateData } from "~/v3/taskRunTemplate";
65+
import { DeleteTaskRunTemplateService } from "~/v3/services/deleteTaskRunTemplate.server";
66+
import { DeleteTaskRunTemplateData, RunTemplateData } from "~/v3/taskRunTemplate";
6667
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
67-
import { DialogClose } from "@radix-ui/react-dialog";
68+
import { DialogClose, DialogDescription } from "@radix-ui/react-dialog";
6869
import { FormButtons } from "~/components/primitives/FormButtons";
6970

7071
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
@@ -137,6 +138,7 @@ export const action: ActionFunction = async ({ request, params }) => {
137138

138139
return json({
139140
success: true,
141+
formAction,
140142
message: `Template "${runTemplateData.value.label}" created successfully`,
141143
});
142144
} catch (e) {
@@ -145,6 +147,29 @@ export const action: ActionFunction = async ({ request, params }) => {
145147
}
146148
}
147149

150+
// Handle run template deletion
151+
if (formAction === "delete-template") {
152+
const submission = parse(formData, { schema: DeleteTaskRunTemplateData });
153+
154+
if (!submission.value) {
155+
return json(submission);
156+
}
157+
158+
const deleteService = new DeleteTaskRunTemplateService();
159+
try {
160+
await deleteService.call(environment, submission.value.templateId);
161+
162+
return json({
163+
success: true,
164+
formAction,
165+
message: `Template deleted successfully`,
166+
});
167+
} catch (e) {
168+
logger.error("Failed to delete template", { error: e instanceof Error ? e.message : e });
169+
return redirectBackWithErrorMessage(request, "Failed to delete template");
170+
}
171+
}
172+
148173
const submission = parse(formData, { schema: TestTaskData });
149174

150175
if (!submission.value) {
@@ -352,7 +377,14 @@ function StandardTaskForm({
352377
] = useForm({
353378
id: "test-task",
354379
// TODO: type this
355-
lastSubmission: lastSubmission as any,
380+
lastSubmission:
381+
lastSubmission &&
382+
typeof lastSubmission === "object" &&
383+
"formAction" in lastSubmission &&
384+
lastSubmission.formAction !== "create-template" &&
385+
lastSubmission.formAction !== "delete-template"
386+
? (lastSubmission as any)
387+
: undefined,
356388
onSubmit(event, { formData }) {
357389
event.preventDefault();
358390

@@ -801,7 +833,14 @@ function ScheduledTaskForm({
801833
] = useForm({
802834
id: "test-task-scheduled",
803835
// TODO: type this
804-
lastSubmission: lastSubmission as any,
836+
lastSubmission:
837+
lastSubmission &&
838+
typeof lastSubmission === "object" &&
839+
"formAction" in lastSubmission &&
840+
lastSubmission.formAction !== "create-template" &&
841+
lastSubmission.formAction !== "delete-template"
842+
? (lastSubmission as any)
843+
: undefined,
805844
onValidate({ formData }) {
806845
return parse(formData, { schema: TestTaskData });
807846
},
@@ -1279,50 +1318,131 @@ function RunTemplatesPopover({
12791318
onTemplateSelected: (run: RunTemplate) => void;
12801319
}) {
12811320
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
1321+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
1322+
const [templateIdToDelete, setTemplateIdToDelete] = useState<string | undefined>();
1323+
1324+
const lastSubmission = useActionData<typeof action>();
1325+
1326+
useEffect(() => {
1327+
if (
1328+
lastSubmission &&
1329+
typeof lastSubmission === "object" &&
1330+
"formAction" in lastSubmission &&
1331+
"success" in lastSubmission &&
1332+
lastSubmission.formAction === "delete-template" &&
1333+
lastSubmission.success === true
1334+
) {
1335+
setIsDeleteDialogOpen(false);
1336+
}
1337+
}, [lastSubmission]);
1338+
1339+
const [deleteForm, { templateId }] = useForm({
1340+
id: "delete-template",
1341+
onValidate({ formData }) {
1342+
return parse(formData, { schema: DeleteTaskRunTemplateData });
1343+
},
1344+
});
12821345

12831346
return (
1284-
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
1285-
<PopoverTrigger asChild>
1286-
{templates.length === 0 ? (
1287-
<SimpleTooltip
1288-
button={
1289-
<Button type="button" variant="tertiary/small" LeadingIcon={StarIcon} disabled={true}>
1290-
Templates
1291-
</Button>
1292-
}
1293-
content="No templates yet"
1294-
/>
1295-
) : (
1296-
<Button type="button" variant="tertiary/small" LeadingIcon={StarIcon}>
1297-
Templates
1298-
</Button>
1299-
)}
1300-
</PopoverTrigger>
1301-
<PopoverContent className="min-w-[279px] p-0" align="end" sideOffset={6}>
1302-
<div className="max-h-80 overflow-y-auto">
1303-
<div className="p-1">
1304-
{templates.map((template) => (
1305-
<button
1306-
key={template.id}
1307-
type="button"
1308-
onClick={() => {
1309-
onTemplateSelected(template);
1310-
setIsPopoverOpen(false);
1311-
}}
1312-
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 outline-none transition-colors focus-custom hover:bg-charcoal-900 "
1313-
>
1314-
<div className="flex flex-col items-start">
1315-
<Paragraph variant="small">{template.label}</Paragraph>
1316-
<div className="flex items-center gap-2 text-xs text-text-dimmed">
1317-
<DateTime date={template.createdAt} showTooltip={false} includeTime={false} />
1318-
</div>
1347+
<>
1348+
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
1349+
<PopoverTrigger asChild>
1350+
{templates.length === 0 ? (
1351+
<SimpleTooltip
1352+
button={
1353+
<Button
1354+
type="button"
1355+
variant="tertiary/small"
1356+
LeadingIcon={StarIcon}
1357+
disabled={true}
1358+
>
1359+
Templates
1360+
</Button>
1361+
}
1362+
content="No templates yet"
1363+
/>
1364+
) : (
1365+
<Button type="button" variant="tertiary/small" LeadingIcon={StarIcon}>
1366+
Templates
1367+
</Button>
1368+
)}
1369+
</PopoverTrigger>
1370+
<PopoverContent className="min-w-[279px] p-0" align="end" sideOffset={6}>
1371+
<div className="max-h-80 overflow-y-auto">
1372+
<div className="p-1">
1373+
{templates.map((template) => (
1374+
<div
1375+
key={template.id}
1376+
className="group flex w-full items-center gap-2 rounded-sm px-2 py-2 outline-none transition-colors hover:bg-charcoal-900"
1377+
>
1378+
<button
1379+
type="button"
1380+
onClick={() => {
1381+
onTemplateSelected(template);
1382+
setIsPopoverOpen(false);
1383+
}}
1384+
className="flex-1 text-left outline-none focus-custom"
1385+
>
1386+
<div className="flex flex-col items-start">
1387+
<Paragraph variant="small" className="truncate">
1388+
{template.label}
1389+
</Paragraph>
1390+
<div className="flex items-center gap-2 text-xs text-text-dimmed">
1391+
<DateTime
1392+
date={template.createdAt}
1393+
showTooltip={false}
1394+
includeTime={false}
1395+
/>
1396+
</div>
1397+
</div>
1398+
</button>
1399+
<Button
1400+
type="button"
1401+
className="group/delete-template shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
1402+
variant="minimal/medium"
1403+
LeadingIcon={TrashIcon}
1404+
leadingIconClassName="group-hover/delete-template:text-error"
1405+
onClick={() => {
1406+
setTemplateIdToDelete(template.id);
1407+
setIsDeleteDialogOpen(true);
1408+
setIsPopoverOpen(false);
1409+
}}
1410+
/>
13191411
</div>
1320-
</button>
1321-
))}
1412+
))}
1413+
</div>
13221414
</div>
1323-
</div>
1324-
</PopoverContent>
1325-
</Popover>
1415+
</PopoverContent>
1416+
</Popover>
1417+
1418+
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
1419+
<DialogContent className="sm:max-w-sm">
1420+
<DialogHeader>Delete template</DialogHeader>
1421+
<DialogDescription className="mt-3">
1422+
Are you sure you want to delete the template? This can't be reversed.
1423+
</DialogDescription>
1424+
<div className="mt-4 flex justify-end gap-2">
1425+
<Button
1426+
type="button"
1427+
variant="tertiary/medium"
1428+
onClick={() => setIsDeleteDialogOpen(false)}
1429+
>
1430+
Cancel
1431+
</Button>
1432+
<Form method="post" {...deleteForm.props}>
1433+
<input type="hidden" name="formAction" value="delete-template" />
1434+
<input
1435+
{...conform.input(templateId, { type: "hidden" })}
1436+
value={templateIdToDelete || ""}
1437+
/>
1438+
<Button type="submit" variant="danger/medium" LeadingIcon={TrashIcon}>
1439+
Delete
1440+
</Button>
1441+
</Form>
1442+
</div>
1443+
</DialogContent>
1444+
</Dialog>
1445+
</>
13261446
);
13271447
}
13281448

@@ -1392,7 +1512,13 @@ function CreateTemplateModal({
13921512
},
13931513
] = useForm({
13941514
id: "save-template",
1395-
lastSubmission: lastSubmission as any,
1515+
lastSubmission:
1516+
lastSubmission &&
1517+
typeof lastSubmission === "object" &&
1518+
"formAction" in lastSubmission &&
1519+
lastSubmission.formAction === "create-template"
1520+
? (lastSubmission as any)
1521+
: undefined,
13961522
onSubmit(event, { formData }) {
13971523
event.preventDefault();
13981524

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { BaseService } from "./baseService.server";
2+
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
3+
4+
export class DeleteTaskRunTemplateService extends BaseService {
5+
public async call(environment: AuthenticatedEnvironment, templateId: string) {
6+
try {
7+
await this._prisma.taskRunTemplate.delete({
8+
where: {
9+
id: templateId,
10+
projectId: environment.projectId,
11+
},
12+
});
13+
} catch (e) {
14+
throw new Error(
15+
`Error deleting template: ${e instanceof Error ? e.message : JSON.stringify(e)}`
16+
);
17+
}
18+
}
19+
}

apps/webapp/app/v3/taskRunTemplate.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ export const RunTemplateData = TestTaskData.and(
88
);
99

1010
export type RunTemplateData = z.infer<typeof RunTemplateData>;
11+
12+
export const DeleteTaskRunTemplateData = z.object({
13+
templateId: z.string(),
14+
});

0 commit comments

Comments
 (0)