11import { conform , useForm } from "@conform-to/react" ;
22import { 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" ;
44import { type ActionFunction , type LoaderFunctionArgs , json } from "@remix-run/server-runtime" ;
55import { useCallback , useEffect , useRef , useState , useMemo } from "react" ;
66import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
@@ -62,9 +62,10 @@ import { MachinePresetName } from "@trigger.dev/core/v3";
6262import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource" ;
6363import { Callout } from "~/components/primitives/Callout" ;
6464import { 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" ;
6667import { 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" ;
6869import { FormButtons } from "~/components/primitives/FormButtons" ;
6970
7071export 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
0 commit comments