Skip to content

Commit a98550d

Browse files
committed
[Dashboard] Feature: Modules UI changes (#5437)
https://linear.app/thirdweb/issue/DASH-469/small-changes-for-modules-ui <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the functionality and user experience of the `Transferable`, `Claimable`, and `Royalty` components in the dashboard application, particularly by adding support for ERC20 tokens and improving form handling for royalties. ### Detailed summary - Added support for `isErc20` state in `Transferable` and `Claimable` components. - Updated UI to display messages related to transfer restrictions and accounts. - Introduced `SequentialTokenIdFieldset` to handle token ID inputs. - Changed royalty handling from BPS to percentage in `Royalty` component. - Improved form validation and error messaging for royalty inputs. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 0e98b09 commit a98550d

File tree

6 files changed

+127
-25
lines changed

6 files changed

+127
-25
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
7373
const [tokenId, setTokenId] = useState<string>("");
7474

7575
const isErc721 = props.contractInfo.name === "ClaimableERC721";
76+
const isErc20 = props.contractInfo.name === "ClaimableERC20";
7677
const isValidTokenId = positiveIntegerRegex.test(tokenId);
7778

7879
const primarySaleRecipientQuery = useReadContract(
@@ -241,6 +242,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
241242
}}
242243
isOwnerAccount={!!ownerAccount}
243244
isErc721={isErc721}
245+
isErc20={isErc20}
244246
contractChainId={props.contract.chain.id}
245247
setTokenId={setTokenId}
246248
isValidTokenId={isValidTokenId}
@@ -256,6 +258,7 @@ export function ClaimableModuleUI(
256258
props: Omit<ModuleCardUIProps, "children" | "updateButton"> & {
257259
isOwnerAccount: boolean;
258260
isErc721: boolean;
261+
isErc20: boolean;
259262
contractChainId: number;
260263
setTokenId: Dispatch<SetStateAction<string>>;
261264
isValidTokenId: boolean;
@@ -295,7 +298,7 @@ export function ClaimableModuleUI(
295298
<Accordion type="single" collapsible className="-mx-1">
296299
<AccordionItem value="metadata" className="border-none">
297300
<AccordionTrigger className="border-border border-t px-1">
298-
Mint NFT
301+
Mint {props.isErc20 ? "Token" : "NFT"}
299302
</AccordionTrigger>
300303
<AccordionContent className="px-1">
301304
<MintNFTSection
@@ -835,7 +838,7 @@ function MintNFTSection(props: {
835838
name="quantity"
836839
render={({ field }) => (
837840
<FormItem className="flex-1">
838-
<FormLabel>quantity</FormLabel>
841+
<FormLabel>Quantity</FormLabel>
839842
<FormControl>
840843
<Input {...field} />
841844
</FormControl>

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

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ function RoyaltyModule(props: ModuleInstanceProps) {
6565
const setRoyaltyForTokenTx = setRoyaltyInfoForToken({
6666
contract: contract,
6767
recipient: values.recipient,
68-
bps: Number(values.bps),
68+
// BPS is 10_000 so we need to multiply by 100
69+
bps: Number(values.percentage) * 100,
6970
tokenId: BigInt(values.tokenId),
7071
});
7172

@@ -108,14 +109,14 @@ function RoyaltyModule(props: ModuleInstanceProps) {
108109
if (!ownerAccount) {
109110
throw new Error("Not an owner account");
110111
}
111-
const [defaultRoyaltyRecipient, defaultRoyaltyBps] =
112+
const [defaultRoyaltyRecipient, defaultRoyaltyPercentage] =
112113
defaultRoyaltyInfoQuery.data || [];
113114

114115
if (
115116
values.recipient &&
116-
values.bps &&
117+
values.percentage &&
117118
(values.recipient !== defaultRoyaltyRecipient ||
118-
Number(values.bps) !== defaultRoyaltyBps)
119+
Number(values.percentage) * 100 !== defaultRoyaltyPercentage)
119120
) {
120121
const setDefaultRoyaltyInfo = isErc721
121122
? RoyaltyERC721.setDefaultRoyaltyInfo
@@ -124,7 +125,7 @@ function RoyaltyModule(props: ModuleInstanceProps) {
124125
const setSaleConfigTx = setDefaultRoyaltyInfo({
125126
contract: contract,
126127
royaltyRecipient: values.recipient,
127-
royaltyBps: Number(values.bps),
128+
royaltyBps: Number(values.percentage) * 100,
128129
});
129130

130131
await sendAndConfirmTransaction({
@@ -250,10 +251,12 @@ const royaltyInfoFormSchema = z.object({
250251
}),
251252

252253
recipient: addressSchema,
253-
bps: z
254+
percentage: z
254255
.string()
255-
.min(1, { message: "Invalid BPS" })
256-
.refine((v) => Number(v) >= 0, { message: "Invalid BPS" }),
256+
.min(1, { message: "Invalid percentage" })
257+
.refine((v) => Number(v) === 0 || (Number(v) >= 0.01 && Number(v) <= 100), {
258+
message: "Invalid percentage",
259+
}),
257260
});
258261

259262
export type RoyaltyInfoFormValues = z.infer<typeof royaltyInfoFormSchema>;
@@ -267,7 +270,7 @@ function RoyaltyInfoPerTokenSection(props: {
267270
values: {
268271
tokenId: "",
269272
recipient: "",
270-
bps: "",
273+
percentage: "",
271274
},
272275
reValidateMode: "onChange",
273276
});
@@ -321,12 +324,17 @@ function RoyaltyInfoPerTokenSection(props: {
321324

322325
<FormField
323326
control={form.control}
324-
name="bps"
327+
name="percentage"
325328
render={({ field }) => (
326329
<FormItem>
327-
<FormLabel>BPS</FormLabel>
330+
<FormLabel>Percentage</FormLabel>
328331
<FormControl>
329-
<Input {...field} />
332+
<div className="flex items-center">
333+
<Input {...field} className="rounded-r-none border-r-0" />
334+
<div className="h-10 rounded-lg rounded-l-none border border-input px-3 py-2">
335+
%
336+
</div>
337+
</div>
330338
</FormControl>
331339
<FormMessage />
332340
</FormItem>
@@ -355,9 +363,12 @@ function RoyaltyInfoPerTokenSection(props: {
355363

356364
const defaultRoyaltyFormSchema = z.object({
357365
recipient: addressSchema,
358-
bps: z.string().refine((v) => v.length === 0 || Number(v) >= 0, {
359-
message: "Invalid BPS",
360-
}),
366+
percentage: z
367+
.string()
368+
.min(1, { message: "Invalid percentage" })
369+
.refine((v) => Number(v) === 0 || (Number(v) >= 0.01 && Number(v) <= 100), {
370+
message: "Invalid percentage",
371+
}),
361372
});
362373

363374
export type DefaultRoyaltyFormValues = z.infer<typeof defaultRoyaltyFormSchema>;
@@ -367,14 +378,16 @@ function DefaultRoyaltyInfoSection(props: {
367378
update: (values: DefaultRoyaltyFormValues) => Promise<void>;
368379
contractChainId: number;
369380
}) {
370-
const [defaultRoyaltyRecipient, defaultRoyaltyBps] =
381+
const [defaultRoyaltyRecipient, defaultRoyaltyPercentage] =
371382
props.defaultRoyaltyInfo || [];
372383

373384
const form = useForm<DefaultRoyaltyFormValues>({
374385
resolver: zodResolver(defaultRoyaltyFormSchema),
375386
values: {
376387
recipient: defaultRoyaltyRecipient || "",
377-
bps: defaultRoyaltyBps ? String(defaultRoyaltyBps) : "",
388+
percentage: defaultRoyaltyPercentage
389+
? String(defaultRoyaltyPercentage / 100)
390+
: "",
378391
},
379392
reValidateMode: "onChange",
380393
});
@@ -414,12 +427,17 @@ function DefaultRoyaltyInfoSection(props: {
414427

415428
<FormField
416429
control={form.control}
417-
name="bps"
430+
name="percentage"
418431
render={({ field }) => (
419432
<FormItem>
420-
<FormLabel>Default Royalty BPS</FormLabel>
433+
<FormLabel>Default Royalty Percentage</FormLabel>
421434
<FormControl>
422-
<Input {...field} />
435+
<div className="flex items-center">
436+
<Input {...field} className="rounded-r-none border-r-0" />
437+
<div className="h-10 rounded-lg rounded-l-none border border-input px-3 py-2">
438+
%
439+
</div>
440+
</div>
423441
</FormControl>
424442
<FormMessage />
425443
</FormItem>

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,18 +237,23 @@ export function TransferableModuleUI(
237237

238238
{isRestricted && (
239239
<div className="w-full">
240-
{/* Warning - TODO add later */}
241-
{/* {formFields.fields.length === 0 && (
240+
{formFields.fields.length === 0 && (
242241
<Alert variant="warning">
243242
<CircleAlertIcon className="size-5 max-sm:hidden" />
244243
<AlertTitle className="max-sm:!pl-0">
245244
Nobody has permission to transfer tokens on this
246245
contract
247246
</AlertTitle>
248247
</Alert>
249-
)} */}
248+
)}
250249

251250
<div className="flex flex-col gap-3">
251+
{formFields.fields.length > 0 && (
252+
<p className="text-muted-foreground text-sm">
253+
Accounts that may override the transfer restrictions
254+
</p>
255+
)}
256+
252257
{/* Addresses */}
253258
{formFields.fields.map((fieldItem, index) => (
254259
<div

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const claimCondition = {
6161
function Component() {
6262
const [isOwner, setIsOwner] = useState(true);
6363
const [isErc721, setIsErc721] = useState(false);
64+
const [isErc20, setIsErc20] = useState(false);
6465
const [isClaimConditionLoading, setIsClaimConditionLoading] = useState(false);
6566
const [isPrimarySaleRecipientLoading, setIsPrimarySaleRecipientLoading] =
6667
useState(false);
@@ -125,6 +126,13 @@ function Component() {
125126
label="isErc721"
126127
/>
127128

129+
<CheckboxWithLabel
130+
value={isErc20}
131+
onChange={setIsErc20}
132+
id="isErc20"
133+
label="isErc20"
134+
/>
135+
128136
<CheckboxWithLabel
129137
value={isClaimConditionLoading}
130138
onChange={setIsClaimConditionLoading}
@@ -179,6 +187,7 @@ function Component() {
179187
}}
180188
isOwnerAccount={isOwner}
181189
isErc721={isErc721}
190+
isErc20={isErc20}
182191
contractChainId={1}
183192
setTokenId={setTokenId}
184193
isValidTokenId={true}

apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { FormErrorMessage, FormLabel } from "tw-components";
66
import type { CustomContractDeploymentForm } from "./custom-contract";
77
import { PrimarySaleFieldset } from "./primary-sale-fieldset";
88
import { RoyaltyFieldset } from "./royalty-fieldset";
9+
import { SequentialTokenIdFieldset } from "./sequential-token-id-fieldset";
910

1011
export function getModuleInstallParams(mod: FetchDeployMetadataResult) {
1112
return (
@@ -67,6 +68,16 @@ function RenderModule(props: {
6768
<RenderPrimarySaleFieldset module={module} form={form} isTWPublisher />
6869
);
6970
}
71+
72+
if (showSequentialTokenIdFieldset(paramNames)) {
73+
return (
74+
<RenderSequentialTokenIdFieldset
75+
module={module}
76+
form={form}
77+
isTWPublisher
78+
/>
79+
);
80+
}
7081
}
7182

7283
return (
@@ -133,6 +144,26 @@ function RenderPrimarySaleFieldset(prosp: {
133144
);
134145
}
135146

147+
function RenderSequentialTokenIdFieldset(prosp: {
148+
module: FetchDeployMetadataResult;
149+
form: CustomContractDeploymentForm;
150+
isTWPublisher: boolean;
151+
}) {
152+
const { module, form } = prosp;
153+
154+
const startTokenIdPath = `moduleData.${module.name}.startTokenId` as const;
155+
156+
return (
157+
<SequentialTokenIdFieldset
158+
isInvalid={!!form.getFieldState(startTokenIdPath, form.formState).error}
159+
register={form.register(startTokenIdPath)}
160+
errorMessage={
161+
form.getFieldState(startTokenIdPath, form.formState).error?.message
162+
}
163+
/>
164+
);
165+
}
166+
136167
function RenderRoyaltyFieldset(props: {
137168
module: FetchDeployMetadataResult;
138169
form: CustomContractDeploymentForm;
@@ -194,3 +225,7 @@ export function showRoyaltyFieldset(paramNames: string[]) {
194225
export function showPrimarySaleFiedset(paramNames: string[]) {
195226
return paramNames.length === 1 && paramNames.includes("primarySaleRecipient");
196227
}
228+
229+
function showSequentialTokenIdFieldset(paramNames: string[]) {
230+
return paramNames.length === 1 && paramNames.includes("startTokenId");
231+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
2+
import { FormControl } from "@/components/ui/form";
3+
import { SolidityInput } from "contract-ui/components/solidity-inputs";
4+
import type { UseFormRegisterReturn } from "react-hook-form";
5+
6+
interface SequentialTokenIdFieldsetProps {
7+
isInvalid: boolean;
8+
register: UseFormRegisterReturn;
9+
errorMessage: string | undefined;
10+
}
11+
12+
export const SequentialTokenIdFieldset: React.FC<
13+
SequentialTokenIdFieldsetProps
14+
> = (props) => {
15+
return (
16+
<FormFieldSetup
17+
htmlFor="startTokenId"
18+
label="Start Token ID"
19+
isRequired={true}
20+
errorMessage={props.errorMessage}
21+
helperText="The starting token ID for the NFT collection."
22+
>
23+
<FormControl>
24+
<SolidityInput
25+
solidityType="uint256"
26+
variant="filled"
27+
{...props.register}
28+
/>
29+
</FormControl>
30+
</FormFieldSetup>
31+
);
32+
};

0 commit comments

Comments
 (0)