Skip to content

Commit a5ab0f1

Browse files
committed
refactor: replace formik with tanstack form
1 parent 75134ea commit a5ab0f1

File tree

14 files changed

+972
-816
lines changed

14 files changed

+972
-816
lines changed

package.json

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@
4242
"@mdx-js/loader": "3.1.0",
4343
"@mdx-js/react": "3.1.0",
4444
"@mui/icons-material": "6.4.2",
45-
"@mui/lab": "6.0.0-beta.25",
46-
"@mui/material": "6.4.2",
47-
"@mui/material-nextjs": "6.4.2",
48-
"@mui/styles": "6.4.2",
45+
"@mui/lab": "6.0.0-beta.31",
46+
"@mui/material": "6.4.8",
47+
"@mui/material-nextjs": "6.4.3",
48+
"@mui/styles": "6.4.8",
4949
"@next/mdx": "15.1.6",
5050
"@rjsf/core": "5.24.8",
5151
"@rjsf/mui": "5.24.8",
@@ -57,7 +57,7 @@
5757
"@squonk/mui-theme": "4.0.0",
5858
"@squonk/sdf-parser": "1.3.0",
5959
"@tanstack/match-sorter-utils": "8.19.4",
60-
"@tanstack/react-form": "^1.0.5",
60+
"@tanstack/react-form": "1.1.0",
6161
"@tanstack/react-query": "5.68.0",
6262
"@tanstack/react-query-devtools": "5.68.0",
6363
"@tanstack/react-table": "8.21.2",
@@ -70,9 +70,6 @@
7070
"axios": "1.8.3",
7171
"dayjs": "1.11.13",
7272
"filesize": "10.1.6",
73-
"formik": "2.4.6",
74-
"formik-mui": "4.0.0",
75-
"formik-mui-lab": "1.0.0",
7673
"immer": "10.1.1",
7774
"jotai": "2.12.2",
7875
"just-compare": "2.3.0",
@@ -99,7 +96,7 @@
9996
"sharp": "0.33.5",
10097
"typescript": "5.7.3",
10198
"use-immer": "0.11.0",
102-
"yup": "1.6.1"
99+
"zod": "^3.24.2"
103100
},
104101
"devDependencies": {
105102
"@next/bundle-analyzer": "15.1.6",

pnpm-lock.yaml

Lines changed: 101 additions & 193 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/CreateDatasetStorageSubscription.tsx

Lines changed: 80 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ import {
44
useCreateUnitProduct,
55
} from "@squonk/account-server-client/product";
66

7-
import { Box, Button } from "@mui/material";
7+
import { Box, Button, TextField } from "@mui/material";
88
import { captureException } from "@sentry/nextjs";
9+
import { useForm } from "@tanstack/react-form";
910
import { useQueryClient } from "@tanstack/react-query";
10-
import { Field, Form, Formik } from "formik";
11-
import { TextField } from "formik-mui";
12-
import * as yup from "yup";
11+
import { z } from "zod";
1312

1413
import { useEnqueueError } from "../hooks/useEnqueueStackError";
1514
import { useGetStorageCost } from "../hooks/useGetStorageCost";
@@ -19,71 +18,90 @@ export interface CreateDatasetStorageSubscriptionProps {
1918
unit: UnitAllDetail;
2019
}
2120

22-
const initialValues = {
23-
allowance: 1000,
24-
name: "Dataset Storage",
25-
};
26-
2721
export const CreateDatasetStorageSubscription = ({
2822
unit,
2923
}: CreateDatasetStorageSubscriptionProps) => {
3024
const { mutateAsync: createProduct } = useCreateUnitProduct();
3125
const { enqueueError, enqueueSnackbar } = useEnqueueError<AsError>();
3226
const queryClient = useQueryClient();
3327
const cost = useGetStorageCost();
28+
29+
// Define Zod schema for validation
30+
const productSchema = z.object({
31+
name: z.string().min(1, "A name is required"),
32+
allowance: z
33+
.number()
34+
.min(1, "Allowance must be at least 1")
35+
.int("Allowance must be an integer"),
36+
});
37+
38+
const form = useForm({
39+
defaultValues: {
40+
allowance: 1000,
41+
name: "Dataset Storage",
42+
},
43+
validators: {
44+
onChange: productSchema,
45+
},
46+
onSubmit: async ({ value }) => {
47+
try {
48+
await createProduct({
49+
unitId: unit.id,
50+
data: {
51+
allowance: value.allowance,
52+
limit: value.allowance, // TODO: we will implement this properly later
53+
name: value.name,
54+
type: "DATA_MANAGER_STORAGE_SUBSCRIPTION",
55+
},
56+
});
57+
enqueueSnackbar("Created product", { variant: "success" });
58+
await queryClient.invalidateQueries({ queryKey: getGetProductsQueryKey() });
59+
form.reset();
60+
} catch (error) {
61+
enqueueError(error);
62+
captureException(error);
63+
}
64+
},
65+
});
66+
3467
return (
35-
<Formik
36-
initialValues={initialValues}
37-
validationSchema={yup.object().shape({
38-
name: yup.string().trim().required("A name is required"),
39-
allowance: yup.number().min(1).integer().required("An allowance is required"),
40-
})}
41-
onSubmit={async ({ allowance, name }) => {
42-
try {
43-
await createProduct({
44-
unitId: unit.id,
45-
data: {
46-
allowance,
47-
limit: allowance, // TODO: we will implement this properly later
48-
name,
49-
type: "DATA_MANAGER_STORAGE_SUBSCRIPTION",
50-
},
51-
});
52-
enqueueSnackbar("Created product", { variant: "success" });
53-
await queryClient.invalidateQueries({ queryKey: getGetProductsQueryKey() });
54-
} catch (error) {
55-
enqueueError(error);
56-
captureException(error);
57-
}
58-
}}
59-
>
60-
{({ submitForm, isSubmitting, isValid, values }) => {
61-
return (
62-
<Form>
63-
<Box sx={{ alignItems: "baseline", display: "flex", flexWrap: "wrap", gap: 2 }}>
64-
<Field
65-
autoFocus
66-
component={TextField}
67-
label="Name"
68-
name="name"
69-
sx={{ maxWidth: 150 }}
70-
/>
71-
<Field
72-
component={TextField}
73-
label="Allowance"
74-
min={1}
75-
name="allowance"
76-
sx={{ maxWidth: 100 }}
77-
type="number"
78-
/>
79-
{!!cost && <span>Cost: {formatCoins(cost * values.allowance)}</span>}
80-
<Button disabled={isSubmitting || !isValid} onClick={() => void submitForm()}>
81-
Create
82-
</Button>
83-
</Box>
84-
</Form>
85-
);
86-
}}
87-
</Formik>
68+
<Box sx={{ alignItems: "baseline", display: "flex", flexWrap: "wrap", gap: 2 }}>
69+
<form.Field name="name">
70+
{(field) => (
71+
<TextField
72+
autoFocus
73+
error={field.state.meta.errors.length > 0}
74+
helperText={field.state.meta.errors.map((error) => error?.message)[0]}
75+
label="Name"
76+
sx={{ maxWidth: 150 }}
77+
value={field.state.value}
78+
onBlur={field.handleBlur}
79+
onChange={(e) => field.handleChange(e.target.value)}
80+
/>
81+
)}
82+
</form.Field>
83+
84+
<form.Field name="allowance">
85+
{(field) => (
86+
<TextField
87+
error={field.state.meta.errors.length > 0}
88+
helperText={field.state.meta.errors.map((error) => error?.message)[0]}
89+
label="Allowance"
90+
slotProps={{ htmlInput: { min: 1 } }}
91+
sx={{ maxWidth: 100 }}
92+
type="number"
93+
value={field.state.value}
94+
onBlur={field.handleBlur}
95+
onChange={(e) => field.handleChange(Number(e.target.value))}
96+
/>
97+
)}
98+
</form.Field>
99+
100+
{!!cost && <span>Cost: {formatCoins(cost * form.state.values.allowance)}</span>}
101+
102+
<Button disabled={!form.state.canSubmit} onClick={() => void form.handleSubmit()}>
103+
Create
104+
</Button>
105+
</Box>
88106
);
89107
};

src/components/LowerCaseTextField.tsx

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/components/labels/NewLabelButton.tsx

Lines changed: 77 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@ import { getGetDatasetsQueryKey } from "@squonk/data-manager-client/dataset";
33
import { useAddMetadata } from "@squonk/data-manager-client/metadata";
44

55
import { AddCircleOutlineRounded as AddCircleOutlineRoundedIcon } from "@mui/icons-material";
6-
import { Box, Button, IconButton, Popover, Tooltip } from "@mui/material";
6+
import { Box, Button, IconButton, Popover, TextField, Tooltip } from "@mui/material";
7+
import { useForm } from "@tanstack/react-form";
78
import { useQueryClient } from "@tanstack/react-query";
8-
import { Field, Form, Formik } from "formik";
9-
import { TextField } from "formik-mui";
109
import { bindPopover, bindTrigger, usePopupState } from "material-ui-popup-state/hooks";
11-
import * as yup from "yup";
10+
import { z } from "zod";
1211

1312
import { type TableDataset } from "../../features/DatasetsTable";
1413
import { useEnqueueError } from "../../hooks/useEnqueueStackError";
15-
import { LowerCaseTextField } from "../LowerCaseTextField";
1614

1715
export interface NewLabelButtonProps {
1816
/**
@@ -28,6 +26,45 @@ export const NewLabelButton = ({ datasetId }: NewLabelButtonProps) => {
2826

2927
const popupState = usePopupState({ variant: "popover", popupId: `add-label-${datasetId}` });
3028

29+
// Define Zod schema for validation
30+
const labelSchema = z.object({
31+
label: z.string().trim().min(1, "A label name is required"),
32+
value: z.string(),
33+
});
34+
35+
const form = useForm({
36+
defaultValues: {
37+
label: "",
38+
value: "",
39+
},
40+
validators: {
41+
onChange: labelSchema,
42+
},
43+
onSubmit: async ({ value }) => {
44+
try {
45+
await addAnnotations({
46+
datasetId,
47+
data: {
48+
labels: JSON.stringify([
49+
{
50+
type: "LabelAnnotation",
51+
label: value.label.trim().toLowerCase(),
52+
value: value.value.trim(),
53+
active: true,
54+
},
55+
]),
56+
},
57+
});
58+
await queryClient.invalidateQueries({ queryKey: getGetDatasetsQueryKey() });
59+
form.reset();
60+
} catch (error) {
61+
enqueueError(error);
62+
} finally {
63+
popupState.close();
64+
}
65+
},
66+
});
67+
3168
return (
3269
<>
3370
<Tooltip title="Add a new label">
@@ -42,47 +79,43 @@ export const NewLabelButton = ({ datasetId }: NewLabelButtonProps) => {
4279
anchorOrigin={{ vertical: "top", horizontal: "center" }}
4380
transformOrigin={{ vertical: "bottom", horizontal: "center" }}
4481
>
45-
<Formik
46-
validateOnMount
47-
initialValues={{ label: "", value: "" }}
48-
validationSchema={yup.object().shape({
49-
label: yup.string().trim().required("A label name is required"),
50-
})}
51-
onSubmit={async ({ label, value }) => {
52-
try {
53-
await addAnnotations({
54-
datasetId,
55-
data: {
56-
labels: JSON.stringify([
57-
{
58-
type: "LabelAnnotation",
59-
label: label.trim().toLowerCase(),
60-
value: value.trim(),
61-
active: true,
62-
},
63-
]),
64-
},
65-
});
66-
await queryClient.invalidateQueries({ queryKey: getGetDatasetsQueryKey() });
67-
} catch (error) {
68-
enqueueError(error);
69-
} finally {
70-
popupState.close();
71-
}
82+
<form
83+
onSubmit={(e) => {
84+
e.preventDefault();
85+
void form.handleSubmit();
7286
}}
7387
>
74-
{({ submitForm, isSubmitting, isValid }) => (
75-
<Form>
76-
<Box sx={{ alignItems: "baseline", display: "flex", gap: 1 }}>
77-
<Field autoFocus component={LowerCaseTextField} label="Name" name="label" />
78-
<Field component={TextField} label="Value" name="value" />
79-
<Button disabled={isSubmitting || !isValid} onClick={() => void submitForm()}>
80-
Add
81-
</Button>
82-
</Box>
83-
</Form>
84-
)}
85-
</Formik>
88+
<Box sx={{ alignItems: "baseline", display: "flex", gap: 1 }}>
89+
<form.Field name="label">
90+
{(field) => (
91+
<TextField
92+
autoFocus
93+
error={field.state.meta.errors.length > 0}
94+
helperText={field.state.meta.errors.map((error) => error?.message)[0]}
95+
label="Name"
96+
value={field.state.value}
97+
onBlur={field.handleBlur}
98+
onChange={(e) => field.handleChange(e.target.value.toLowerCase())}
99+
/>
100+
)}
101+
</form.Field>
102+
<form.Field name="value">
103+
{(field) => (
104+
<TextField
105+
error={field.state.meta.errors.length > 0}
106+
helperText={field.state.meta.errors.map((error) => error?.message)[0]}
107+
label="Value"
108+
value={field.state.value}
109+
onBlur={field.handleBlur}
110+
onChange={(e) => field.handleChange(e.target.value)}
111+
/>
112+
)}
113+
</form.Field>
114+
<Button disabled={!form.state.canSubmit} type="submit">
115+
Add
116+
</Button>
117+
</Box>
118+
</form>
86119
</Popover>
87120
</>
88121
);

0 commit comments

Comments
 (0)