Skip to content

Commit 54e5c06

Browse files
authored
feat(explorer): copy button (#3423)
1 parent bf5e220 commit 54e5c06

8 files changed

Lines changed: 79 additions & 19 deletions

File tree

.changeset/five-berries-act.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/explorer": patch
3+
---
4+
5+
Added 'Copy to Clipboard' button to relevant sections for easier data copying.

packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/ExportButton.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DownloadIcon } from "lucide-react";
2+
import { stringify } from "viem";
23
import { Button } from "../../../../../../components/ui/Button";
34
import {
45
DropdownMenu,
@@ -29,7 +30,7 @@ export function ExportButton({ tableData, isLoading }: { tableData?: TData; isLo
2930
</DropdownMenuItem>
3031
<DropdownMenuItem
3132
onClick={() => {
32-
const json = JSON.stringify(tableData?.rows, null, 2);
33+
const json = stringify(tableData?.rows, null, 2);
3334
exportTableData(json, "data.json", "application/json");
3435
}}
3536
>

packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/FunctionField.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { Coins, ExternalLinkIcon, Eye, LoaderIcon, Send } from "lucide-react";
44
import Link from "next/link";
55
import { useParams } from "next/navigation";
66
import { toast } from "sonner";
7-
import { Abi, AbiFunction, Address, Hex, decodeEventLog } from "viem";
7+
import { Abi, AbiFunction, Address, Hex, decodeEventLog, stringify } from "viem";
88
import { useAccount, useConfig } from "wagmi";
99
import { readContract, waitForTransactionReceipt, writeContract } from "wagmi/actions";
1010
import { z } from "zod";
1111
import { useState } from "react";
1212
import { useForm } from "react-hook-form";
1313
import { zodResolver } from "@hookform/resolvers/zod";
1414
import { useConnectModal } from "@rainbow-me/rainbowkit";
15+
import { CopyButton } from "../../../../../../components/CopyButton";
1516
import { Button } from "../../../../../../components/ui/Button";
1617
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../../../../../../components/ui/Form";
1718
import { Input } from "../../../../../../components/ui/Input";
@@ -79,7 +80,7 @@ export function FunctionField({ worldAbi, functionAbi }: Props) {
7980
chainId,
8081
});
8182

82-
setResult(JSON.stringify(result, null, 2));
83+
setResult(stringify(result, null, 2));
8384
} else {
8485
toastId = toast.loading("Transaction submitted");
8586
const txHash = await writeContract(wagmiConfig, {
@@ -167,9 +168,14 @@ export function FunctionField({ worldAbi, functionAbi }: Props) {
167168
</form>
168169
</Form>
169170

170-
{result && <pre className="text-md mt-4 rounded border p-3 text-sm">{result}</pre>}
171+
{result && (
172+
<pre className="text-md relative mt-4 rounded border p-3 text-sm">
173+
{result}
174+
<CopyButton value={result} className="absolute right-1.5 top-1.5" />
175+
</pre>
176+
)}
171177
{events && (
172-
<div className="mt-4 flex-grow break-all border border-white/20 p-2 pb-3">
178+
<div className="relative mt-4 flex-grow break-all rounded border border-white/20 p-2 pb-3">
173179
<ul>
174180
{events.map((event, idx) => (
175181
<li key={idx}>
@@ -188,6 +194,8 @@ export function FunctionField({ worldAbi, functionAbi }: Props) {
188194
</li>
189195
))}
190196
</ul>
197+
198+
<CopyButton value={stringify(events, null, 2)} className="absolute right-1.5 top-1.5" />
191199
</div>
192200
)}
193201
{txUrl && (

packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/interact/InteractForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Coins, Eye, Send } from "lucide-react";
44
import { useQueryState } from "nuqs";
5-
import { AbiFunction } from "viem";
5+
import { AbiFunction, stringify } from "viem";
66
import { useDeferredValue, useMemo } from "react";
77
import { Input } from "../../../../../../components/ui/Input";
88
import { Separator } from "../../../../../../components/ui/Separator";
@@ -92,7 +92,7 @@ export function InteractForm() {
9292

9393
{data?.abi &&
9494
filteredFunctions.map((abi) => (
95-
<FunctionField key={JSON.stringify(abi)} worldAbi={data.abi} functionAbi={abi} />
95+
<FunctionField key={stringify(abi)} worldAbi={data.abi} functionAbi={abi} />
9696
))}
9797
</div>
9898
</div>

packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionTableRow.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
2-
import { formatEther } from "viem";
2+
import { formatEther, stringify } from "viem";
33
import { Row, flexRender } from "@tanstack/react-table";
4+
import { CopyButton } from "../../../../../../components/CopyButton";
45
import { Separator } from "../../../../../../components/ui/Separator";
56
import { Skeleton } from "../../../../../../components/ui/Skeleton";
67
import { TableCell, TableRow } from "../../../../../../components/ui/Table";
@@ -85,22 +86,20 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
8586
<div className="flex items-start gap-x-4">
8687
<h3 className="w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Inputs</h3>
8788

88-
<div className="flex w-full flex-col gap-y-4">
89+
<div className="relative flex w-full flex-col gap-y-4">
8990
{calls.map((call, idx) => {
9091
if (!call.args || call.args.length === 0) {
9192
return null;
9293
}
9394

9495
return (
95-
<div key={idx} className="min-w-0 flex-grow border border-white/20 p-2 pt-1">
96+
<div key={idx} className="min-w-0 flex-grow rounded border border-white/20 p-2 pt-1">
9697
<span className="text-xs">{call.functionName}:</span>
9798
{call.args?.map((arg, argIdx) => (
9899
<div key={argIdx} className="flex">
99100
<span className="flex-shrink-0 text-xs text-white/60">arg {argIdx + 1}:</span>
100101
<span className="ml-2 break-all text-xs">
101-
{typeof arg === "object" && arg !== null
102-
? JSON.stringify(arg, null, 2)
103-
: String(arg)}
102+
{typeof arg === "object" && arg !== null ? stringify(arg, null, 2) : String(arg)}
104103
</span>
105104
</div>
106105
))}
@@ -111,6 +110,8 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
111110
</div>
112111
);
113112
})}
113+
114+
<CopyButton value={stringify(data.calls, null, 2)} className="absolute right-1.5 top-1.5" />
114115
</div>
115116
</div>
116117
</>
@@ -121,7 +122,7 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
121122
<Separator className="my-5" />
122123
<div className="flex items-start gap-x-4">
123124
<h3 className="w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Error</h3>
124-
<div className="flex-grow whitespace-pre-wrap border border-red-500 p-2 font-mono text-xs">
125+
<div className="flex-grow whitespace-pre-wrap rounded border border-red-500 p-2 font-mono text-xs">
125126
{data.error.message}
126127
</div>
127128
</div>
@@ -134,7 +135,7 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
134135
<div className="flex items-start gap-x-4">
135136
<h3 className="inline-block w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Logs</h3>
136137
{Array.isArray(logs) && logs.length > 0 ? (
137-
<div className="flex-grow break-all border border-white/20 p-2 pb-3">
138+
<div className="relative flex-grow break-all rounded border border-white/20 p-2 pb-3">
138139
<ul>
139140
{logs.map((log, idx) => {
140141
const eventName = "eventName" in log ? log.eventName : null;
@@ -157,6 +158,8 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
157158
);
158159
})}
159160
</ul>
161+
162+
<CopyButton value={stringify(logs, null, 2)} className="absolute right-1.5 top-1.5" />
160163
</div>
161164
) : status === "pending" ? (
162165
<Skeleton className="h-4 w-full" />

packages/explorer/src/app/(explorer)/queries/useTableDataQuery.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useParams } from "next/navigation";
2-
import { Hex } from "viem";
2+
import { Hex, stringify } from "viem";
33
import { Table } from "@latticexyz/config";
44
import { useQuery } from "@tanstack/react-query";
55
import { useChain } from "../hooks/useChain";
@@ -32,7 +32,7 @@ export function useTableDataQuery({ table, query, isLiveQuery }: Props) {
3232
headers: {
3333
"Content-Type": "application/json",
3434
},
35-
body: JSON.stringify([
35+
body: stringify([
3636
{
3737
address: worldAddress as Hex,
3838
query: decodedQuery,

packages/explorer/src/app/(explorer)/queries/useTablesQuery.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useParams } from "next/navigation";
2-
import { Hex } from "viem";
2+
import { Hex, stringify } from "viem";
33
import { isDefined } from "@latticexyz/common/utils";
44
import { Table } from "@latticexyz/config";
55
import mudConfig from "@latticexyz/store/mud.config";
@@ -29,7 +29,7 @@ export function useTablesQuery() {
2929
headers: {
3030
"Content-Type": "application/json",
3131
},
32-
body: JSON.stringify([
32+
body: stringify([
3333
{
3434
address: worldAddress as Hex,
3535
query,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { CheckIcon, ClipboardIcon } from "lucide-react";
2+
import { useEffect, useState } from "react";
3+
import { cn } from "../utils";
4+
import { Button, ButtonProps } from "./ui/Button";
5+
6+
interface CopyButtonProps extends ButtonProps {
7+
value: string;
8+
}
9+
10+
function copyToClipboard(value: string) {
11+
navigator.clipboard.writeText(value);
12+
}
13+
14+
export function CopyButton({ className, variant = "outline", value, ...props }: CopyButtonProps) {
15+
const [hasCopied, setHasCopied] = useState(false);
16+
17+
useEffect(() => {
18+
if (hasCopied) {
19+
setTimeout(() => {
20+
setHasCopied(false);
21+
}, 2000);
22+
}
23+
}, [hasCopied]);
24+
25+
return (
26+
<Button
27+
size="icon"
28+
variant={variant}
29+
className={cn(
30+
"relative z-10 h-6 w-6 border-white/15 bg-transparent text-zinc-50 hover:bg-secondary hover:text-zinc-50 [&_svg]:h-3 [&_svg]:w-3",
31+
className,
32+
)}
33+
onClick={() => {
34+
copyToClipboard(value);
35+
setHasCopied(true);
36+
}}
37+
{...props}
38+
>
39+
<span className="sr-only">Copy</span>
40+
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
41+
</Button>
42+
);
43+
}

0 commit comments

Comments
 (0)