Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useMutationForm hook #2301

Merged
merged 5 commits into from
Sep 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,43 +1,25 @@
import { FC, useMemo, useState } from "react";
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import { FormProvider, useFieldArray } from "react-hook-form";
import { graphql, useFragment } from "react-relay";

import { PlaygroundToolbarItem } from "@quri/squiggle-components";
import { Button, LinkIcon, TextTooltip, useToast } from "@quri/ui";
import { Button, LinkIcon, TextTooltip } from "@quri/ui";
import {
SquigglePlaygroundVersionPicker,
SquiggleVersionShower,
VersionedSquigglePlayground,
type SquiggleVersion,
} from "@quri/versioned-playground";

import { EditSquiggleSnippetModel$key } from "@/__generated__/EditSquiggleSnippetModel.graphql";
import {
EditSquiggleSnippetModelMutation,
RelativeValuesExportInput,
} from "@/__generated__/EditSquiggleSnippetModelMutation.graphql";
import { EditModelExports } from "@/components/exports/EditModelExports";
import { useAsyncMutation } from "@/hooks/useAsyncMutation";
import { useAvailableHeight } from "@/hooks/useAvailableHeight";
import { useMutationForm } from "@/hooks/useMutationForm";
import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers";
import {
SquigglePlaygroundVersionPicker,
VersionedSquigglePlayground,
type SquiggleVersion,
SquiggleVersionShower,
} from "@quri/versioned-playground";

export const Mutation = graphql`
mutation EditSquiggleSnippetModelMutation(
$input: MutationUpdateSquiggleSnippetModelInput!
) {
result: updateSquiggleSnippetModel(input: $input) {
__typename
... on BaseError {
message
}
... on UpdateSquiggleSnippetResult {
model {
...EditSquiggleSnippetModel
}
}
}
}
`;

type FormShape = {
code: string;
Expand All @@ -51,8 +33,6 @@ type Props = {
};

export const EditSquiggleSnippetModel: FC<Props> = ({ modelRef }) => {
const toast = useToast();

const model = useFragment(
graphql`
fragment EditSquiggleSnippetModel on Model {
Expand Down Expand Up @@ -96,8 +76,6 @@ export const EditSquiggleSnippetModel: FC<Props> = ({ modelRef }) => {
"SquiggleSnippet"
);

const { height, ref } = useAvailableHeight();

const initialFormValues: FormShape = useMemo(() => {
return {
code: content.code,
Expand All @@ -111,8 +89,42 @@ export const EditSquiggleSnippetModel: FC<Props> = ({ modelRef }) => {
};
}, [content, revision.relativeValuesExports]);

const form = useForm<FormShape>({
const { form, onSubmit, inFlight } = useMutationForm<
FormShape,
EditSquiggleSnippetModelMutation,
"UpdateSquiggleSnippetResult"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates the expectedTypename field, and unfortunately it's necessary for type safety. (It's also necessary in useAsyncMutation that we had before, but this hook's type parameters are stricter).

This is a known TypeScript limitation: it lacks partial type argument inference (see microsoft/TypeScript#26242 for the details).

I hope they'll get around to adding it in the next 6-12 months (people have been complaining about its priority for a long time, e.g. microsoft/TypeScript#55486 (comment)), but until then we'll have to tolerate it.

>({
defaultValues: initialFormValues,
mutation: graphql`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure if inlining mutations is good or bad, but started to lean towards it being good. Mutation name (EditSquiggleSnippetModelMutation) and its expectedTypename (UpdateSquiggleSnippetResult) need to be mentioned in generics and other params.

So if the mutation was declared separately outside of the component, you'd have to jump back and forth to change it.

mutation EditSquiggleSnippetModelMutation(
$input: MutationUpdateSquiggleSnippetModelInput!
) {
result: updateSquiggleSnippetModel(input: $input) {
__typename
... on BaseError {
message
}
... on UpdateSquiggleSnippetResult {
model {
...EditSquiggleSnippetModel
}
}
}
}
`,
expectedTypename: "UpdateSquiggleSnippetResult",
formDataToVariables: (formData) => ({
input: {
content: {
code: formData.code,
version,
},
relativeValuesExports: formData.relativeValuesExports,
slug: model.slug,
owner: model.owner.slug,
},
}),
confirmation: "Saved",
});

// could version picker be part of the form?
Expand All @@ -127,31 +139,6 @@ export const EditSquiggleSnippetModel: FC<Props> = ({ modelRef }) => {
control: form.control,
});

const [saveMutation, saveInFlight] = useAsyncMutation<
EditSquiggleSnippetModelMutation,
"UpdateSquiggleSnippetResult"
>({
mutation: Mutation,
expectedTypename: "UpdateSquiggleSnippetResult",
});

const save = form.handleSubmit((formData) => {
saveMutation({
variables: {
input: {
content: {
code: formData.code,
version,
},
relativeValuesExports: formData.relativeValuesExports,
slug: model.slug,
owner: model.owner.slug,
},
},
onCompleted: () => toast("Saved", "confirmation"),
});
});

const onCodeChange = (code: string) => {
form.setValue("code", code);
};
Expand All @@ -165,9 +152,11 @@ export const EditSquiggleSnippetModel: FC<Props> = ({ modelRef }) => {
setDefaultCode(form.getValues("code"));
};

const { height, ref } = useAvailableHeight();

return (
<FormProvider {...form}>
<form onSubmit={save}>
<form onSubmit={onSubmit}>
<div ref={ref}>
<VersionedSquigglePlayground
version={version}
Expand Down Expand Up @@ -205,9 +194,9 @@ export const EditSquiggleSnippetModel: FC<Props> = ({ modelRef }) => {
{model.isEditable && (
<Button
theme="primary"
onClick={save}
onClick={onSubmit}
size="small"
disabled={saveInFlight}
disabled={inFlight}
>
Save
</Button>
Expand Down
48 changes: 19 additions & 29 deletions packages/hub/src/app/new/group/NewGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { FC } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FormProvider } from "react-hook-form";
import { graphql } from "relay-runtime";

import { Button, useToast } from "@quri/ui";
import { Button } from "@quri/ui";

import { NewGroupMutation } from "@/__generated__/NewGroupMutation.graphql";
import { H1 } from "@/components/ui/Headers";
import { SlugFormField } from "@/components/ui/SlugFormField";
import { useAsyncMutation } from "@/hooks/useAsyncMutation";
import { useMutationForm } from "@/hooks/useMutationForm";
import { groupRoute } from "@/routes";

const Mutation = graphql`
Expand All @@ -33,44 +33,34 @@ const Mutation = graphql`
export const NewGroup: FC = () => {
useSession({ required: true });

const toast = useToast();
const router = useRouter();

type FormShape = {
slug: string | undefined;
};

const form = useForm<FormShape>({
const { form, onSubmit, inFlight } = useMutationForm<
FormShape,
NewGroupMutation,
"CreateGroupResult"
>({
defaultValues: {},
mode: "onChange",
});

const router = useRouter();

const [saveMutation, isSaveInFlight] = useAsyncMutation<NewGroupMutation>({
mutation: Mutation,
expectedTypename: "CreateGroupResult",
blockOnSuccess: true,
});

const save = form.handleSubmit(async (data) => {
const slug = data.slug;
if (!slug) {
// shouldn't happen but satisfies Typescript
toast("Slug is undefined", "error");
return;
}
await saveMutation({
variables: {
input: { slug },
},
onCompleted() {
router.push(groupRoute({ slug }));
formDataToVariables: (data) => ({
input: {
slug: data.slug ?? "", // shouldn't happen, but satisfies TypeScript
},
});
}),
onCompleted(result) {
router.push(groupRoute({ slug: result.group.slug }));
},
});

return (
<form onSubmit={save}>
<form onSubmit={onSubmit}>
<FormProvider {...form}>
<H1>New Group</H1>
<div className="mb-4">
Expand All @@ -82,8 +72,8 @@ export const NewGroup: FC = () => {
/>
</div>
<Button
onClick={save}
disabled={!form.formState.isValid || isSaveInFlight}
onClick={onSubmit}
disabled={!form.formState.isValid || inFlight}
theme="primary"
>
Create
Expand Down
Loading