From a98550d229e7fd124e1977fcc64dbd1b602ce656 Mon Sep 17 00:00:00 2001 From: GWSzeto Date: Mon, 25 Nov 2024 23:06:58 +0000 Subject: [PATCH] [Dashboard] Feature: Modules UI changes (#5437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://linear.app/thirdweb/issue/DASH-469/small-changes-for-modules-ui --- ## 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}` --- .../modules/components/Claimable.tsx | 7 ++- .../modules/components/Royalty.tsx | 58 ++++++++++++------- .../modules/components/Transferable.tsx | 11 +++- .../modules/components/claimable.stories.tsx | 9 +++ ...ular-contract-default-modules-fieldset.tsx | 35 +++++++++++ .../sequential-token-id-fieldset.tsx | 32 ++++++++++ 6 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 apps/dashboard/src/components/contract-components/contract-deploy-form/sequential-token-id-fieldset.tsx diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx index abf3e976214..92e1a50c008 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Claimable.tsx @@ -73,6 +73,7 @@ function ClaimableModule(props: ModuleInstanceProps) { const [tokenId, setTokenId] = useState(""); const isErc721 = props.contractInfo.name === "ClaimableERC721"; + const isErc20 = props.contractInfo.name === "ClaimableERC20"; const isValidTokenId = positiveIntegerRegex.test(tokenId); const primarySaleRecipientQuery = useReadContract( @@ -241,6 +242,7 @@ function ClaimableModule(props: ModuleInstanceProps) { }} isOwnerAccount={!!ownerAccount} isErc721={isErc721} + isErc20={isErc20} contractChainId={props.contract.chain.id} setTokenId={setTokenId} isValidTokenId={isValidTokenId} @@ -256,6 +258,7 @@ export function ClaimableModuleUI( props: Omit & { isOwnerAccount: boolean; isErc721: boolean; + isErc20: boolean; contractChainId: number; setTokenId: Dispatch>; isValidTokenId: boolean; @@ -295,7 +298,7 @@ export function ClaimableModuleUI( - Mint NFT + Mint {props.isErc20 ? "Token" : "NFT"} ( - quantity + Quantity diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Royalty.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Royalty.tsx index 2e7ffdda13e..d158f504367 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Royalty.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Royalty.tsx @@ -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), }); @@ -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 @@ -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({ @@ -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; @@ -267,7 +270,7 @@ function RoyaltyInfoPerTokenSection(props: { values: { tokenId: "", recipient: "", - bps: "", + percentage: "", }, reValidateMode: "onChange", }); @@ -321,12 +324,17 @@ function RoyaltyInfoPerTokenSection(props: { ( - BPS + Percentage - +
+ +
+ % +
+
@@ -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; @@ -367,14 +378,16 @@ function DefaultRoyaltyInfoSection(props: { update: (values: DefaultRoyaltyFormValues) => Promise; contractChainId: number; }) { - const [defaultRoyaltyRecipient, defaultRoyaltyBps] = + const [defaultRoyaltyRecipient, defaultRoyaltyPercentage] = props.defaultRoyaltyInfo || []; const form = useForm({ resolver: zodResolver(defaultRoyaltyFormSchema), values: { recipient: defaultRoyaltyRecipient || "", - bps: defaultRoyaltyBps ? String(defaultRoyaltyBps) : "", + percentage: defaultRoyaltyPercentage + ? String(defaultRoyaltyPercentage / 100) + : "", }, reValidateMode: "onChange", }); @@ -414,12 +427,17 @@ function DefaultRoyaltyInfoSection(props: { ( - Default Royalty BPS + Default Royalty Percentage - +
+ +
+ % +
+
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Transferable.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Transferable.tsx index b9f06d50c85..b24d0c1c838 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Transferable.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Transferable.tsx @@ -237,8 +237,7 @@ export function TransferableModuleUI( {isRestricted && (
- {/* Warning - TODO add later */} - {/* {formFields.fields.length === 0 && ( + {formFields.fields.length === 0 && ( @@ -246,9 +245,15 @@ export function TransferableModuleUI( contract - )} */} + )}
+ {formFields.fields.length > 0 && ( +

+ Accounts that may override the transfer restrictions +

+ )} + {/* Addresses */} {formFields.fields.map((fieldItem, index) => (
+ + ); } + + if (showSequentialTokenIdFieldset(paramNames)) { + return ( + + ); + } } return ( @@ -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 ( + + ); +} + function RenderRoyaltyFieldset(props: { module: FetchDeployMetadataResult; form: CustomContractDeploymentForm; @@ -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"); +} diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/sequential-token-id-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-deploy-form/sequential-token-id-fieldset.tsx new file mode 100644 index 00000000000..5cc16325642 --- /dev/null +++ b/apps/dashboard/src/components/contract-components/contract-deploy-form/sequential-token-id-fieldset.tsx @@ -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 ( + + + + + + ); +};