Skip to content

Commit 5f0b4d1

Browse files
authored
fix(explorer): various fixes (#3712)
1 parent 091ece6 commit 5f0b4d1

13 files changed

Lines changed: 138 additions & 72 deletions

File tree

.changeset/gold-swans-eat.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@latticexyz/explorer": patch
3+
---
4+
5+
- Boolean values can now be submitted on the Interact page.
6+
- Fixed redirects when only a single local world is deployed.
7+
- Chain logos are now displayed correctly when running the Explorer locally.
8+
- Fixed scrolling behavior in the table dropdown when viewing non-selected tables.
9+
- Verified world dropdown now appears immediately after pasting a world address, without waiting for all worlds to resolve.

packages/explorer/src/app/(explorer)/[chainName]/worlds/WorldsForm.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,6 @@ export function WorldsForm({ worlds, isLoading }: Props) {
5353
}
5454
};
5555

56-
const handleOpenOptions = () => {
57-
if (!open && worlds.length > 0) {
58-
setOpen(true);
59-
}
60-
};
61-
6256
return (
6357
<div className="mx-auto flex min-h-screen w-[450px] flex-col items-center justify-center p-4">
6458
<h1 className="flex items-center gap-6 self-start font-mono text-4xl font-bold uppercase">
@@ -80,23 +74,20 @@ export function WorldsForm({ worlds, isLoading }: Props) {
8074
<CommandPrimitive.Input
8175
asChild
8276
value={field.value}
83-
onValueChange={(value) => {
84-
field.onChange(value);
85-
}}
77+
onValueChange={(value) => field.onChange(value)}
8678
onBlur={() => {
8779
field.onBlur();
8880
setOpen(false);
8981
}}
90-
onFocus={handleOpenOptions}
91-
placeholder={isLoading ? "Loading worlds..." : "Enter world address..."}
82+
onFocus={() => setOpen(true)}
83+
placeholder="Enter world address..."
9284
// Need to manually trigger form submission as CommandPrimitive.Input captures Enter key events
9385
onKeyDown={(e) => {
9486
if (!open && e.key === "Enter") {
9587
e.preventDefault();
9688
form.handleSubmit(onSubmit)();
9789
}
9890
}}
99-
disabled={isLoading}
10091
>
10192
<Input className="h-12" />
10293
</CommandPrimitive.Input>
@@ -112,32 +103,36 @@ export function WorldsForm({ worlds, isLoading }: Props) {
112103

113104
<div className="relative">
114105
<CommandList>
115-
{open && worlds.length > 0 ? (
106+
{open ? (
116107
<div className="absolute top-3 z-10 max-h-[200px] w-full overflow-y-auto rounded-md border bg-popover text-popover-foreground outline-none animate-in">
117108
<CommandGroup>
118109
{worlds?.map((world) => {
110+
const displayValue = world.name ? `${world.name} (${world.address})` : world.address;
119111
return (
120112
<CommandItem
121113
key={world.address}
122-
value={world.address}
114+
className="flex cursor-pointer items-center font-mono"
115+
value={displayValue}
123116
onMouseDown={(event) => {
124117
event.preventDefault();
125118
event.stopPropagation();
126119
}}
127-
onSelect={(value) => {
128-
form.setValue("worldAddress", value as Address, {
120+
onSelect={() => {
121+
form.setValue("worldAddress", world.address, {
129122
shouldValidate: true,
130123
});
131124
setOpen(false);
132125
form.handleSubmit(onSubmit)();
133126
}}
134-
className="flex cursor-pointer items-center font-mono"
135127
>
136128
{world.name || world.address}
137129
{world.verified ? <BadgeCheckIcon className="ml-2 h-4 w-4 text-green-500" /> : null}
138130
</CommandItem>
139131
);
140132
})}
133+
{isLoading && !form.getValues("worldAddress") && (
134+
<div className="px-2 py-1.5 text-sm text-muted-foreground">Loading worlds...</div>
135+
)}
141136
</CommandGroup>
142137
</div>
143138
) : null}
@@ -146,7 +141,12 @@ export function WorldsForm({ worlds, isLoading }: Props) {
146141
</div>
147142

148143
<div className="flex w-full items-center gap-x-2">
149-
<Button type="submit" className="flex-1 uppercase" variant="default" disabled={isLoading}>
144+
<Button
145+
type="submit"
146+
className="flex-1 uppercase"
147+
variant="default"
148+
disabled={!form.getValues("worldAddress")}
149+
>
150150
Explore the world
151151
</Button>
152152
<Button

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Loader } from "lucide-react";
1+
import { LoaderIcon } from "lucide-react";
22
import { useParams } from "next/navigation";
33
import { toast } from "sonner";
44
import { Hex } from "viem";
@@ -99,7 +99,7 @@ export function EditableTableCell({ name, table, keyTuple, value: defaultValue }
9999
onCheckedChange={handleSubmit}
100100
disabled={isPending}
101101
/>
102-
{isPending && <Loader className="h-4 w-4 animate-spin" />}
102+
{isPending && <LoaderIcon className="h-4 w-4 animate-spin" />}
103103
</div>
104104
);
105105
}
@@ -130,7 +130,7 @@ export function EditableTableCell({ name, table, keyTuple, value: defaultValue }
130130
{isPending && (
131131
<>
132132
{String(value)}
133-
<Loader className="h-4 w-4 animate-spin" />
133+
<LoaderIcon className="h-4 w-4 animate-spin" />
134134
</>
135135
)}
136136
</div>

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CheckIcon, ChevronsUpDownIcon, Link2Icon, Link2OffIcon } from "lucide-r
22
import { useParams } from "next/navigation";
33
import { useQueryState } from "nuqs";
44
import { Hex } from "viem";
5-
import { useEffect, useLayoutEffect, useState } from "react";
5+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
66
import { Table } from "@latticexyz/config";
77
import { Button } from "../../../../../../components/ui/Button";
88
import {
@@ -37,6 +37,7 @@ export function TableSelector({ tables }: { tables?: Table[] }) {
3737
const indexer = useIndexerForChainId(chainId);
3838
const [selectedTableId, setTableId] = useQueryState("tableId");
3939
const [open, setOpen] = useState(false);
40+
const hasScrolledTo = useRef(false);
4041
const [searchValue, setSearchValue] = useState("");
4142
const selectedTableConfig = tables?.find(({ tableId }) => tableId === selectedTableId);
4243

@@ -47,13 +48,17 @@ export function TableSelector({ tables }: { tables?: Table[] }) {
4748
}, [selectedTableId, setTableId, tables]);
4849

4950
useLayoutEffect(() => {
50-
if (open && selectedTableId && selectedTableConfig) {
51+
if (open && selectedTableId && selectedTableConfig && !hasScrolledTo.current) {
5152
setTimeout(() => {
5253
const selectedTableId = constructTableName(selectedTableConfig, worldAddress as Hex, indexer.type);
5354
const selectedElement = document.querySelector(`[data-value="${selectedTableId}"]`);
5455
selectedElement?.scrollIntoView({ behavior: "instant", block: "center" });
56+
hasScrolledTo.current = true;
5557
}, 0);
5658
}
59+
if (!open) {
60+
hasScrolledTo.current = false;
61+
}
5762
}, [chainId, indexer.type, open, selectedTableConfig, selectedTableId, worldAddress]);
5863

5964
return (

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,12 @@ export function FunctionField({ worldAbi, functionAbi }: Props) {
113113
abi: worldAbi,
114114
address: worldAddress as Address,
115115
functionName: functionAbi.name,
116-
args: values.inputs.map((value, index) =>
117-
functionAbi.inputs[index]?.type === "tuple" ? JSON.parse(value) : value,
118-
),
116+
args: values.inputs.map((value, index) => {
117+
const type = functionAbi.inputs[index]?.type;
118+
if (type === "tuple") return JSON.parse(value);
119+
if (type === "bool") return value === "true";
120+
return value;
121+
}),
119122
...(values.value && { value: BigInt(values.value) }),
120123
chainId,
121124
});

packages/explorer/src/app/(explorer)/[chainName]/worlds/page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use client";
22

33
import { redirect } from "next/navigation";
4-
import { supportedChainName } from "../../../../common";
4+
import { anvil } from "viem/chains";
5+
import { supportedChainName, supportedChains } from "../../../../common";
56
import { useWorldsQuery } from "../../queries/useWorldsQuery";
67
import { WorldsForm } from "./WorldsForm";
78

@@ -18,9 +19,10 @@ export default function WorldsPage({ params: { chainName } }: Props) {
1819
}
1920

2021
const worlds = data?.worlds || [];
21-
if (worlds.length === 1) {
22-
redirect(`/${chainName}/worlds/${worlds[0]}`);
22+
const chain = supportedChains[chainName];
23+
if (worlds.length === 1 && chain.id === anvil.id) {
24+
redirect(`/${chainName}/worlds/${worlds[0]?.address}`);
2325
}
2426

25-
return <WorldsForm worlds={worlds} isLoading={isLoading} />;
27+
return <WorldsForm worlds={worlds} isLoading={isLoading.verified || isLoading.indexer} />;
2628
}

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

Lines changed: 72 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useParams } from "next/navigation";
22
import { Address, getAddress } from "viem";
3+
import { useMemo } from "react";
34
import { MUDChain } from "@latticexyz/common/chains";
4-
import { UseQueryResult, useQuery } from "@tanstack/react-query";
5+
import { useQuery } from "@tanstack/react-query";
56
import { supportedChains, validateChainName } from "../../../common";
67
import { VerifiedWorld } from "../api/verified-worlds/route";
78
import { useIndexerForChainId } from "../hooks/useIndexerForChainId";
@@ -16,6 +17,17 @@ export type WorldsQueryResult = {
1617
worlds: WorldSelectItem[];
1718
};
1819

20+
export type WorldsQueryState = {
21+
data: WorldsQueryResult;
22+
isLoading: {
23+
verified: boolean;
24+
indexer: boolean;
25+
};
26+
isError: boolean;
27+
error: Error | null;
28+
isSuccess: boolean;
29+
};
30+
1931
type ApiResponse = {
2032
items: Array<{ address: { hash: Address } }>;
2133
};
@@ -74,56 +86,77 @@ async function getExternalWorlds(chain: MUDChain) {
7486
return data.items.map((world) => getAddress(world.address.hash));
7587
}
7688

77-
export function useWorldsQuery(): UseQueryResult<WorldsQueryResult> {
78-
const { chainName } = useParams();
79-
validateChainName(chainName);
80-
const chain = supportedChains[chainName];
81-
const indexer = useIndexerForChainId(chain.id);
82-
89+
function useVerifiedWorldsQuery(chain: MUDChain) {
8390
return useQuery({
84-
queryKey: ["worlds", chainName],
91+
queryKey: ["verifiedWorlds", chain.id],
8592
queryFn: async () => {
86-
const worlds: WorldSelectItem[] = [];
87-
const worldsAddresses = new Set<Address>();
93+
try {
94+
const verifiedWorlds = await getVerifiedWorlds(chain);
95+
return new Map(verifiedWorlds.map((world) => [world.address, world.name]));
96+
} catch (error) {
97+
console.error("Failed to fetch verified worlds:", error);
98+
return new Map<Address, string>();
99+
}
100+
},
101+
refetchInterval: 5000,
102+
});
103+
}
88104

89-
let indexerWorlds: Address[] = [];
105+
function useIndexerWorldsQuery(chain: MUDChain, indexer: ReturnType<typeof useIndexerForChainId>) {
106+
return useQuery({
107+
queryKey: ["indexerWorlds", chain.id, indexer.type],
108+
queryFn: async () => {
90109
if (indexer.type === "sqlite") {
91-
indexerWorlds = await getLocalWorlds(indexer);
110+
return await getLocalWorlds(indexer);
92111
} else {
93-
indexerWorlds = await getExternalWorlds(chain);
112+
return await getExternalWorlds(chain);
94113
}
114+
},
115+
refetchInterval: 5000,
116+
});
117+
}
95118

96-
for (const world of indexerWorlds) {
97-
worldsAddresses.add(world);
98-
worlds.push({ address: world, name: "", verified: false });
99-
}
119+
export function useWorldsQuery(): WorldsQueryState {
120+
const { chainName } = useParams();
121+
validateChainName(chainName);
122+
const chain = supportedChains[chainName];
123+
const indexer = useIndexerForChainId(chain.id);
100124

101-
let verifiedWorldsMap = new Map<Address, string>();
102-
try {
103-
const verifiedWorlds = await getVerifiedWorlds(chain);
104-
verifiedWorldsMap = new Map(verifiedWorlds.map((world) => [world.address, world.name]));
125+
const verifiedWorldsQuery = useVerifiedWorldsQuery(chain);
126+
const indexerWorldsQuery = useIndexerWorldsQuery(chain, indexer);
105127

106-
for (const [address, name] of verifiedWorldsMap) {
107-
if (!worldsAddresses.has(address)) {
108-
worlds.push({ address, name, verified: true });
109-
}
110-
}
111-
} catch (error) {
112-
console.error("Failed to fetch verified worlds:", error);
128+
const result = useMemo(() => {
129+
const worlds: WorldSelectItem[] = [];
130+
const worldsAddresses = new Set<Address>();
131+
132+
if (verifiedWorldsQuery.data) {
133+
for (const [address, name] of verifiedWorldsQuery.data) {
134+
worldsAddresses.add(address);
135+
worlds.push({ address, name, verified: true });
113136
}
137+
}
114138

115-
for (const world of worlds) {
116-
const name = verifiedWorldsMap.get(world.address);
117-
if (name) {
118-
world.name = name;
119-
world.verified = true;
139+
if (indexerWorldsQuery.data) {
140+
for (const world of indexerWorldsQuery.data) {
141+
if (!worldsAddresses.has(world)) {
142+
worlds.push({ address: world, name: "", verified: false });
120143
}
121144
}
122-
123-
return {
124-
worlds: worlds.sort((a) => (a.verified ? -1 : 1)),
125-
};
145+
}
146+
147+
return {
148+
worlds: worlds.sort((a) => (a.verified ? -1 : 1)),
149+
};
150+
}, [verifiedWorldsQuery.data, indexerWorldsQuery.data]);
151+
152+
return {
153+
data: result,
154+
isLoading: {
155+
verified: verifiedWorldsQuery.isLoading,
156+
indexer: indexerWorldsQuery.isLoading,
126157
},
127-
refetchInterval: 5000,
128-
});
158+
isError: verifiedWorldsQuery.isError || indexerWorldsQuery.isError,
159+
error: verifiedWorldsQuery.error || indexerWorldsQuery.error,
160+
isSuccess: verifiedWorldsQuery.isSuccess || indexerWorldsQuery.isSuccess,
161+
};
129162
}
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)