From 67abd0db5b671431a34ec32d368bb3c1ab60cbf5 Mon Sep 17 00:00:00 2001 From: MananTank Date: Sat, 26 Jul 2025 19:15:41 +0000 Subject: [PATCH] Dashboard: Migrate engine/contract-submissions from chakra to tailwind (#7721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on refactoring the `EngineContractSubscriptions` and `ContractSubscriptionTable` components to improve their structure, style, and functionality. It also enhances the user experience by updating UI elements and integrating new components. ### Detailed summary - Updated `className` in the `input` component to use `selection:bg-inverted`. - Refactored `EngineContractSubscriptions` to use `div` instead of `Flex` for layout. - Integrated `UnderlineLink` for better link styling. - Changed `Switch` component's props from `isChecked` to `checked` and `onChange` to `onCheckedChange`. - Replaced `Flex` with `div` for various elements to improve layout consistency. - Enhanced `ContractSubscriptionTable` with new styling and improved event handling. - Updated modal components to use `Dialog` instead of `Modal`. - Improved error handling and user notifications using `toast` for actions like adding and removing subscriptions. - Refactored form handling with `react-hook-form` and `zod` for validation. - Updated `FilterSelector` to use `MultiSelect` for better user interaction. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Improved contract subscription management with a redesigned multi-step form, enhanced validation, and more user-friendly filtering controls. * Updated contract subscriptions table with improved modals and tooltips for better usability. * **Refactor** * Replaced Chakra UI components with a custom UI library for dialogs, forms, tables, tooltips, and switches. * Enhanced visual styling using Tailwind CSS classes. * Streamlined error notifications and form validation for a more consistent user experience. * **Style** * Updated selection styling for input fields to improve appearance. --- apps/dashboard/src/@/components/ui/input.tsx | 2 +- .../add-contract-subscription-button.tsx | 703 +++++++++--------- .../contract-subscriptions-table.tsx | 435 ++++++----- .../engine-contract-subscription.tsx | 73 +- 4 files changed, 611 insertions(+), 602 deletions(-) diff --git a/apps/dashboard/src/@/components/ui/input.tsx b/apps/dashboard/src/@/components/ui/input.tsx index 12ff2495f7f..dd260dee640 100644 --- a/apps/dashboard/src/@/components/ui/input.tsx +++ b/apps/dashboard/src/@/components/ui/input.tsx @@ -10,7 +10,7 @@ const Input = React.forwardRef( return ( isAddress(val), "Invalid contract address"), + webhookUrl: z + .string() + .min(1, "Webhook URL is required") + .url("Invalid URL format"), + processEventLogs: z.boolean(), + filterEvents: z.array(z.string()), + processTransactionReceipts: z.boolean(), + filterFunctions: z.array(z.string()), +}); + +type AddContractSubscriptionForm = z.infer< + typeof addContractSubscriptionSchema +>; + +export function AddContractSubscriptionButton({ + instanceUrl, + authToken, + client, +}: { instanceUrl: string; authToken: string; client: ThirdwebClient; -} - -export const AddContractSubscriptionButton: React.FC< - AddContractSubscriptionButtonProps -> = ({ instanceUrl, authToken, client }) => { - const disclosure = useDisclosure(); +}) { + const [isOpen, setIsOpen] = useState(false); return ( - <> - - - {disclosure.isOpen && ( - + + + + + + - )} - + + ); -}; - -interface AddContractSubscriptionForm { - chainId: number; - contractAddress: string; - webhookUrl: string; - processEventLogs: boolean; - filterEvents: string[]; - processTransactionReceipts: boolean; - filterFunctions: string[]; } -const AddModal = ({ +function AddModalContent({ instanceUrl, - disclosure, authToken, client, + setIsOpen, }: { instanceUrl: string; - disclosure: UseDisclosureReturn; authToken: string; client: ThirdwebClient; -}) => { - const { mutate: addContractSubscription } = useEngineAddContractSubscription({ + setIsOpen: Dispatch>; +}) { + const addContractSubscription = useEngineAddContractSubscription({ authToken, instanceUrl, }); - const { onSuccess, onError } = useTxNotifications( - "Created Contract Subscription.", - "Failed to create Contract Subscription.", - ); const [modalState, setModalState] = useState<"inputContract" | "inputData">( "inputContract", ); const form = useForm({ + resolver: zodResolver(addContractSubscriptionSchema), defaultValues: { chainId: 84532, filterEvents: [], @@ -128,51 +139,62 @@ const AddModal = ({ webhookUrl: data.webhookUrl.trim(), }; - addContractSubscription(input, { + addContractSubscription.mutate(input, { onError: (error) => { - onError(error); + toast.error("Failed to create Contract Subscription.", { + description: parseError(error), + }); console.error(error); }, onSuccess: () => { - onSuccess(); - disclosure.onClose(); + toast.success("Created Contract Subscription."); + setIsOpen(false); }, }); }; return ( - - - - Add Contract Subscription - - - {modalState === "inputContract" ? ( - - ) : modalState === "inputData" ? ( - - ) : null} - - +
+ + Add Contract Subscription + {modalState === "inputContract" && ( + + Add a contract subscription to process real-time onchain data. + + )} + + {modalState === "inputData" && ( + + Select the data type to process. +
+ Events logs are arbitrary data triggered by a smart contract call. +
+ Transaction receipts contain details about the blockchain execution. +
+ )} +
+ +
+ + {modalState === "inputContract" ? ( + + ) : modalState === "inputData" ? ( + + ) : null} + + +
); -}; +} const ModalBodyInputContract = ({ form, @@ -184,89 +206,82 @@ const ModalBodyInputContract = ({ client: ThirdwebClient; }) => { return ( - <> - -
- - Add a contract subscription to process real-time onchain data. - +
+
+ ( + + Chain + + field.onChange(val)} + className="bg-card" + /> + + + + )} + /> - - Chain - form.setValue("chainId", val)} - /> - - - - Contract Address - { - const isValid = isAddress(v); - return !isValid ? "Invalid address" : true; - }, - })} - /> - - { - form.getFieldState("contractAddress", form.formState).error - ?.message - } - - - - - Webhook URL - { - try { - new URL(v); - return true; - } catch { - return "Invalid URL"; - } - }, - })} - /> - - Engine sends an HTTP request to your backend when new onchain data - for this contract is detected. - - - {form.getFieldState("webhookUrl", form.formState).error?.message} - - -
- + ( + + Contract Address + + + + + + )} + /> - + ( + + Webhook URL + + + + + Engine sends an HTTP request to your backend when new onchain + data for this contract is detected. + + + + )} + /> +
+ +
- - +
+
); }; @@ -274,17 +289,18 @@ const ModalBodyInputData = ({ form, setModalState, client, + isAdding, }: { form: UseFormReturn; setModalState: Dispatch>; client: ThirdwebClient; + isAdding: boolean; }) => { - const processEventLogsDisclosure = useDisclosure({ - defaultIsOpen: form.getValues("processEventLogs"), - }); - const processTransactionReceiptsDisclosure = useDisclosure({ - defaultIsOpen: form.getValues("processTransactionReceipts"), - }); + const [processEventLogsOpen, setProcessEventLogsOpen] = useState( + form.getValues("processEventLogs"), + ); + const [processTransactionReceiptsOpen, setProcessTransactionReceiptsOpen] = + useState(form.getValues("processTransactionReceipts")); const [shouldFilterEvents, setShouldFilterEvents] = useState(false); const [shouldFilterFunctions, setShouldFilterFunctions] = useState(false); @@ -305,156 +321,158 @@ const ModalBodyInputData = ({ filterFunctions.length === 0); return ( - <> - -
- - Select the data type to process. -
- Events logs are arbitrary data triggered by a smart contract call. -
- Transaction receipts contain details about the blockchain execution. -
- - - Processed Data - -
- - { - const checked = !!val; - form.setValue("processEventLogs", checked); - if (checked) { - processEventLogsDisclosure.onOpen(); - } else { - processEventLogsDisclosure.onClose(); - } - }} - /> - Event Logs - - {/* Shows all/specific events if processing event logs */} - -
- { - if (val === "true") { - setShouldFilterEvents(true); - } else { - setShouldFilterEvents(false); - form.setValue("filterEvents", []); +
+
+
+ + { + const checked = !!val; + form.setValue("processEventLogs", checked); + setProcessEventLogsOpen(checked); + }} + /> + Event Logs + + + {/* Shows all/specific events if processing event logs */} + {processEventLogsOpen && ( +
+ { + if (val === "true") { + setShouldFilterEvents(true); + } else { + setShouldFilterEvents(false); + form.setValue("filterEvents", []); + } + }} + > +
+
+ + +
+
+ + +
+ + {/* List event names to select */} + {shouldFilterEvents && ( + + form.setValue("filterEvents", value) } - }} - > -
- - All events - - - - Specific events{" "} - {!!filterEvents.length && - `(${filterEvents.length} selected)`} - - - {/* List event names to select */} - - - form.setValue("filterEvents", value) - } - /> - -
- + /> + )}
- - - - { - const checked = !!val; - form.setValue("processTransactionReceipts", checked); - if (checked) { - processTransactionReceiptsDisclosure.onOpen(); - } else { - processTransactionReceiptsDisclosure.onClose(); - } - }} - /> - Transaction Receipts - - {/* Shows all/specific functions if processing transaction receipts */} - -
- { - if (val === "true") { - setShouldFilterFunctions(true); - } else { - setShouldFilterFunctions(false); - form.setValue("filterFunctions", []); + +
+ )} +
+ +
+ + { + const checked = !!val; + form.setValue("processTransactionReceipts", checked); + setProcessTransactionReceiptsOpen(checked); + }} + /> + + Transaction Receipts + + + + {/* Shows all/specific functions if processing transaction receipts */} + {processTransactionReceiptsOpen && ( +
+ { + if (val === "true") { + setShouldFilterFunctions(true); + } else { + setShouldFilterFunctions(false); + form.setValue("filterFunctions", []); + } + }} + > +
+
+ + +
+
+ + +
+ + {/* List function names to select */} + {shouldFilterFunctions && ( + + form.setValue("filterFunctions", value) } - }} - > -
- - All functions - - - - Specific functions{" "} - {!!filterFunctions.length && - `(${filterFunctions.length} selected)`} - - - {/* List function names to select */} - - - form.setValue("filterFunctions", value) - } - /> - -
- + /> + )}
- +
- + )}
- +
- +
- - +
+
); }; @@ -466,7 +484,6 @@ const FilterSelector = ({ client, }: { abiItemType: "function" | "event"; - form: UseFormReturn; filter: string[]; setFilter: (value: string[]) => void; @@ -534,33 +551,25 @@ const FilterSelector = ({ } }, [abiItemType, abiItems.events, abiItems.writeFunctions]); + if (abiQuery.isPending) { + return ; + } + + if (filterNames.length === 0) { + return ( +

+ Cannot resolve the contract definition. Can not select {abiItemType}s. +

+ ); + } + return ( - - {abiQuery.isPending ? ( - - ) : filterNames.length === 0 ? ( - - Cannot resolve the contract definition. Filters are unavailable. - - ) : ( -
- {filterNames.map((name) => ( - - { - if (val) { - setFilter([...filter, name]); - } else { - setFilter(filter.filter((item) => item !== name)); - } - }} - /> - {name} - - ))} -
- )} -
+ ({ label: name, value: name }))} + selectedValues={filter} + onSelectedValuesChange={setFilter} + placeholder={`Select ${abiItemType}s`} + className="w-full bg-card" + /> ); }; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/contract-subscriptions/components/contract-subscriptions-table.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/contract-subscriptions/components/contract-subscriptions-table.tsx index fc0a9c93332..c51ccc304b8 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/contract-subscriptions/components/contract-subscriptions-table.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/contract-subscriptions/components/contract-subscriptions-table.tsx @@ -1,33 +1,32 @@ "use client"; -import { - Flex, - FormControl, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - Tooltip, - type UseDisclosureReturn, - useDisclosure, -} from "@chakra-ui/react"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { createColumnHelper } from "@tanstack/react-table"; -import { Button, LinkButton } from "chakra/button"; -import { Card } from "chakra/card"; -import { FormLabel } from "chakra/form"; -import { Text } from "chakra/text"; import { format } from "date-fns"; import { InfoIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; +import { toast } from "sonner"; import type { ThirdwebClient } from "thirdweb"; import { eth_getBlockByNumber, getRpcClient } from "thirdweb"; import { shortenAddress as shortenAddressThrows } from "thirdweb/utils"; import { TWTable } from "@/components/blocks/TWTable"; +import { Button } from "@/components/ui/button"; import { CopyAddressButton } from "@/components/ui/CopyAddressButton"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { useAllChainsData } from "@/hooks/chains/allChains"; import { useV5DashboardChain } from "@/hooks/chains/v5-adapter"; import { @@ -35,36 +34,12 @@ import { useEngineRemoveContractSubscription, useEngineSubscriptionsLastBlock, } from "@/hooks/useEngine"; -import { useTxNotifications } from "@/hooks/useTxNotifications"; import { ChainIconClient } from "@/icons/ChainIcon"; - -function shortenAddress(address: string) { - if (!address) { - return ""; - } - - try { - return shortenAddressThrows(address); - } catch { - return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`; - } -} - -interface ContractSubscriptionTableProps { - instanceUrl: string; - contractSubscriptions: EngineContractSubscription[]; - isPending: boolean; - isFetched: boolean; - autoUpdate: boolean; - authToken: string; - client: ThirdwebClient; -} +import { parseError } from "@/utils/errorParser"; const columnHelper = createColumnHelper(); -export const ContractSubscriptionTable: React.FC< - ContractSubscriptionTableProps -> = ({ +export function ContractSubscriptionTable({ instanceUrl, contractSubscriptions, isPending, @@ -72,8 +47,16 @@ export const ContractSubscriptionTable: React.FC< autoUpdate, authToken, client, -}) => { - const removeDisclosure = useDisclosure(); +}: { + instanceUrl: string; + contractSubscriptions: EngineContractSubscription[]; + isPending: boolean; + isFetched: boolean; + autoUpdate: boolean; + authToken: string; + client: ThirdwebClient; +}) { + const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false); const [selectedContractSub, setSelectedContractSub] = useState(); const { idToChain } = useAllChainsData(); @@ -83,14 +66,14 @@ export const ContractSubscriptionTable: React.FC< cell: (cell) => { const chain = idToChain.get(cell.getValue()); return ( - +
- {chain?.name ?? "N/A"} - + {chain?.name ?? "N/A"} +
); }, header: "Chain", @@ -109,15 +92,17 @@ export const ContractSubscriptionTable: React.FC< ); } return ( - - {shortenAddress(cell.getValue())} - + ); }, header: "Contract Address", @@ -127,11 +112,7 @@ export const ContractSubscriptionTable: React.FC< const webhook = cell.getValue(); const url = webhook?.url ?? ""; - return ( - - {url} - - ); + return {url}; }, header: "Webhook URL", }), @@ -145,63 +126,61 @@ export const ContractSubscriptionTable: React.FC< } = cell.row.original; return ( - +
{/* Show logs + events */} {processEventLogs && ( - - Logs: +
+ Logs: {filterEvents.length === 0 ? ( - All + All ) : ( - - {filterEvents.map((name) => ( - {name} - ))} -
- } - p={0} - shouldWrapChildren - > - - {filterEvents.length} events - - + + + + + {filterEvents.length} events + + + +
+ {filterEvents.map((name) => ( + {name} + ))} +
+
+
+
)} -
+
)} {/* Show receipts + functions */} {processTransactionReceipts && ( - - Receipts: +
+ Receipts: {filterFunctions.length === 0 ? ( - All + All ) : ( - - {filterFunctions.map((name) => ( - {name} - ))} -
- } - p={0} - shouldWrapChildren - > - - {filterFunctions.length} functions - - + + + + + {filterFunctions.length} functions + + + +
+ {filterFunctions.map((name) => ( + {name} + ))} +
+
+
+
)} -
+
)} - +
); }, header: "Filters", @@ -236,7 +215,7 @@ export const ContractSubscriptionTable: React.FC< isDestructive: true, onClick: (contractSub) => { setSelectedContractSub(contractSub); - removeDisclosure.onOpen(); + setIsRemoveModalOpen(true); }, text: "Remove", }, @@ -244,20 +223,21 @@ export const ContractSubscriptionTable: React.FC< title="contract subscriptions" /> - {selectedContractSub && removeDisclosure.isOpen && ( + {selectedContractSub && ( setIsRemoveModalOpen(false)} instanceUrl={instanceUrl} /> )} ); -}; +} -const ChainLastBlockTimestamp = ({ +function ChainLastBlockTimestamp({ chainId, blockNumber, client, @@ -265,7 +245,7 @@ const ChainLastBlockTimestamp = ({ chainId: number; blockNumber: bigint; client: ThirdwebClient; -}) => { +}) { const chain = useV5DashboardChain(chainId); // Get the block timestamp to display how delayed the last processed block is. const ethBlockQuery = useQuery({ @@ -290,13 +270,15 @@ const ChainLastBlockTimestamp = ({ } return ( - - {format(ethBlockQuery.data, "PP pp z")} + + + {format(ethBlockQuery.data, "PP pp z")} + ); -}; +} -const ChainLastBlock = ({ +function ChainLastBlock({ instanceUrl, chainId, autoUpdate, @@ -308,7 +290,7 @@ const ChainLastBlock = ({ autoUpdate: boolean; authToken: string; client: ThirdwebClient; -}) => { +}) { const lastBlockQuery = useEngineSubscriptionsLastBlock({ authToken, autoUpdate, @@ -320,145 +302,158 @@ const ChainLastBlock = ({ } return ( - - {lastBlockQuery.data} - - } - placement="auto" - shouldWrapChildren - > - - - +
+ {lastBlockQuery.data} + + + + + + + + + + +
); -}; +} -const RemoveModal = ({ +function RemoveModal({ contractSubscription, - disclosure, + isOpen, + onClose, instanceUrl, authToken, client, }: { contractSubscription: EngineContractSubscription; - disclosure: UseDisclosureReturn; + isOpen: boolean; + onClose: () => void; instanceUrl: string; authToken: string; client: ThirdwebClient; -}) => { - const { mutate: removeContractSubscription } = - useEngineRemoveContractSubscription({ - authToken, - instanceUrl, - }); +}) { + const removeContractSubscription = useEngineRemoveContractSubscription({ + authToken, + instanceUrl, + }); - const { onSuccess, onError } = useTxNotifications( - "Successfully removed contract subscription.", - "Failed to remove contract subscription.", - ); const { idToChain } = useAllChainsData(); const chain = idToChain.get(contractSubscription.chainId); const onClick = () => { - removeContractSubscription( + removeContractSubscription.mutate( { contractSubscriptionId: contractSubscription.id, }, { onError: (error) => { - onError(error); + toast.error("Failed to remove contract subscription.", { + description: parseError(error), + }); console.error(error); }, onSuccess: () => { - onSuccess(); - disclosure.onClose(); + toast.success("Successfully removed contract subscription."); + onClose(); }, }, ); }; return ( - - - - Remove Contract Subscription - - -
- - This action will delete all stored data for this contract - subscription. - + + + + Remove Contract Subscription + + This action will delete all stored data for this contract + subscription. + + - - - Chain - - - {chain?.name ?? "N/A"} - - +
+
+

Chain

+
+ + + {chain?.name ?? "N/A"} + +
+
- - Contract Address - - {contractSubscription.contractAddress} - - +
+

Contract Address

+ + {contractSubscription.contractAddress} + +
- - Webhook - {contractSubscription.webhook ? ( - {contractSubscription.webhook.url} - ) : ( - N/A - )} - +
+

Webhook

+ {contractSubscription.webhook ? ( + + {contractSubscription.webhook.url} + + ) : ( + N/A + )} +
- - Filters - {contractSubscription.processEventLogs && ( - - Logs:{" "} - {contractSubscription.filterEvents.length === 0 - ? "All" - : contractSubscription.filterEvents.join(", ")} - - )} - {contractSubscription.processTransactionReceipts && ( - - Receipts:{" "} - {contractSubscription.filterFunctions.length === 0 - ? "All" - : contractSubscription.filterFunctions.join(", ")} - - )} - - +
+

Filters

+ {contractSubscription.processEventLogs && ( + + Logs:{" "} + {contractSubscription.filterEvents.length === 0 + ? "All" + : contractSubscription.filterEvents.join(", ")} + + )} + {contractSubscription.processTransactionReceipts && ( + + Receipts:{" "} + {contractSubscription.filterFunctions.length === 0 + ? "All" + : contractSubscription.filterFunctions.join(", ")} + + )}
- - -
+ +
+ - - - - +
+
+
); -}; +} + +function shortenAddress(address: string) { + if (!address) { + return ""; + } + + try { + return shortenAddressThrows(address); + } catch { + return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`; + } +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/contract-subscriptions/components/engine-contract-subscription.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/contract-subscriptions/components/engine-contract-subscription.tsx index 3e80cb87799..7a9a820aaa5 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/contract-subscriptions/components/engine-contract-subscription.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/contract-subscriptions/components/engine-contract-subscription.tsx @@ -1,39 +1,38 @@ "use client"; -import { Flex, FormControl, Switch } from "@chakra-ui/react"; -import { FormLabel } from "chakra/form"; -import { Heading } from "chakra/heading"; -import { Text } from "chakra/text"; import { useId, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; +import { Switch } from "@/components/ui/switch"; import { UnderlineLink } from "@/components/ui/UnderlineLink"; import { useEngineContractSubscription } from "@/hooks/useEngine"; import { AddContractSubscriptionButton } from "./add-contract-subscription-button"; import { ContractSubscriptionTable } from "./contract-subscriptions-table"; -interface EngineContractSubscriptionsProps { +export function EngineContractSubscriptions({ + instanceUrl, + authToken, + client, +}: { instanceUrl: string; authToken: string; client: ThirdwebClient; -} - -export const EngineContractSubscriptions: React.FC< - EngineContractSubscriptionsProps -> = ({ instanceUrl, authToken, client }) => { - const [autoUpdate, setAutoUpdate] = useState(true); +}) { + const [autoUpdate, setAutoUpdate] = useState(true); const contractSubscriptionsQuery = useEngineContractSubscription({ authToken, instanceUrl, }); - const autoUpdateId = useId(); return ( - - - - Contract Subscriptions - +
+ {/* Header */} +
+
+

+ Contract Subscriptions +

+

Subscribe to event logs and transaction receipts on any contract.{" "} . - - -

- - +

+
+ +
+
+ setAutoUpdate((val) => !val)} + checked={autoUpdate} + onCheckedChange={() => setAutoUpdate((val) => !val)} /> - +
- +
+ - - + + {/* add */} +
+ +
+
); -}; +}