From 03617047e2ada113681cdd0062baeee3e9c61d7e Mon Sep 17 00:00:00 2001 From: MananTank Date: Tue, 29 Jul 2025 18:15:59 +0000 Subject: [PATCH] Dashboard: Migrate contract/permissions page from chakra to tailwind, UI improvements (#7750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on enhancing the `Permissions` component and related files by improving UI elements, restructuring code for clarity, and integrating better user feedback mechanisms such as alerts and toast notifications. ### Detailed summary - Updated the `Permissions` component's return structure to include headings and descriptions. - Refined the `alertVariants` in `alert.tsx` for better styling. - Replaced `ButtonGroup` with a `div` for layout adjustments in the `Permissions` component. - Enhanced error handling with toast notifications in `permissions-editor.tsx`. - Improved form handling and cleaned up values before submission. - Replaced `DelayedDisplay` with an `Alert` component in `permissions-editor.tsx`. - Updated the `ContractPermission` component to utilize `Select` for role management instead of traditional dropdowns. - Added conditional rendering for alerts based on role states in `ContractPermission`. - Adjusted button styles and behavior for better user experience across components. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../src/@/components/misc/delayed-display.tsx | 37 -- apps/dashboard/src/@/components/ui/alert.tsx | 6 +- .../permissions/ContractPermissionsPage.tsx | 12 +- .../components/contract-permission.tsx | 379 ++++++++---------- .../permissions/components/index.tsx | 101 +++-- .../components/permissions-editor.tsx | 271 ++++--------- 6 files changed, 320 insertions(+), 486 deletions(-) delete mode 100644 apps/dashboard/src/@/components/misc/delayed-display.tsx diff --git a/apps/dashboard/src/@/components/misc/delayed-display.tsx b/apps/dashboard/src/@/components/misc/delayed-display.tsx deleted file mode 100644 index 05a3c3f17ad..00000000000 --- a/apps/dashboard/src/@/components/misc/delayed-display.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useState } from "react"; -import type { ComponentWithChildren } from "@/types/component-with-children"; - -interface DelayedDisplayProps { - delay: number; -} - -function useDelayedDisplay(delay: number) { - const [displayContent, setDisplayContent] = useState(false); - - // FIXME: this is a weird thing, we should not need it - in the meantime this is a legit use case - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - const timer = setTimeout(() => { - setDisplayContent(true); - }, delay); - - return () => { - clearTimeout(timer); - }; - }, [delay]); - - return displayContent; -} - -export const DelayedDisplay: ComponentWithChildren = ({ - delay, - children, -}) => { - const displayContent = useDelayedDisplay(delay); - - if (!displayContent) { - return null; - } - - return <>{children}; -}; diff --git a/apps/dashboard/src/@/components/ui/alert.tsx b/apps/dashboard/src/@/components/ui/alert.tsx index f574853b564..2ea180d4c35 100644 --- a/apps/dashboard/src/@/components/ui/alert.tsx +++ b/apps/dashboard/src/@/components/ui/alert.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; const alertVariants = cva( - "bg-card relative w-full rounded-lg border border-border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + "bg-card relative w-full rounded-xl border border-border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", { defaultVariants: { variant: "default", @@ -38,7 +38,7 @@ const AlertTitle = React.forwardRef< React.HTMLAttributes >(({ className, ...props }, ref) => (
@@ -51,7 +51,7 @@ const AlertDescription = React.forwardRef< >(({ className, ...props }, ref) => (
; + return ( +
+

+ Permissions +

+

+ View and manage the permissions for this contract +

+ +
+ ); } diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/contract-permission.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/contract-permission.tsx index 30b55392fb1..b87af4dd65b 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/contract-permission.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/contract-permission.tsx @@ -1,9 +1,17 @@ -import { Flex, Select, Spinner } from "@chakra-ui/react"; import { InfoIcon } from "lucide-react"; import { useFormContext } from "react-hook-form"; import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; -import { Card } from "@/components/ui/card"; +import { Alert, AlertTitle } from "@/components/ui/alert"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Skeleton } from "@/components/ui/skeleton"; import { useIsAdmin } from "@/hooks/useContractRoles"; +import { cn } from "@/lib/utils"; import { PermissionEditor } from "./permissions-editor"; interface ContractPermissionProps { @@ -19,227 +27,178 @@ export const ContractPermission: React.FC = ({ isPending, contract, }) => { - const { - watch, - setValue, - formState: { isSubmitting }, - } = useFormContext(); + const form = useFormContext(); - const roleMembers: string[] = watch()?.[role] || []; + const roleMembers: string[] = form.watch(role) || []; const isRestricted = !roleMembers.includes(ZERO_ADDRESS) || (role !== "transfer" && role !== "lister" && role !== "asset"); const isAdmin = useIsAdmin(contract); return ( - - -
-
-
-

- {role === "minter" ? "Minter / Creator" : role} -

-

{description}

-
+
+ {/* header */} +
+ {/* left */} +
+

+ {role === "minter" ? "Minter / Creator" : role} +

+

{description}

+
- {role === "transfer" && ( - - {isPending || isSubmitting ? ( - - -

{isPending ? "Loading ..." : "Updating ..."}

-
- ) : ( - - )} -
- )} + {/* right */} + {role === "transfer" && isAdmin && ( + + )} - {role === "lister" && ( - - {isPending || isSubmitting ? ( - - -

{isPending ? "Loading ..." : "Updating ..."}

-
- ) : ( - - )} -
- )} + {/* right */} + {role === "lister" && isAdmin && ( + + )} - {role === "asset" && ( - - {isPending || isSubmitting ? ( - - -

{isPending ? "Loading ..." : "Updating ..."}

-
- ) : ( - - )} -
- )} -
+ {/* right */} + {role === "asset" && isAdmin && ( + + )} +
- {role === "transfer" && ( - - -

- {isRestricted ? ( - <> - The tokens in this contract are currently{" "} - non-transferable. Only wallets that you - explicitly add to the list below will be able to transfer - tokens. - - ) : ( - <> - Transferring tokens in this contract is currently{" "} - not restricted. Everyone is free to - transfer their tokens. - - )} -

-
- )} +
+ {/* alert */} + {role === "transfer" && ( + + + + {isRestricted ? ( + <> + The tokens in this contract are currently{" "} + non-transferable. Only wallets that you + explicitly add to the list below will be able to transfer + tokens + + ) : ( + <> + Transferring tokens in this contract is currently{" "} + not restricted. Everyone is free to transfer + their tokens + + )} + + + )} - {role === "lister" && ( - - -

- {isRestricted - ? "Currently, only addresses specified below will be able to create listings on this marketplace." - : "This marketplace is open for anyone to create listings."} -

-
- )} + {/* alert */} + {role === "lister" && ( + + + + {isRestricted + ? "Currently, only addresses specified below will be able to create listings on this marketplace." + : "This marketplace is open for anyone to create listings."} + + + )} - {role === "asset" && ( - - -

- {isRestricted - ? "Currently, only assets from the contracts specified below will be able to be used on this contract." - : "This contract is open for people to list assets from any contract."} -

-
- )} + {/* alert */} + {role === "asset" && ( + + + + {isRestricted + ? "Currently, only assets from the contracts specified below will be able to be used on this contract." + : "This contract is open for people to list assets from any contract."} + + + )} - {isPending ? ( - - ) : ( - isRestricted && - role && - )} -
- - + {/* content */} + {isPending ? ( + + ) : ( + isRestricted && + role && ( + + ) + )} +
+
); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/index.tsx index 34721354027..061bb5d4036 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/index.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/index.tsx @@ -1,9 +1,9 @@ "use client"; -import { ButtonGroup, Flex } from "@chakra-ui/react"; -import { Button } from "chakra/button"; +import { RotateCcwIcon } from "lucide-react"; import { useMemo } from "react"; -import { FormProvider, useForm } from "react-hook-form"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; import type { ThirdwebContract } from "thirdweb"; import { useActiveAccount, @@ -11,12 +11,14 @@ import { useSendAndConfirmTransaction, } from "thirdweb/react"; import { TransactionButton } from "@/components/tx-button"; +import { Button } from "@/components/ui/button"; +import { Form } from "@/components/ui/form"; import { ROLE_DESCRIPTION_MAP } from "@/constants/mappings"; import { createSetAllRoleMembersTx, getAllRoleMembers, } from "@/hooks/contract-ui/permissions"; -import { useTxNotifications } from "@/hooks/useTxNotifications"; +import { parseError } from "@/utils/errorParser"; import { ContractPermission } from "./contract-permission"; type PermissionFormContext = { @@ -48,78 +50,91 @@ export function Permissions({ values: transformedQueryData, }); - const { onSuccess, onError } = useTxNotifications( - "Permissions updated", - "Failed to update permissions", - ); - const roles = useMemo(() => { return Object.keys(allRoleMembers.data || ROLE_DESCRIPTION_MAP); }, [allRoleMembers.data]); return ( - - { +
+ { if (!account) { - onError(new Error("Wallet not connected!")); + toast.error("Wallet not connected!"); return; } + + // remove empty values + const cleanedValues: { [k: string]: string[] } = {}; + + for (const k in values) { + const originalValue = values[k]; + if (originalValue) { + const cleanedValue: string[] = []; + for (const v of originalValue) { + if (v !== "") { + cleanedValue.push(v); + } + } + cleanedValues[k] = cleanedValue; + } + } + const tx = createSetAllRoleMembersTx({ account, contract, - roleMemberMap: d, + roleMemberMap: cleanedValues, }); + sendTx.mutate(tx, { onError: (error) => { - onError(error); + toast.error("Failed to update permissions", { + description: parseError(error), + }); }, onSuccess: () => { - form.reset(d); - onSuccess(); + toast.success("Permissions updated successfully"); }, }); })} > - {roles.map((role) => { - return ( - - ); - })} - +
+ {roles.map((role) => { + return ( + + ); + })} +
+ +
{sendTx.isPending ? "Updating permissions" : "Update permissions"} - - - +
+ + ); } diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/permissions-editor.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/permissions-editor.tsx index 07a94e83cb5..f39f3c49b48 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/permissions-editor.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/permissions/components/permissions-editor.tsx @@ -1,222 +1,109 @@ -import { - Flex, - FormControl, - IconButton, - Input, - InputGroup, - InputLeftAddon, - InputRightAddon, -} from "@chakra-ui/react"; -import { Button } from "chakra/button"; -import { FormErrorMessage } from "chakra/form"; -import { Text } from "chakra/text"; -import { - ClipboardPasteIcon, - CopyIcon, - InfoIcon, - PlusIcon, - TrashIcon, -} from "lucide-react"; -import { useState } from "react"; +import { InfoIcon, PlusIcon, XIcon } from "lucide-react"; import { useFieldArray, useFormContext } from "react-hook-form"; -import { toast } from "sonner"; -import { isAddress, type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; -import { - AdminOnly, - AdminOrSelfOnly, -} from "@/components/contracts/roles/admin-only"; -import { DelayedDisplay } from "@/components/misc/delayed-display"; -import { ToolTipLabel } from "@/components/ui/tooltip"; -import { useClipboard } from "@/hooks/useClipboard"; +import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; +import { AdminOrSelfOnly } from "@/components/contracts/roles/admin-only"; +import { Alert, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; -interface PermissionEditorProps { - role: string; - contract: ThirdwebContract; -} - -export const PermissionEditor: React.FC = ({ +export const PermissionEditor = ({ role, contract, + isUserAdmin, +}: { + role: string; + contract: ThirdwebContract; + isUserAdmin: boolean; }) => { - const { - control, - watch, - formState: { isSubmitting }, - } = useFormContext(); - const { fields, append, remove } = useFieldArray({ - control, + const form = useFormContext(); + const formFields = useFieldArray({ + control: form.control, name: role, }); - const [address, setAddress] = useState(""); - - const members = watch(role) || []; - const isDisabled = !isAddress(address) || members.includes(address); - - const addAddress = () => { - if (isDisabled) { - return; - } - - append(address); - setAddress(""); - }; return ( -
- {!fields?.length && ( - -
- - - {role === "asset" - ? "No asset contracts are permitted to be listed on this marketplace." - : "Nobody has this permission for this contract."} - -
-
+
+ {formFields.fields.length === 0 && ( + + + + {role === "asset" + ? "No asset contracts are permitted to be listed on this marketplace" + : "Nobody has this permission for this contract"} + + )} - {fields?.map((field, index) => ( - remove(index)} - role={role} - /> - ))} - - - - - - } - onClick={() => { - navigator.clipboard - .readText() - .then((text) => { - setAddress(text); - return void 0; - }) - .catch((error) => { - console.error(error); - toast.error("Failed to paste from clipboard"); - }); - }} - width="100%" - /> - - - setAddress(e.target.value)} - placeholder={ZERO_ADDRESS} - px={2} - value={address} - variant="filled" + + {formFields.fields.length > 0 && ( +
+ {formFields.fields?.map((field, index) => ( + formFields.remove(index)} + role={role} + index={index} + isUserAdmin={isUserAdmin} /> - + ))} + + {isUserAdmin && ( +
- - - - {members.includes(address) - ? "Address already has this role" - : !isAddress(address) - ? "Not a valid address" - : ""} - - - +
+ )} +
+ )}
); }; -interface PermissionAddressProps { +const PermissionAddress = ({ + member, + removeAddress, + isSubmitting, + contract, + index, + role, + isUserAdmin, +}: { role: string; member: string; removeAddress: () => void; isSubmitting: boolean; contract: ThirdwebContract; -} - -const PermissionAddress: React.FC = ({ - member, - removeAddress, - isSubmitting, - contract, + index: number; + isUserAdmin: boolean; }) => { - const { onCopy } = useClipboard(member); + const form = useFormContext(); return ( - - - - - } - onClick={(e) => { - e.stopPropagation(); - e.preventDefault(); - onCopy(); - toast.info("Address copied."); - }} - width="100%" - /> - - - - - - - - - - +
+ + + + +
); };