Skip to content

Commit 5031fd9

Browse files
committed
Form action for skipping a datetime waitpoint
1 parent 32b89a2 commit 5031fd9

File tree

2 files changed

+184
-104
lines changed
  • apps/webapp/app

2 files changed

+184
-104
lines changed

apps/webapp/app/presenters/v3/SpanPresenter.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ export class SpanPresenter extends BasePresenter {
447447
output: true,
448448
outputType: true,
449449
outputIsError: true,
450+
completedAfter: true,
450451
},
451452
});
452453

Lines changed: 183 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { parse } from "@conform-to/zod";
22
import { 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";
44
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
5+
import { WaitpointId } from "@trigger.dev/core/v3/apps";
56
import { Waitpoint } from "@trigger.dev/database";
67
import { motion } from "framer-motion";
78
import { useCallback, useRef } from "react";
@@ -13,40 +14,49 @@ import { Button } from "~/components/primitives/Buttons";
1314
import { DateTime } from "~/components/primitives/DateTime";
1415
import { Paragraph } from "~/components/primitives/Paragraph";
1516
import { LiveCountdown } from "~/components/runs/v3/LiveTimer";
16-
import { prisma } from "~/db.server";
17+
import { $replica, prisma } from "~/db.server";
1718
import { useOrganization } from "~/hooks/useOrganizations";
1819
import { useProject } from "~/hooks/useProject";
1920
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
2021
import { logger } from "~/services/logger.server";
2122
import { 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";
2325
import { UpsertSchedule } from "~/v3/schedules";
2426
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
2527

2628
const 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+
3646
export 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

89121
export 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={`
171214
await 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

Comments
 (0)