Skip to content

Commit

Permalink
[Dashboard] Feature: Modules UI changes (#5437)
Browse files Browse the repository at this point in the history
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 -->
  • Loading branch information
GWSzeto committed Nov 25, 2024
1 parent 0e98b09 commit a98550d
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
const [tokenId, setTokenId] = useState<string>("");

const isErc721 = props.contractInfo.name === "ClaimableERC721";
const isErc20 = props.contractInfo.name === "ClaimableERC20";
const isValidTokenId = positiveIntegerRegex.test(tokenId);

const primarySaleRecipientQuery = useReadContract(
Expand Down Expand Up @@ -241,6 +242,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
}}
isOwnerAccount={!!ownerAccount}
isErc721={isErc721}
isErc20={isErc20}
contractChainId={props.contract.chain.id}
setTokenId={setTokenId}
isValidTokenId={isValidTokenId}
Expand All @@ -256,6 +258,7 @@ export function ClaimableModuleUI(
props: Omit<ModuleCardUIProps, "children" | "updateButton"> & {
isOwnerAccount: boolean;
isErc721: boolean;
isErc20: boolean;
contractChainId: number;
setTokenId: Dispatch<SetStateAction<string>>;
isValidTokenId: boolean;
Expand Down Expand Up @@ -295,7 +298,7 @@ export function ClaimableModuleUI(
<Accordion type="single" collapsible className="-mx-1">
<AccordionItem value="metadata" className="border-none">
<AccordionTrigger className="border-border border-t px-1">
Mint NFT
Mint {props.isErc20 ? "Token" : "NFT"}
</AccordionTrigger>
<AccordionContent className="px-1">
<MintNFTSection
Expand Down Expand Up @@ -835,7 +838,7 @@ function MintNFTSection(props: {
name="quantity"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>quantity</FormLabel>
<FormLabel>Quantity</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ function RoyaltyModule(props: ModuleInstanceProps) {
const setRoyaltyForTokenTx = setRoyaltyInfoForToken({
contract: contract,
recipient: values.recipient,
bps: Number(values.bps),
// BPS is 10_000 so we need to multiply by 100
bps: Number(values.percentage) * 100,
tokenId: BigInt(values.tokenId),
});

Expand Down Expand Up @@ -108,14 +109,14 @@ function RoyaltyModule(props: ModuleInstanceProps) {
if (!ownerAccount) {
throw new Error("Not an owner account");
}
const [defaultRoyaltyRecipient, defaultRoyaltyBps] =
const [defaultRoyaltyRecipient, defaultRoyaltyPercentage] =
defaultRoyaltyInfoQuery.data || [];

if (
values.recipient &&
values.bps &&
values.percentage &&
(values.recipient !== defaultRoyaltyRecipient ||
Number(values.bps) !== defaultRoyaltyBps)
Number(values.percentage) * 100 !== defaultRoyaltyPercentage)
) {
const setDefaultRoyaltyInfo = isErc721
? RoyaltyERC721.setDefaultRoyaltyInfo
Expand All @@ -124,7 +125,7 @@ function RoyaltyModule(props: ModuleInstanceProps) {
const setSaleConfigTx = setDefaultRoyaltyInfo({
contract: contract,
royaltyRecipient: values.recipient,
royaltyBps: Number(values.bps),
royaltyBps: Number(values.percentage) * 100,
});

await sendAndConfirmTransaction({
Expand Down Expand Up @@ -250,10 +251,12 @@ const royaltyInfoFormSchema = z.object({
}),

recipient: addressSchema,
bps: z
percentage: z
.string()
.min(1, { message: "Invalid BPS" })
.refine((v) => Number(v) >= 0, { message: "Invalid BPS" }),
.min(1, { message: "Invalid percentage" })
.refine((v) => Number(v) === 0 || (Number(v) >= 0.01 && Number(v) <= 100), {
message: "Invalid percentage",
}),
});

export type RoyaltyInfoFormValues = z.infer<typeof royaltyInfoFormSchema>;
Expand All @@ -267,7 +270,7 @@ function RoyaltyInfoPerTokenSection(props: {
values: {
tokenId: "",
recipient: "",
bps: "",
percentage: "",
},
reValidateMode: "onChange",
});
Expand Down Expand Up @@ -321,12 +324,17 @@ function RoyaltyInfoPerTokenSection(props: {

<FormField
control={form.control}
name="bps"
name="percentage"
render={({ field }) => (
<FormItem>
<FormLabel>BPS</FormLabel>
<FormLabel>Percentage</FormLabel>
<FormControl>
<Input {...field} />
<div className="flex items-center">
<Input {...field} className="rounded-r-none border-r-0" />
<div className="h-10 rounded-lg rounded-l-none border border-input px-3 py-2">
%
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
Expand Down Expand Up @@ -355,9 +363,12 @@ function RoyaltyInfoPerTokenSection(props: {

const defaultRoyaltyFormSchema = z.object({
recipient: addressSchema,
bps: z.string().refine((v) => v.length === 0 || Number(v) >= 0, {
message: "Invalid BPS",
}),
percentage: z
.string()
.min(1, { message: "Invalid percentage" })
.refine((v) => Number(v) === 0 || (Number(v) >= 0.01 && Number(v) <= 100), {
message: "Invalid percentage",
}),
});

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

const form = useForm<DefaultRoyaltyFormValues>({
resolver: zodResolver(defaultRoyaltyFormSchema),
values: {
recipient: defaultRoyaltyRecipient || "",
bps: defaultRoyaltyBps ? String(defaultRoyaltyBps) : "",
percentage: defaultRoyaltyPercentage
? String(defaultRoyaltyPercentage / 100)
: "",
},
reValidateMode: "onChange",
});
Expand Down Expand Up @@ -414,12 +427,17 @@ function DefaultRoyaltyInfoSection(props: {

<FormField
control={form.control}
name="bps"
name="percentage"
render={({ field }) => (
<FormItem>
<FormLabel>Default Royalty BPS</FormLabel>
<FormLabel>Default Royalty Percentage</FormLabel>
<FormControl>
<Input {...field} />
<div className="flex items-center">
<Input {...field} className="rounded-r-none border-r-0" />
<div className="h-10 rounded-lg rounded-l-none border border-input px-3 py-2">
%
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,18 +237,23 @@ export function TransferableModuleUI(

{isRestricted && (
<div className="w-full">
{/* Warning - TODO add later */}
{/* {formFields.fields.length === 0 && (
{formFields.fields.length === 0 && (
<Alert variant="warning">
<CircleAlertIcon className="size-5 max-sm:hidden" />
<AlertTitle className="max-sm:!pl-0">
Nobody has permission to transfer tokens on this
contract
</AlertTitle>
</Alert>
)} */}
)}

<div className="flex flex-col gap-3">
{formFields.fields.length > 0 && (
<p className="text-muted-foreground text-sm">
Accounts that may override the transfer restrictions
</p>
)}

{/* Addresses */}
{formFields.fields.map((fieldItem, index) => (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const claimCondition = {
function Component() {
const [isOwner, setIsOwner] = useState(true);
const [isErc721, setIsErc721] = useState(false);
const [isErc20, setIsErc20] = useState(false);
const [isClaimConditionLoading, setIsClaimConditionLoading] = useState(false);
const [isPrimarySaleRecipientLoading, setIsPrimarySaleRecipientLoading] =
useState(false);
Expand Down Expand Up @@ -125,6 +126,13 @@ function Component() {
label="isErc721"
/>

<CheckboxWithLabel
value={isErc20}
onChange={setIsErc20}
id="isErc20"
label="isErc20"
/>

<CheckboxWithLabel
value={isClaimConditionLoading}
onChange={setIsClaimConditionLoading}
Expand Down Expand Up @@ -179,6 +187,7 @@ function Component() {
}}
isOwnerAccount={isOwner}
isErc721={isErc721}
isErc20={isErc20}
contractChainId={1}
setTokenId={setTokenId}
isValidTokenId={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FormErrorMessage, FormLabel } from "tw-components";
import type { CustomContractDeploymentForm } from "./custom-contract";
import { PrimarySaleFieldset } from "./primary-sale-fieldset";
import { RoyaltyFieldset } from "./royalty-fieldset";
import { SequentialTokenIdFieldset } from "./sequential-token-id-fieldset";

export function getModuleInstallParams(mod: FetchDeployMetadataResult) {
return (
Expand Down Expand Up @@ -67,6 +68,16 @@ function RenderModule(props: {
<RenderPrimarySaleFieldset module={module} form={form} isTWPublisher />
);
}

if (showSequentialTokenIdFieldset(paramNames)) {
return (
<RenderSequentialTokenIdFieldset
module={module}
form={form}
isTWPublisher
/>
);
}
}

return (
Expand Down Expand Up @@ -133,6 +144,26 @@ function RenderPrimarySaleFieldset(prosp: {
);
}

function RenderSequentialTokenIdFieldset(prosp: {
module: FetchDeployMetadataResult;
form: CustomContractDeploymentForm;
isTWPublisher: boolean;
}) {
const { module, form } = prosp;

const startTokenIdPath = `moduleData.${module.name}.startTokenId` as const;

return (
<SequentialTokenIdFieldset
isInvalid={!!form.getFieldState(startTokenIdPath, form.formState).error}
register={form.register(startTokenIdPath)}
errorMessage={
form.getFieldState(startTokenIdPath, form.formState).error?.message
}
/>
);
}

function RenderRoyaltyFieldset(props: {
module: FetchDeployMetadataResult;
form: CustomContractDeploymentForm;
Expand Down Expand Up @@ -194,3 +225,7 @@ export function showRoyaltyFieldset(paramNames: string[]) {
export function showPrimarySaleFiedset(paramNames: string[]) {
return paramNames.length === 1 && paramNames.includes("primarySaleRecipient");
}

function showSequentialTokenIdFieldset(paramNames: string[]) {
return paramNames.length === 1 && paramNames.includes("startTokenId");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { FormControl } from "@/components/ui/form";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import type { UseFormRegisterReturn } from "react-hook-form";

interface SequentialTokenIdFieldsetProps {
isInvalid: boolean;
register: UseFormRegisterReturn;
errorMessage: string | undefined;
}

export const SequentialTokenIdFieldset: React.FC<
SequentialTokenIdFieldsetProps
> = (props) => {
return (
<FormFieldSetup
htmlFor="startTokenId"
label="Start Token ID"
isRequired={true}
errorMessage={props.errorMessage}
helperText="The starting token ID for the NFT collection."
>
<FormControl>
<SolidityInput
solidityType="uint256"
variant="filled"
{...props.register}
/>
</FormControl>
</FormFieldSetup>
);
};

0 comments on commit a98550d

Please sign in to comment.