diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx index f3bf509b0f..59bdc40560 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx @@ -1,11 +1,10 @@ import { conform, list, requestIntent, useFieldList, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { EnvelopeIcon, LockOpenIcon, UserPlusIcon } from "@heroicons/react/20/solid"; -import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node"; -import { json } from "@remix-run/node"; -import { Form, useActionData } from "@remix-run/react"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; +import { Form } from "@remix-run/react"; import { Fragment, useRef, useState } from "react"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import simplur from "simplur"; import invariant from "tiny-invariant"; import { z } from "zod"; @@ -29,6 +28,7 @@ import { TeamPresenter } from "~/presenters/TeamPresenter.server"; import { scheduleEmail } from "~/services/email.server"; import { requireUserId } from "~/services/session.server"; import { acceptInvitePath, organizationTeamPath, v3BillingPath } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "~/utils/conformTo"; const Params = z.object({ organizationSlug: z.string(), @@ -76,7 +76,7 @@ const schema = z.object({ }, z.string().email().array().nonempty("At least one email is required")), }); -export const action: ActionFunction = async ({ request, params }) => { +export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); const { organizationSlug } = params; invariant(organizationSlug, "organizationSlug is required"); @@ -85,7 +85,7 @@ export const action: ActionFunction = async ({ request, params }) => { const submission = parse(formData, { schema }); if (!submission.value || submission.intent !== "submit") { - return json(submission); + return typedjson(submission); } try { @@ -117,7 +117,7 @@ export const action: ActionFunction = async ({ request, params }) => { simplur`${submission.value.emails.length} member[|s] invited` ); } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } }; @@ -125,12 +125,13 @@ export default function Page() { const { limits } = useTypedLoaderData(); const [total, setTotal] = useState(limits.used); const organization = useOrganization(); - const lastSubmission = useActionData(); + + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const [form, { emails }] = useForm({ id: "invite-members", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index e408d82aa5..aa3ee96188 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -1,12 +1,12 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { HashtagIcon, LockClosedIcon } from "@heroicons/react/20/solid"; -import { Form, useActionData, useNavigate, useNavigation } from "@remix-run/react"; +import { Form, useNavigate, useNavigation } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/router"; import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { SlackIcon } from "@trigger.dev/companyicons"; import { useEffect, useState } from "react"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; @@ -42,6 +42,7 @@ import { type CreateAlertChannelOptions, CreateAlertChannelService, } from "~/v3/services/alerts/createAlertChannel.server"; +import { isSubmissionResult } from "~/utils/conformTo"; const FormSchema = z .object({ @@ -180,14 +181,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const submission = parse(formData, { schema: FormSchema }); if (!submission.value) { - return json(submission); + return typedjson(submission); } const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { submission.error.key = ["Project not found"]; - return json(submission); + return typedjson(submission); } const service = new CreateAlertChannelService(); @@ -199,7 +200,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (!alertChannel) { submission.error.key = ["Failed to create alert channel"]; - return json(submission); + return typedjson(submission); } return redirectWithSuccessMessage( @@ -212,7 +213,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { export default function Page() { const [isOpen, setIsOpen] = useState(false); const { slack, option, emailAlertsEnabled } = useTypedLoaderData(); - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const navigation = useNavigation(); const navigate = useNavigate(); const organization = useOrganization(); @@ -234,8 +236,7 @@ export default function Page() { const [form, { channelValue: channelValue, alertTypes, environmentTypes, type, integrationId }] = useForm({ id: "create-alert", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema: FormSchema }); }, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx index a4debb8329..0cc4f75878 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx @@ -10,12 +10,12 @@ import { PlusIcon, TrashIcon, } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, Outlet, useActionData, useNavigation } from "@remix-run/react"; -import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; +import { Form, type MetaFunction, Outlet, useNavigation } from "@remix-run/react"; +import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { SlackIcon } from "@trigger.dev/companyicons"; import { type ProjectAlertChannelType, type ProjectAlertType } from "@trigger.dev/database"; import assertNever from "assert-never"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { AlertsNoneDev, AlertsNoneDeployed } from "~/components/BlankStatePanels"; import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; @@ -63,6 +63,7 @@ import { v3NewProjectAlertPath, v3ProjectAlertsPath, } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "~/utils/conformTo"; export const meta: MetaFunction = () => { return [ @@ -116,14 +117,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const submission = parse(formData, { schema }); if (!submission.value) { - return json(submission); + return typedjson(submission); } const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { submission.error.key = ["Project not found"]; - return json(submission); + return typedjson(submission); } switch (submission.value.action) { @@ -355,7 +356,9 @@ export default function Page() { } function DeleteAlertChannelButton(props: { id: string }) { - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; + const navigation = useNavigation(); const isLoading = @@ -365,8 +368,7 @@ function DeleteAlertChannelButton(props: { id: string }) { const [form, { id }] = useForm({ id: "delete-alert-channel", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, @@ -394,7 +396,9 @@ function DeleteAlertChannelButton(props: { id: string }) { } function DisableAlertChannelButton(props: { id: string }) { - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; + const navigation = useNavigation(); const isLoading = @@ -404,8 +408,7 @@ function DisableAlertChannelButton(props: { id: string }) { const [form, { id }] = useForm({ id: "disable-alert-channel", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission: lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, @@ -434,7 +437,9 @@ function DisableAlertChannelButton(props: { id: string }) { } function EnableAlertChannelButton(props: { id: string }) { - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; + const navigation = useNavigation(); const isLoading = @@ -444,8 +449,7 @@ function EnableAlertChannelButton(props: { id: string }) { const [form, { id }] = useForm({ id: "enable-alert-channel", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx index c52942a8ac..42492e8ec7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx @@ -8,11 +8,11 @@ import { } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { LockClosedIcon, LockOpenIcon, PlusIcon, XMarkIcon } from "@heroicons/react/20/solid"; -import { Form, useActionData, useNavigate, useNavigation } from "@remix-run/react"; -import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; +import { Form, useNavigate, useNavigation } from "@remix-run/react"; +import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import dotenv from "dotenv"; import { type RefObject, useCallback, useEffect, useRef, useState } from "react"; -import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; +import { redirect, typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { Button, LinkButton } from "~/components/primitives/Buttons"; @@ -52,6 +52,7 @@ import { import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server"; import { EnvironmentVariableKey } from "~/v3/environmentVariables/repository"; import { Select, SelectItem } from "~/components/primitives/Select"; +import { isSubmissionResult } from "~/utils/conformTo"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -127,7 +128,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const submission = parse(formData, { schema }); if (!submission.value) { - return json(submission); + return typedjson(submission); } const project = await prisma.project.findUnique({ @@ -147,7 +148,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { }); if (!project) { submission.error.key = ["Project not found"]; - return json(submission); + return typedjson(submission); } const repository = new EnvironmentVariablesRepository(prisma); @@ -166,7 +167,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { submission.error.variables = [result.error]; } - return json(submission); + return typedjson(submission); } return redirect( @@ -181,7 +182,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { export default function Page() { const [isOpen, setIsOpen] = useState(false); const { environments, hasStaging } = useTypedLoaderData(); - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const navigation = useNavigation(); const navigate = useNavigate(); const organization = useOrganization(); @@ -201,8 +203,7 @@ export default function Page() { const [form, { environmentIds, variables }] = useForm({ id: "create-environment-variables", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx index edef3d3060..9f4e213d89 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx @@ -9,15 +9,14 @@ import { PlusIcon, TrashIcon, } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, Outlet, useActionData, useNavigation } from "@remix-run/react"; +import { Form, type MetaFunction, Outlet, useNavigation } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs, - json, redirectDocument, } from "@remix-run/server-runtime"; import { useMemo, useState } from "react"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; @@ -71,6 +70,7 @@ import { EditEnvironmentVariableValue, EnvironmentVariable, } from "~/v3/environmentVariables/repository"; +import { isSubmissionResult } from "~/utils/conformTo"; export const meta: MetaFunction = () => { return [ @@ -126,7 +126,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const submission = parse(formData, { schema }); if (!submission.value) { - return json(submission); + return typedjson(submission); } const project = await prisma.project.findUnique({ @@ -146,7 +146,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { }); if (!project) { submission.error.key = ["Project not found"]; - return json(submission); + return typedjson(submission); } switch (submission.value.action) { @@ -156,7 +156,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (!result.success) { submission.error.key = [result.error]; - return json(submission); + return typedjson(submission); } //use redirectDocument because it reloads the page @@ -179,7 +179,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (!result.success) { submission.error.key = [result.error]; - return json(submission); + return typedjson(submission); } return redirectWithSuccessMessage( @@ -417,7 +417,8 @@ function EditEnvironmentVariablePanel({ revealAll: boolean; }) { const [isOpen, setIsOpen] = useState(false); - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const navigation = useNavigation(); const isLoading = @@ -427,8 +428,7 @@ function EditEnvironmentVariablePanel({ const [form, { id, environmentId, value }] = useForm({ id: "edit-environment-variable", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, @@ -501,7 +501,9 @@ function DeleteEnvironmentVariableButton({ }: { variable: EnvironmentVariableWithSetValues; }) { - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; + const navigation = useNavigation(); const isLoading = @@ -511,8 +513,7 @@ function DeleteEnvironmentVariableButton({ const [form, { id }] = useForm({ id: "delete-environment-variable", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx index db6f641f5d..a778d0c9e5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx @@ -1,8 +1,9 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { ExclamationTriangleIcon, FolderIcon, TrashIcon } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, useActionData, useNavigation } from "@remix-run/react"; -import { type ActionFunction, json } from "@remix-run/server-runtime"; +import { Form, type MetaFunction, useNavigation } from "@remix-run/react"; +import { type ActionFunctionArgs } from "@remix-run/server-runtime"; +import { typedjson, useTypedActionData } from "remix-typedjson"; import { z } from "zod"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { InlineCode } from "~/components/code/InlineCode"; @@ -32,6 +33,7 @@ import { DeleteProjectService } from "~/services/deleteProject.server"; import { logger } from "~/services/logger.server"; import { requireUserId } from "~/services/session.server"; import { organizationPath, v3ProjectPath } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "~/utils/conformTo"; export const meta: MetaFunction = () => { return [ @@ -75,11 +77,11 @@ export function createSchema( ]); } -export const action: ActionFunction = async ({ request, params }) => { +export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); const { organizationSlug, projectParam } = params; if (!organizationSlug || !projectParam) { - return json({ errors: { body: "organizationSlug is required" } }, { status: 400 }); + return typedjson({ errors: { body: "organizationSlug is required" } }, { status: 400 }); } const formData = await request.formData(); @@ -92,7 +94,7 @@ export const action: ActionFunction = async ({ request, params }) => { const submission = parse(formData, { schema }); if (!submission.value || submission.intent !== "submit") { - return json(submission); + return typedjson(submission); } try { @@ -143,19 +145,21 @@ export const action: ActionFunction = async ({ request, params }) => { } } } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } }; export default function Page() { const project = useProject(); - const lastSubmission = useActionData(); + + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; + const navigation = useNavigation(); const [renameForm, { projectName }] = useForm({ id: "rename-project", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, shouldRevalidate: "onSubmit", onValidate({ formData }) { return parse(formData, { @@ -170,8 +174,7 @@ export default function Page() { const [deleteForm, { projectSlug }] = useForm({ id: "delete-project", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, shouldValidate: "onInput", shouldRevalidate: "onSubmit", onValidate({ formData }) { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index c9d59a126b..afd181f961 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -1,11 +1,11 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { BeakerIcon } from "@heroicons/react/20/solid"; -import { Form, useActionData, useSubmit } from "@remix-run/react"; -import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; +import { Form, useSubmit } from "@remix-run/react"; +import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { type TaskRunStatus } from "@trigger.dev/database"; import { useCallback, useEffect, useRef, useState } from "react"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import { JSONEditor } from "~/components/code/JSONEditor"; import { EnvironmentCombo, EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { Button } from "~/components/primitives/Buttons"; @@ -53,6 +53,7 @@ import { docsPath, v3RunSpanPath, v3TaskParamsSchema, v3TestPath } from "~/utils import { TestTaskService } from "~/v3/services/testTask.server"; import { OutOfEntitlementError } from "~/v3/services/triggerTask.server"; import { TestTaskData } from "~/v3/testTask"; +import { isSubmissionResult } from "~/utils/conformTo"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -93,7 +94,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { } }; -export const action: ActionFunction = async ({ request, params }) => { +export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); const { organizationSlug, projectParam, envParam, taskParam } = v3TaskParamsSchema.parse(params); @@ -101,7 +102,7 @@ export const action: ActionFunction = async ({ request, params }) => { const submission = parse(formData, { schema: TestTaskData }); if (!submission.value) { - return json(submission); + return typedjson(submission); } const project = await findProjectBySlug(organizationSlug, projectParam, userId); @@ -190,7 +191,8 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa //form submission const submit = useSubmit(); - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; //recent runs const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id); @@ -238,8 +240,7 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa const [form, { environmentId, payload }] = useForm({ id: "test-task", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema: TestTaskData }); }, @@ -365,7 +366,8 @@ function ScheduledTaskForm({ possibleTimezones: string[]; }) { const environment = useEnvironment(); - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id); const [timestampValue, setTimestampValue] = useState(); const [lastTimestampValue, setLastTimestampValue] = useState(); @@ -399,8 +401,7 @@ function ScheduledTaskForm({ }, ] = useForm({ id: "test-task-scheduled", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema: TestTaskData }); }, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx index e4c3967a36..d2ba8605b4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -7,9 +7,9 @@ import { FolderIcon, TrashIcon, } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, useActionData, useNavigation } from "@remix-run/react"; -import { type ActionFunction, json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; +import { Form, type MetaFunction, useNavigation } from "@remix-run/react"; +import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { redirect, typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; import { @@ -58,6 +58,7 @@ import { organizationSettingsPath, rootPath, } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "~/utils/conformTo"; export const meta: MetaFunction = () => { return [ @@ -134,11 +135,11 @@ export function createSchema( ]); } -export const action: ActionFunction = async ({ request, params }) => { +export const action = async ({ request, params }: ActionFunctionArgs) => { const user = await requireUser(request); const { organizationSlug } = params; if (!organizationSlug) { - return json({ errors: { body: "organizationSlug is required" } }, { status: 400 }); + return typedjson({ errors: { body: "organizationSlug is required" } }, { status: 400 }); } const formData = await request.formData(); @@ -150,7 +151,7 @@ export const action: ActionFunction = async ({ request, params }) => { const submission = parse(formData, { schema }); if (!submission.value || submission.intent !== "submit") { - return json(submission); + return typedjson(submission); } try { @@ -231,19 +232,19 @@ export const action: ActionFunction = async ({ request, params }) => { } } } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } }; export default function Page() { const { organization } = useTypedLoaderData(); - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const navigation = useNavigation(); const [renameForm, { organizationName }] = useForm({ id: "rename-organization", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, shouldRevalidate: "onSubmit", onValidate({ formData }) { return parse(formData, { @@ -254,8 +255,7 @@ export default function Page() { const [deleteForm, { organizationSlug }] = useForm({ id: "delete-organization", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, shouldValidate: "onInput", shouldRevalidate: "onSubmit", onValidate({ formData }) { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx index c83e10ced0..c17d22adfa 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx @@ -1,10 +1,15 @@ import { useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { EnvelopeIcon, LockOpenIcon, TrashIcon, UserPlusIcon } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, useActionData } from "@remix-run/react"; -import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; +import { Form, type MetaFunction } from "@remix-run/react"; +import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { useState } from "react"; -import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson"; +import { + type UseDataFunctionReturn, + typedjson, + useTypedActionData, + useTypedLoaderData, +} from "remix-typedjson"; import invariant from "tiny-invariant"; import { z } from "zod"; import { UserAvatar } from "~/components/UserProfilePhoto"; @@ -46,6 +51,7 @@ import { revokeInvitePath, v3BillingPath, } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "~/utils/conformTo"; export const meta: MetaFunction = () => { return [ @@ -89,7 +95,7 @@ const schema = z.object({ memberId: z.string(), }); -export const action: ActionFunction = async ({ request, params }) => { +export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); const { organizationSlug } = params; invariant(organizationSlug, "organizationSlug not found"); @@ -98,7 +104,7 @@ export const action: ActionFunction = async ({ request, params }) => { const submission = parse(formData, { schema }); if (!submission.value || submission.intent !== "submit") { - return json(submission); + return typedjson(submission); } try { @@ -118,7 +124,7 @@ export const action: ActionFunction = async ({ request, params }) => { `Removed ${deletedMember.user.name ?? "member"} from team` ); } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } }; @@ -318,12 +324,12 @@ function LeaveTeamModal({ actionText: string; }) { const [open, setOpen] = useState(false); - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const [form, { memberId }] = useForm({ id: "remove-member", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx index 3cd51e2641..72c1fabf3e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx @@ -1,10 +1,9 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { FolderIcon } from "@heroicons/react/20/solid"; -import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node"; -import { json } from "@remix-run/node"; -import { Form, useActionData, useNavigation } from "@remix-run/react"; -import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; +import { Form, useNavigation } from "@remix-run/react"; +import { redirect, typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import invariant from "tiny-invariant"; import { z } from "zod"; import { MainCenteredContainer } from "~/components/layout/AppLayout"; @@ -30,6 +29,7 @@ import { v3ProjectPath, selectPlanPath, } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "~/utils/conformTo"; export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await requireUserId(request); @@ -89,7 +89,7 @@ const schema = z.object({ projectVersion: z.enum(["v2", "v3"]), }); -export const action: ActionFunction = async ({ request, params }) => { +export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); const { organizationSlug } = params; invariant(organizationSlug, "organizationSlug is required"); @@ -98,7 +98,7 @@ export const action: ActionFunction = async ({ request, params }) => { const submission = parse(formData, { schema }); if (!submission.value || submission.intent !== "submit") { - return json(submission); + return typedjson(submission); } try { @@ -115,20 +115,21 @@ export const action: ActionFunction = async ({ request, params }) => { `${submission.value.projectName} created` ); } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } }; export default function Page() { const { organization, message } = useTypedLoaderData(); - const lastSubmission = useActionData(); + + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const canCreateV3Projects = organization.v3Enabled; const [form, { projectName, projectVersion }] = useForm({ id: "create-project", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx index a171153510..0cc73169c6 100644 --- a/apps/webapp/app/routes/_app.orgs.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx @@ -2,10 +2,10 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { BuildingOffice2Icon } from "@heroicons/react/20/solid"; import { RadioGroup } from "@radix-ui/react-radio-group"; -import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node"; -import { json, redirect } from "@remix-run/node"; -import { Form, useActionData, useNavigation } from "@remix-run/react"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; +import { Form, useNavigation } from "@remix-run/react"; +import { typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { MainCenteredContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; @@ -25,6 +25,7 @@ import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter. import { requireUser, requireUserId } from "~/services/session.server"; import { sendNewOrgMessage } from "~/services/slack.server"; import { organizationPath, rootPath } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "~/utils/conformTo"; const schema = z.object({ orgName: z.string().min(3).max(50), @@ -42,13 +43,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { }); }; -export const action: ActionFunction = async ({ request }) => { +export const action = async ({ request }: ActionFunctionArgs) => { const user = await requireUser(request); const formData = await request.formData(); const submission = parse(formData, { schema }); if (!submission.value || submission.intent !== "submit") { - return json(submission); + return typedjson(submission); } try { @@ -70,20 +71,20 @@ export const action: ActionFunction = async ({ request }) => { return redirect(organizationPath(organization)); } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } }; export default function NewOrganizationPage() { const { hasOrganizations } = useTypedLoaderData(); - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const { isManagedCloud } = useFeatures(); const navigation = useNavigation(); const [form, { orgName }] = useForm({ id: "create-organization", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, diff --git a/apps/webapp/app/routes/account._index/route.tsx b/apps/webapp/app/routes/account._index/route.tsx index e32498a263..4fa183bfa4 100644 --- a/apps/webapp/app/routes/account._index/route.tsx +++ b/apps/webapp/app/routes/account._index/route.tsx @@ -1,8 +1,9 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { EnvelopeIcon, UserCircleIcon } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, useActionData } from "@remix-run/react"; -import { type ActionFunction, json } from "@remix-run/server-runtime"; +import { Form, type MetaFunction } from "@remix-run/react"; +import { type ActionFunctionArgs } from "@remix-run/server-runtime"; +import { typedjson, useTypedActionData } from "remix-typedjson"; import { z } from "zod"; import { UserProfilePhoto } from "~/components/UserProfilePhoto"; import { @@ -28,6 +29,7 @@ import { redirectWithSuccessMessage } from "~/models/message.server"; import { updateUser } from "~/models/user.server"; import { requireUserId } from "~/services/session.server"; import { accountPath } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "~/utils/conformTo"; export const meta: MetaFunction = () => { return [ @@ -75,7 +77,7 @@ function createSchema( }); } -export const action: ActionFunction = async ({ request }) => { +export const action = async ({ request }: ActionFunctionArgs) => { const userId = await requireUserId(request); const formData = await request.formData(); @@ -103,7 +105,7 @@ export const action: ActionFunction = async ({ request }) => { const submission = await parse(formData, { schema: formSchema, async: true }); if (!submission.value || submission.intent !== "submit") { - return json(submission); + return typedjson(submission); } try { @@ -120,18 +122,18 @@ export const action: ActionFunction = async ({ request }) => { "Your account profile has been updated." ); } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } }; export default function Page() { const user = useUser(); - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const [form, { name, email, marketingEmails }] = useForm({ id: "account", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema: createSchema() }); }, diff --git a/apps/webapp/app/routes/account.tokens/route.tsx b/apps/webapp/app/routes/account.tokens/route.tsx index f95f53f453..228a4125e4 100644 --- a/apps/webapp/app/routes/account.tokens/route.tsx +++ b/apps/webapp/app/routes/account.tokens/route.tsx @@ -3,9 +3,14 @@ import { parse } from "@conform-to/zod"; import { BookOpenIcon, ShieldCheckIcon, TrashIcon } from "@heroicons/react/20/solid"; import { ShieldExclamationIcon } from "@heroicons/react/24/solid"; import { DialogClose } from "@radix-ui/react-dialog"; -import { Form, MetaFunction, useActionData, useFetcher } from "@remix-run/react"; -import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { Form, MetaFunction } from "@remix-run/react"; +import { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { + typedjson, + useTypedActionData, + useTypedFetcher, + useTypedLoaderData, +} from "remix-typedjson"; import { z } from "zod"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; @@ -44,6 +49,7 @@ import { } from "~/services/personalAccessToken.server"; import { requireUserId } from "~/services/session.server"; import { docsPath, personalAccessTokensPath } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "~/utils/conformTo"; export const meta: MetaFunction = () => { return [ @@ -89,13 +95,13 @@ const CreateTokenSchema = z.discriminatedUnion("action", [ }), ]); -export const action: ActionFunction = async ({ request }) => { +export const action = async ({ request }: ActionFunctionArgs) => { const userId = await requireUserId(request); const formData = await request.formData(); const submission = parse(formData, { schema: CreateTokenSchema }); if (!submission.value) { - return json(submission); + return typedjson(submission); } switch (submission.value.action) { @@ -106,9 +112,9 @@ export const action: ActionFunction = async ({ request }) => { userId, }); - return json({ ...submission, payload: { token: tokenResult } }); + return typedjson({ ...submission, payload: { token: tokenResult } }); } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } } case "revoke": { @@ -121,11 +127,11 @@ export const action: ActionFunction = async ({ request }) => { "Personal Access Token revoked" ); } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } } default: { - return json({ errors: { body: "Invalid action" } }, { status: 400 }); + return typedjson({ errors: { body: "Invalid action" } }, { status: 400 }); } } }; @@ -212,13 +218,12 @@ export default function Page() { } function CreatePersonalAccessToken() { - const fetcher = useFetcher(); - const lastSubmission = fetcher.data as any; + const fetcher = useTypedFetcher(); + const lastSubmission = isSubmissionResult(fetcher.data) ? fetcher.data : undefined; const [form, { tokenName }] = useForm({ id: "create-personal-access-token", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema: CreateTokenSchema }); }, @@ -286,12 +291,12 @@ function CreatePersonalAccessToken() { } function RevokePersonalAccessToken({ token }: { token: ObfuscatedPersonalAccessToken }) { - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const [form, { tokenId }] = useForm({ id: "revoke-personal-access-token", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema: CreateTokenSchema }); }, diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index 048878a85b..26fb29852c 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -2,10 +2,11 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { ArrowRightIcon, EnvelopeIcon, HeartIcon, UserIcon } from "@heroicons/react/20/solid"; import { HandRaisedIcon } from "@heroicons/react/24/solid"; -import { ActionFunction, json } from "@remix-run/node"; -import { Form, useActionData } from "@remix-run/react"; +import { ActionFunctionArgs } from "@remix-run/node"; +import { Form } from "@remix-run/react"; import { motion } from "framer-motion"; import { forwardRef, useState } from "react"; +import { typedjson, useTypedActionData } from "remix-typedjson"; import { z } from "zod"; import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout"; import { Button } from "~/components/primitives/Buttons"; @@ -24,6 +25,7 @@ import { redirectWithSuccessMessage } from "~/models/message.server"; import { updateUser } from "~/models/user.server"; import { requireUserId } from "~/services/session.server"; import { rootPath } from "~/utils/pathBuilder"; +import { isSubmissionResult } from "../utils/conformTo"; function createSchema( constraints: { @@ -66,7 +68,7 @@ function createSchema( }); } -export const action: ActionFunction = async ({ request }) => { +export const action = async ({ request }: ActionFunctionArgs) => { const userId = await requireUserId(request); const formData = await request.formData(); @@ -93,7 +95,7 @@ export const action: ActionFunction = async ({ request }) => { const submission = await parse(formData, { schema: formSchema, async: true }); if (!submission.value) { - return json(submission); + return typedjson(submission); } try { @@ -106,7 +108,7 @@ export const action: ActionFunction = async ({ request }) => { return redirectWithSuccessMessage(rootPath(), request, "Your details have been updated."); } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } }; @@ -121,14 +123,16 @@ const MotionHand = motion(HandIcon); export default function Page() { const user = useUser(); - const lastSubmission = useActionData(); + + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; + const [enteredEmail, setEnteredEmail] = useState(user.email ?? ""); const { isManagedCloud } = useFeatures(); const [form, { name, email, confirmEmail, referralSource }] = useForm({ id: "confirm-basic-details", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema: createSchema() }); }, diff --git a/apps/webapp/app/routes/invites.tsx b/apps/webapp/app/routes/invites.tsx index daa5408719..ec1bd9c1ba 100644 --- a/apps/webapp/app/routes/invites.tsx +++ b/apps/webapp/app/routes/invites.tsx @@ -1,8 +1,8 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; -import { ActionFunction, LoaderFunctionArgs, json, redirect } from "@remix-run/node"; -import { Form, useActionData } from "@remix-run/react"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { ActionFunctionArgs, LoaderFunctionArgs, redirect } from "@remix-run/node"; +import { Form } from "@remix-run/react"; +import { typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import simplur from "simplur"; import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout"; @@ -17,6 +17,7 @@ import { redirectWithSuccessMessage } from "~/models/message.server"; import { requireUser, requireUserId } from "~/services/session.server"; import { invitesPath, rootPath } from "~/utils/pathBuilder"; import { EnvelopeIcon } from "@heroicons/react/20/solid"; +import { isSubmissionResult } from "../utils/conformTo"; export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await requireUser(request); @@ -34,14 +35,14 @@ const schema = z.object({ inviteId: z.string(), }); -export const action: ActionFunction = async ({ request }) => { +export const action = async ({ request }: ActionFunctionArgs) => { const userId = await requireUserId(request); const formData = await request.formData(); const submission = parse(formData, { schema }); if (!submission.value) { - return json(submission); + return typedjson(submission); } try { @@ -80,18 +81,19 @@ export const action: ActionFunction = async ({ request }) => { } } } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); + return typedjson({ errors: { body: error.message } }, { status: 400 }); } }; export default function Page() { const { invites } = useTypedLoaderData(); - const lastSubmission = useActionData(); + + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const [form, { inviteId }] = useForm({ id: "accept-invite", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, onValidate({ formData }) { return parse(formData, { schema }); }, diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx index 1e44dbdc00..390e30d1a6 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx @@ -1,8 +1,8 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; -import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react"; -import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; +import { Form, useLocation, useNavigation } from "@remix-run/react"; +import { ActionFunctionArgs } from "@remix-run/server-runtime"; import { useVirtualizer } from "@tanstack/react-virtual"; import { parseExpression } from "cron-parser"; import cronstrue from "cronstrue"; @@ -54,6 +54,8 @@ import { logger } from "~/services/logger.server"; import { Spinner } from "~/components/primitives/Spinner"; import { cond } from "effect/STM"; import { useEnvironment } from "~/hooks/useEnvironment"; +import { typedjson, useTypedActionData } from "remix-typedjson"; +import { isSubmissionResult } from "~/utils/conformTo"; const cronFormat = `* * * * * ┬ ┬ ┬ ┬ ┬ @@ -72,7 +74,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const submission = parse(formData, { schema: UpsertSchedule }); if (!submission.value) { - return json(submission); + return typedjson(submission); } try { @@ -132,7 +134,8 @@ export function UpsertScheduleForm({ possibleTimezones, showGenerateField, }: EditableScheduleElements & { showGenerateField: boolean }) { - const lastSubmission = useActionData(); + const _lastSubmission = useTypedActionData(); + const lastSubmission = isSubmissionResult(_lastSubmission) ? _lastSubmission : undefined; const [selectedTimezone, setSelectedTimezone] = useState(schedule?.timezone ?? "UTC"); const isUtc = selectedTimezone === "UTC"; const [cronPattern, setCronPattern] = useState(schedule?.cron ?? ""); @@ -146,8 +149,7 @@ export function UpsertScheduleForm({ const [form, { taskIdentifier, cron, timezone, externalId, environments, deduplicationKey }] = useForm({ id: "create-schedule", - // TODO: type this - lastSubmission: lastSubmission as any, + lastSubmission, shouldRevalidate: "onSubmit", onValidate({ formData }) { return parse(formData, { schema: UpsertSchedule }); diff --git a/apps/webapp/app/utils/conformTo.ts b/apps/webapp/app/utils/conformTo.ts new file mode 100644 index 0000000000..96b7cc4d49 --- /dev/null +++ b/apps/webapp/app/utils/conformTo.ts @@ -0,0 +1,13 @@ +import { Submission } from "@conform-to/react"; +import { z } from "zod"; + +const schema = z.object({ + intent: z.string(), + payload: z.record(z.unknown()), + error: z.record(z.array(z.string())), + value: z.any().nullable().optional(), +}); + +export function isSubmissionResult(obj: unknown): obj is Submission { + return schema.safeParse(obj).success; +}