Skip to content

Commit 9798428

Browse files
committed
Add form validation in batchMetadata module and other corrections
1 parent 44ca031 commit 9798428

File tree

4 files changed

+107
-71
lines changed

4 files changed

+107
-71
lines changed

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/BatchMetadata.tsx

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,51 @@ import {
1818
} from "@/components/ui/form";
1919
import { Input } from "@/components/ui/input";
2020
import { Textarea } from "@/components/ui/textarea";
21+
import { zodResolver } from "@hookform/resolvers/zod";
2122
import { useMutation } from "@tanstack/react-query";
2223
import { useTxNotifications } from "hooks/useTxNotifications";
2324
import { CircleAlertIcon } from "lucide-react";
2425
import { useCallback } from "react";
2526
import { useForm } from "react-hook-form";
2627
import { sendAndConfirmTransaction } from "thirdweb";
2728
import { BatchMetadataERC721 } from "thirdweb/modules";
28-
import type { NFTMetadataInputLimited } from "types/modified-types";
2929
import { parseAttributes } from "utils/parseAttributes";
30+
import { z } from "zod";
31+
import { fileBufferOrStringSchema } from "../zod-schemas";
3032
import { ModuleCardUI, type ModuleCardUIProps } from "./module-card";
3133
import type { ModuleInstanceProps } from "./module-instance";
3234
import { AdvancedNFTMetadataFormGroup } from "./nft/AdvancedNFTMetadataFormGroup";
3335
import { NFTMediaFormGroup } from "./nft/NFTMediaFormGroup";
3436
import { PropertiesFormControl } from "./nft/PropertiesFormControl";
3537

36-
// TODO: add form validation on the upload form
37-
// TODO: this module currently shows the UI for doing a single upload, but it should be batch upload UI
38+
const uploadMetadataFormSchema = z.object({
39+
name: z.string().min(1),
40+
description: z.string().optional(),
41+
image: fileBufferOrStringSchema.optional(),
42+
animationUri: fileBufferOrStringSchema.optional(),
43+
external_url: fileBufferOrStringSchema.optional(),
44+
customImage: z.string().optional(),
45+
customAnimationUrl: z.string().optional(),
46+
background_color: z
47+
.string()
48+
.refine(
49+
(c) => {
50+
return /^#[0-9a-f]{6}$/i.test(c.toLowerCase());
51+
},
52+
{
53+
message: "Invalid Hex Color",
54+
},
55+
)
56+
.optional(),
57+
attributes: z.array(
58+
z.object({
59+
trait_type: z.string().min(1),
60+
value: z.string().min(1),
61+
}),
62+
),
63+
});
3864

39-
export type UploadMetadataFormValues = NFTMetadataInputLimited & {
40-
supply: number;
41-
customImage: string;
42-
customAnimationUrl: string;
43-
attributes: { trait_type: string; value: string }[];
44-
tokenId?: string;
45-
};
65+
export type UploadMetadataFormValues = z.infer<typeof uploadMetadataFormSchema>;
4666

4767
function BatchMetadataModule(props: ModuleInstanceProps) {
4868
const { contract, ownerAccount } = props;
@@ -137,11 +157,10 @@ function UploadMetadataNFTSection(props: {
137157
uploadMetadata: (values: UploadMetadataFormValues) => Promise<void>;
138158
}) {
139159
const form = useForm<UploadMetadataFormValues>({
160+
resolver: zodResolver(uploadMetadataFormSchema),
140161
values: {
141-
supply: 1,
142-
customImage: "",
143-
customAnimationUrl: "",
144-
attributes: [{ trait_type: "", value: "" }],
162+
name: "",
163+
attributes: [],
145164
},
146165
reValidateMode: "onChange",
147166
});

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/nft/AdvancedNFTMetadataFormGroup.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import type { UseFormReturn } from "react-hook-form";
1414
import type { NFTInput } from "thirdweb/utils";
1515

1616
type AdvancedNFTMetadataFormGroupValues = {
17-
customImage: string;
18-
customAnimationUrl: string;
17+
customImage?: string;
18+
customAnimationUrl?: string;
1919
background_color?: NFTInput["background_color"];
2020
external_url?: NFTInput["external_url"];
2121
};
@@ -59,7 +59,7 @@ export function AdvancedNFTMetadataFormGroup<
5959
name="customImage"
6060
render={({ field }) => (
6161
<FormItem>
62-
<FormLabel>Image URL</FormLabel>
62+
<FormLabel>Image URI</FormLabel>
6363
<FormControl>
6464
<Input {...field} />
6565
</FormControl>
@@ -77,7 +77,7 @@ export function AdvancedNFTMetadataFormGroup<
7777
name="customAnimationUrl"
7878
render={({ field }) => (
7979
<FormItem>
80-
<FormLabel>Animation URL</FormLabel>
80+
<FormLabel>Animation URI</FormLabel>
8181
<FormControl>
8282
<Input {...field} />
8383
</FormControl>
@@ -92,13 +92,17 @@ export function AdvancedNFTMetadataFormGroup<
9292

9393
{!externalIsTextFile && (
9494
<FormFieldSetup
95-
errorMessage={undefined}
95+
errorMessage={form.formState.errors.external_url?.message}
9696
htmlFor="external-url"
9797
isRequired={false}
9898
label="External URL"
9999
helperText="This is the URL that will appear below the asset's image on OpenSea and will allow users to leave OpenSea and view the item on your site."
100100
>
101-
<Input placeholder="https://" {...form.register("external_url")} />
101+
<Input
102+
placeholder="https://"
103+
{...form.register("external_url")}
104+
type="url"
105+
/>
102106
</FormFieldSetup>
103107
)}
104108
</div>

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/nft/PropertiesFormControl.tsx

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -31,68 +31,71 @@ export function PropertiesFormControl<T extends PropertiesFormValues>(props: {
3131
<div className="flex flex-col gap-4">
3232
<h4>Attributes</h4>
3333

34-
<div className="flex flex-col gap-3">
35-
{/* Addresses */}
36-
{fields.map((fieldItem, index) => (
37-
<div className="flex items-start gap-3" key={fieldItem.id}>
38-
<FormField
39-
control={form.control}
40-
name={`attributes.${index}.trait_type`}
41-
render={({ field }) => (
42-
<FormItem className="grow">
43-
<FormControl>
44-
<Input {...field} placeholder="Trait Type" />
45-
</FormControl>
46-
<FormMessage />
47-
</FormItem>
48-
)}
49-
/>
34+
{fields.length > 0 && (
35+
<div className="flex flex-col gap-3">
36+
{/* Addresses */}
37+
{fields.map((fieldItem, index) => (
38+
<div className="flex items-start gap-3" key={fieldItem.id}>
39+
<FormField
40+
control={form.control}
41+
name={`attributes.${index}.trait_type`}
42+
render={({ field }) => (
43+
<FormItem className="grow">
44+
<FormControl>
45+
<Input {...field} placeholder="Trait Type" />
46+
</FormControl>
47+
<FormMessage />
48+
</FormItem>
49+
)}
50+
/>
5051

51-
<FormField
52-
control={form.control}
53-
name={`attributes.${index}.value`}
54-
render={({ field }) => (
55-
<FormItem className="grow">
56-
<FormControl>
57-
<Input {...field} placeholder="Value" />
58-
</FormControl>
59-
<FormMessage />
60-
</FormItem>
61-
)}
62-
/>
52+
<FormField
53+
control={form.control}
54+
name={`attributes.${index}.value`}
55+
render={({ field }) => (
56+
<FormItem className="grow">
57+
<FormControl>
58+
<Input {...field} placeholder="Value" />
59+
</FormControl>
60+
<FormMessage />
61+
</FormItem>
62+
)}
63+
/>
6364

64-
<Button
65-
variant="outline"
66-
className="!text-destructive-text bg-background"
67-
onClick={() => remove(index)}
68-
>
69-
<Trash2Icon className="size-4" />
70-
</Button>
71-
</div>
72-
))}
73-
</div>
65+
<Button
66+
variant="outline"
67+
className="!text-destructive-text bg-background"
68+
onClick={() => remove(index)}
69+
>
70+
<Trash2Icon className="size-4" />
71+
</Button>
72+
</div>
73+
))}
74+
</div>
75+
)}
7476

7577
<div className="flex flex-row gap-3">
7678
<Button
7779
size="sm"
80+
variant="outline"
7881
className="flex items-center gap-2"
7982
onClick={() => append({ trait_type: "", value: "" })}
8083
>
81-
<PlusIcon className="size-5" />
82-
Add Row
84+
<PlusIcon className="size-4" />
85+
Add Attribute
8386
</Button>
8487

85-
<Button
86-
className="flex items-center gap-2"
87-
variant="outline"
88-
size="sm"
89-
onClick={() =>
90-
form.setValue("attributes", [{ trait_type: "", value: "" }])
91-
}
92-
>
93-
Reset
94-
<RotateCcwIcon className="size-4" />
95-
</Button>
88+
{fields.length > 0 && (
89+
<Button
90+
className="flex items-center gap-2"
91+
variant="outline"
92+
size="sm"
93+
onClick={() => form.setValue("attributes", [])}
94+
>
95+
Reset
96+
<RotateCcwIcon className="size-4" />
97+
</Button>
98+
)}
9699
</div>
97100
</div>
98101
);

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/zod-schemas.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,13 @@ export const addressSchema = z.string().refine(
1111
},
1212
{ message: "Invalid Address" },
1313
);
14+
15+
export const fileBufferOrStringSchema = z.union([
16+
z.string(),
17+
z.instanceof(File),
18+
z.instanceof(Uint8Array),
19+
z.object({
20+
data: z.union([z.string(), z.instanceof(Uint8Array)]),
21+
name: z.string(),
22+
}),
23+
]);

0 commit comments

Comments
 (0)