11import { parse } from "@conform-to/zod" ;
22import { InformationCircleIcon } from "@heroicons/react/20/solid" ;
3- import { Form , useNavigation , useSubmit } from "@remix-run/react" ;
3+ import { Form , useLocation , useNavigation , useSubmit } from "@remix-run/react" ;
44import { ActionFunctionArgs , json } from "@remix-run/server-runtime" ;
5+ import { WaitpointId } from "@trigger.dev/core/v3/apps" ;
56import { Waitpoint } from "@trigger.dev/database" ;
67import { motion } from "framer-motion" ;
78import { useCallback , useRef } from "react" ;
@@ -13,40 +14,49 @@ import { Button } from "~/components/primitives/Buttons";
1314import { DateTime } from "~/components/primitives/DateTime" ;
1415import { Paragraph } from "~/components/primitives/Paragraph" ;
1516import { LiveCountdown } from "~/components/runs/v3/LiveTimer" ;
16- import { prisma } from "~/db.server" ;
17+ import { $replica , prisma } from "~/db.server" ;
1718import { useOrganization } from "~/hooks/useOrganizations" ;
1819import { useProject } from "~/hooks/useProject" ;
1920import { redirectWithErrorMessage , redirectWithSuccessMessage } from "~/models/message.server" ;
2021import { logger } from "~/services/logger.server" ;
2122import { requireUserId } from "~/services/session.server" ;
22- import { ProjectParamSchema , v3SchedulesPath } from "~/utils/pathBuilder" ;
23+ import { ProjectParamSchema , v3RunsPath , v3SchedulesPath } from "~/utils/pathBuilder" ;
24+ import { engine } from "~/v3/runEngine.server" ;
2325import { UpsertSchedule } from "~/v3/schedules" ;
2426import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server" ;
2527
2628const CompleteWaitpointFormData = z . discriminatedUnion ( "type" , [
2729 z . object ( {
2830 type : z . literal ( "MANUAL" ) ,
2931 payload : z . string ( ) ,
32+ successRedirect : z . string ( ) ,
33+ failureRedirect : z . string ( ) ,
3034 } ) ,
3135 z . object ( {
3236 type : z . literal ( "DATETIME" ) ,
37+ successRedirect : z . string ( ) ,
38+ failureRedirect : z . string ( ) ,
3339 } ) ,
3440] ) ;
3541
42+ const Params = ProjectParamSchema . extend ( {
43+ waitpointFriendlyId : z . string ( ) ,
44+ } ) ;
45+
3646export const action = async ( { request, params } : ActionFunctionArgs ) => {
3747 const userId = await requireUserId ( request ) ;
38- const { organizationSlug, projectParam } = ProjectParamSchema . parse ( params ) ;
48+ const { organizationSlug, projectParam, waitpointFriendlyId } = Params . parse ( params ) ;
3949
4050 const formData = await request . formData ( ) ;
41- const submission = parse ( formData , { schema : UpsertSchedule } ) ;
51+ const submission = parse ( formData , { schema : CompleteWaitpointFormData } ) ;
4252
4353 if ( ! submission . value ) {
4454 return json ( submission ) ;
4555 }
4656
4757 try {
4858 //first check that the user has access to the project
49- const project = await prisma . project . findUnique ( {
59+ const project = await $replica . project . findUnique ( {
5060 where : {
5161 slug : projectParam ,
5262 organization : {
@@ -64,27 +74,49 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
6474 throw new Error ( "Project not found" ) ;
6575 }
6676
67- const createSchedule = new UpsertTaskScheduleService ( ) ;
68- const result = await createSchedule . call ( project . id , submission . value ) ;
77+ switch ( submission . value . type ) {
78+ case "DATETIME" : {
79+ const waitpointId = WaitpointId . toId ( waitpointFriendlyId ) ;
6980
70- return redirectWithSuccessMessage (
71- v3SchedulesPath ( { slug : organizationSlug } , { slug : projectParam } ) ,
72- request ,
73- submission . value ?. friendlyId === result . id ? "Schedule updated" : "Schedule created"
74- ) ;
81+ const waitpoint = await $replica . waitpoint . findFirst ( {
82+ select : {
83+ projectId : true ,
84+ } ,
85+ where : {
86+ id : waitpointId ,
87+ } ,
88+ } ) ;
89+
90+ if ( waitpoint ?. projectId !== project . id ) {
91+ throw new Error ( "Waitpoint not found" ) ;
92+ }
93+
94+ const result = await engine . completeWaitpoint ( {
95+ id : waitpointId ,
96+ } ) ;
97+
98+ return redirectWithSuccessMessage (
99+ submission . value . successRedirect ,
100+ request ,
101+ "Waitpoint skipped"
102+ ) ;
103+ }
104+ case "MANUAL" : {
105+ }
106+ }
75107 } catch ( error : any ) {
76- logger . error ( "Failed to create schedule " , error ) ;
108+ logger . error ( "Failed to complete waitpoint " , error ) ;
77109
78110 const errorMessage = `Something went wrong. Please try again.` ;
79111 return redirectWithErrorMessage (
80- v3SchedulesPath ( { slug : organizationSlug } , { slug : projectParam } ) ,
112+ v3RunsPath ( { slug : organizationSlug } , { slug : projectParam } ) ,
81113 request ,
82114 errorMessage
83115 ) ;
84116 }
85117} ;
86118
87- type FormWaitpoint = Pick < Waitpoint , "friendlyId" | "type" > ;
119+ type FormWaitpoint = Pick < Waitpoint , "friendlyId" | "type" | "completedAfter" | "status" > ;
88120
89121export function CompleteWaitpointForm ( { waitpoint } : { waitpoint : FormWaitpoint } ) {
90122 const navigation = useNavigation ( ) ;
@@ -115,101 +147,148 @@ export function CompleteWaitpointForm({ waitpoint }: { waitpoint: FormWaitpoint
115147 [ currentJson ]
116148 ) ;
117149
118- const endTime = new Date ( Date . now ( ) + 60_000 * 113 ) ;
119-
120150 return (
121151 < div className = "space-y-3" >
122- < Form
123- action = { formAction }
124- method = "post"
125- onSubmit = { ( e ) => submitForm ( e ) }
126- className = "grid h-full max-h-full grid-rows-[2.5rem_1fr_2.5rem] overflow-hidden rounded-md border border-grid-bright"
127- >
128- < div className = "mx-3 flex items-center" >
129- < Paragraph variant = "small/bright" > Manually complete this waitpoint</ Paragraph >
130- </ div >
131- < div className = "overflow-y-auto border-t border-grid-dimmed bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" >
132- < input type = "hidden" name = "type" value = { waitpoint . type } />
133- < div className = "max-h-[70vh] min-h-40 overflow-y-auto bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" >
134- < JSONEditor
135- autoFocus
136- defaultValue = { currentJson . current }
137- readOnly = { false }
138- basicSetup
139- onChange = { ( v ) => {
140- currentJson . current = v ;
141- } }
142- showClearButton = { false }
143- showCopyButton = { false }
144- height = "100%"
145- min-height = "100%"
146- max-height = "100%"
147- />
148- </ div >
149- </ div >
150- < div className = "bg-charcoal-900 px-2" >
151- < div className = "mb-2 flex items-center justify-end gap-2 border-t border-grid-dimmed pt-2" >
152- < Button
153- variant = "secondary/small"
154- type = "submit"
155- disabled = { isLoading }
156- LeadingIcon = { isLoading ? "spinner" : undefined }
157- >
158- { isLoading ? "Completing…" : "Complete waitpoint" }
159- </ Button >
160- </ div >
161- </ div >
162- </ Form >
163- < CodeBlock
164- rowTitle = {
165- < span className = "-ml-1 flex items-center gap-1 text-text-dimmed" >
166- < InformationCircleIcon className = "size-5 shrink-0 text-text-dimmed" />
167- To complete this waitpoint in your code use:
168- </ span >
169- }
170- code = { `
152+ { waitpoint . type === "DATETIME" ? (
153+ waitpoint . completedAfter ? (
154+ < CompleteDateTimeWaitpointForm
155+ waitpoint = { {
156+ friendlyId : waitpoint . friendlyId ,
157+ completedAfter : waitpoint . completedAfter ,
158+ } }
159+ />
160+ ) : (
161+ < > Waitpoint doesn't have a complete date</ >
162+ )
163+ ) : (
164+ < >
165+ < Form
166+ action = { formAction }
167+ method = "post"
168+ onSubmit = { ( e ) => submitForm ( e ) }
169+ className = "grid h-full max-h-full grid-rows-[2.5rem_1fr_2.5rem] overflow-hidden rounded-md border border-grid-bright"
170+ >
171+ < input type = "hidden" name = "type" value = { waitpoint . type } />
172+ < div className = "mx-3 flex items-center" >
173+ < Paragraph variant = "small/bright" > Manually complete this waitpoint</ Paragraph >
174+ </ div >
175+ < div className = "overflow-y-auto border-t border-grid-dimmed bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" >
176+ < div className = "max-h-[70vh] min-h-40 overflow-y-auto bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" >
177+ < JSONEditor
178+ autoFocus
179+ defaultValue = { currentJson . current }
180+ readOnly = { false }
181+ basicSetup
182+ onChange = { ( v ) => {
183+ currentJson . current = v ;
184+ } }
185+ showClearButton = { false }
186+ showCopyButton = { false }
187+ height = "100%"
188+ min-height = "100%"
189+ max-height = "100%"
190+ />
191+ </ div >
192+ </ div >
193+ < div className = "bg-charcoal-900 px-2" >
194+ < div className = "mb-2 flex items-center justify-end gap-2 border-t border-grid-dimmed pt-2" >
195+ < Button
196+ variant = "secondary/small"
197+ type = "submit"
198+ disabled = { isLoading }
199+ LeadingIcon = { isLoading ? "spinner" : undefined }
200+ >
201+ { isLoading ? "Completing…" : "Complete waitpoint" }
202+ </ Button >
203+ </ div >
204+ </ div >
205+ </ Form >
206+ < CodeBlock
207+ rowTitle = {
208+ < span className = "-ml-1 flex items-center gap-1 text-text-dimmed" >
209+ < InformationCircleIcon className = "size-5 shrink-0 text-text-dimmed" />
210+ To complete this waitpoint in your code use:
211+ </ span >
212+ }
213+ code = { `
171214await wait.completeToken<YourType>(tokenId,
172215 output
173216);` }
174- showLineNumbers = { false }
175- />
176- < Form
177- action = { formAction }
178- method = "post"
179- onSubmit = { ( e ) => submitForm ( e ) }
180- className = "grid h-full max-h-full grid-rows-[2.5rem_1fr_2.5rem] overflow-hidden rounded-md border border-grid-bright"
181- >
182- < div className = "mx-3 flex items-center" >
183- < Paragraph variant = "small/bright" > Manually skip this waitpoint</ Paragraph >
184- </ div >
185- < div className = "border-t border-grid-dimmed" >
186- < input type = "hidden" name = "type" value = { waitpoint . type } />
187- < div className = "flex flex-wrap items-center justify-between gap-1 p-2 text-sm tabular-nums" >
188- < div className = "flex items-center gap-1" >
189- < AnimatedHourglassIcon
190- className = "text-dimmed-dimmed size-4"
191- delay = { ( endTime . getMilliseconds ( ) - Date . now ( ) ) / 1000 }
192- />
193- < span className = "mt-0.5 " >
194- < LiveCountdown endTime = { endTime } />
195- </ span >
196- </ div >
197- < DateTime date = { endTime } />
217+ showLineNumbers = { false }
218+ />
219+ </ >
220+ ) }
221+ </ div >
222+ ) ;
223+ }
224+
225+ function CompleteDateTimeWaitpointForm ( {
226+ waitpoint,
227+ } : {
228+ waitpoint : { friendlyId : string ; completedAfter : Date } ;
229+ } ) {
230+ const location = useLocation ( ) ;
231+ const navigation = useNavigation ( ) ;
232+ const submit = useSubmit ( ) ;
233+ const isLoading = navigation . state !== "idle" ;
234+ const organization = useOrganization ( ) ;
235+ const project = useProject ( ) ;
236+
237+ const timeToComplete = waitpoint . completedAfter . getTime ( ) - Date . now ( ) ;
238+ if ( timeToComplete < 0 ) {
239+ return (
240+ < div className = "flex items-center justify-center" >
241+ < Paragraph variant = "small/bright" > Waitpoint completed</ Paragraph >
242+ </ div >
243+ ) ;
244+ }
245+
246+ return (
247+ < Form
248+ action = { `/resources/orgs/${ organization . slug } /projects/${ project . slug } /waitpoints/${ waitpoint . friendlyId } /complete` }
249+ method = "post"
250+ className = "grid h-full max-h-full grid-rows-[2.5rem_1fr_2.5rem] overflow-hidden rounded-md border border-grid-bright"
251+ >
252+ < div className = "mx-3 flex items-center" >
253+ < Paragraph variant = "small/bright" > Manually skip this waitpoint</ Paragraph >
254+ </ div >
255+ < div className = "border-t border-grid-dimmed" >
256+ < input type = "hidden" name = "type" value = { "DATETIME" } />
257+ < input
258+ type = "hidden"
259+ name = "successRedirect"
260+ value = { `${ location . pathname } ${ location . search } ` }
261+ />
262+ < input
263+ type = "hidden"
264+ name = "failureRedirect"
265+ value = { `${ location . pathname } ${ location . search } ` }
266+ />
267+ < div className = "flex flex-wrap items-center justify-between gap-1 p-2 text-sm tabular-nums" >
268+ < div className = "flex items-center gap-1" >
269+ < AnimatedHourglassIcon
270+ className = "text-dimmed-dimmed size-4"
271+ delay = { ( waitpoint . completedAfter . getMilliseconds ( ) - Date . now ( ) ) / 1000 }
272+ />
273+ < span className = "mt-0.5 " >
274+ < LiveCountdown endTime = { waitpoint . completedAfter } />
275+ </ span >
198276 </ div >
277+ < DateTime date = { waitpoint . completedAfter } />
199278 </ div >
200- < div className = "px-2" >
201- < div className = "mb-2 flex items-center justify-end gap-2 border-t border-grid-dimmed pt -2" >
202- < Button
203- variant = "secondary/small"
204- type = "submit "
205- disabled = { isLoading }
206- LeadingIcon = { isLoading ? "spinner" : undefined }
207- >
208- { isLoading ? "Completing…" : "Skip waitpoint" }
209- </ Button >
210- </ div >
279+ </ div >
280+ < div className = "px -2" >
281+ < div className = "mb-2 flex items-center justify-end gap-2 border-t border-grid-dimmed pt-2" >
282+ < Button
283+ variant = "secondary/small "
284+ type = "submit"
285+ disabled = { isLoading }
286+ LeadingIcon = { isLoading ? "spinner" : undefined }
287+ >
288+ { isLoading ? "Completing…" : "Skip waitpoint" }
289+ </ Button >
211290 </ div >
212- </ Form >
213- </ div >
291+ </ div >
292+ </ Form >
214293 ) ;
215294}
0 commit comments