feat(EthAccount): add EthAccount edit component

This commit is contained in:
Josep M Sobrepere
2024-12-10 08:32:25 +01:00
parent 59235cd5ed
commit 00d45f2cdb
6 changed files with 4745 additions and 3582 deletions
+4518 -3572
View File
File diff suppressed because it is too large Load Diff
@@ -31,10 +31,16 @@ import { map, switchMap, take } from "rxjs"
const hintedAccounts$ = state(
combineKeys(
accounts$.pipe(map((accounts) => [...accounts.keys()])),
(account) =>
accounts$.pipe(
map((accounts) =>
[...accounts.entries()]
.filter(([, { address }]) => !address.startsWith("0x"))
.map(([key]) => key),
),
),
(address) =>
accounts$.pipe(
map((v) => v.get(account)!),
map((v) => v.get(address)!),
take(1),
switchMap((details) =>
identity$(details.address).pipe(
@@ -0,0 +1,192 @@
import { EthAccountDisplay } from "@/components/EthAccountDisplay"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { accountDetail$, accounts$ } from "@/state/extension-accounts.state"
import { cn } from "@/utils/cn"
import { EditEthAccount, NOTIN } from "@polkadot-api/react-builder"
import { ethAccount, HexString } from "@polkadot-api/substrate-bindings"
import { state, useStateObservable } from "@react-rxjs/core"
import { combineKeys } from "@react-rxjs/utils"
import { Check, ChevronsUpDown } from "lucide-react"
import { FC, useMemo, useState } from "react"
import { map, take } from "rxjs"
const hintedAccounts$ = state(
combineKeys(
accounts$.pipe(
map((accounts) =>
[...accounts.entries()]
.filter(([, { address }]) => address.startsWith("0x"))
.map(([key]) => key),
),
),
(account) =>
accounts$.pipe(
map((v) => {
const details = v.get(account)!
return {
address: details.address,
name: details.name,
}
}),
take(1),
),
).pipe(
map(
(v) => new Map([...v].map(([key, value]) => [key.toLowerCase(), value])),
),
),
new Map<
string,
{
address: string
name: string | undefined
}
>(),
)
const isEthAddressValid = (address: HexString) => {
try {
ethAccount.dec(address)
} catch {
return false
}
return true
}
export const CEthAccount: EditEthAccount = ({ value, onValueChanged }) => {
const accounts = useStateObservable(hintedAccounts$)
const [query, setQuery] = useState("")
const isValid = useMemo(() => isEthAddressValid(query), [query])
const [open, _setOpen] = useState(false)
const setOpen = (value: boolean) => {
_setOpen(value)
setQuery("")
}
const valueIsNew =
value !== NOTIN &&
!accounts.has(isEthAddressValid(value) ? value.toLowerCase() : "")
const accountList = Array.from(accounts.entries())
if (value !== NOTIN) {
accountList.sort(([a], [b]) =>
a === b ? -1 : b === value.toLowerCase() ? 1 : 0,
)
}
const onTriggerKeyDown = (evt: React.KeyboardEvent) => {
if (evt.key.length === 1) {
setOpen(true)
}
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild onKeyDown={onTriggerKeyDown}>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="flex w-64 justify-between overflow-hidden px-2 border border-border bg-input"
forceSvgSize={false}
>
{value !== NOTIN ? (
<EthAccountDisplay value={value} className="overflow-hidden" />
) : (
<span className="opacity-80">Select…</span>
)}
<ChevronsUpDown size={14} className="opacity-50 flex-shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-64 p-0">
<Command>
<CommandInput
placeholder="Filter…"
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>
<div className="text-foreground/50">
The value is not a valid Ethereum Address
</div>
</CommandEmpty>
<CommandGroup>
{valueIsNew && (
<AccountOption
account={value}
selected={true}
onSelect={() => setOpen(false)}
/>
)}
{accountList.map(([key, account]) => (
<AccountOption
key={key}
account={account.address}
selected={value === account.address}
onSelect={() => {
onValueChanged(account.address)
setOpen(false)
}}
/>
))}
{isValid && (
<AccountOption
account={query}
selected={value === query}
onSelect={() => {
onValueChanged(query)
setOpen(false)
}}
/>
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
const AccountOption: FC<{
account: string
selected: boolean
onSelect: () => void
}> = ({ account, selected, onSelect }) => {
const details = useStateObservable(accountDetail$(account))
const name = details?.name
return (
<CommandItem
value={account + "_" + name}
onSelect={onSelect}
className="flex flex-row items-center gap-2 p-1"
forceSvgSize={false}
>
<EthAccountDisplay value={account} className="overflow-hidden" />
<Check
size={12}
className={cn(
"ml-auto flex-shrink-0",
selected ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
)
}
@@ -1,9 +1,7 @@
import SliderToggle from "@/components/Toggle"
import { withDefault } from "@/utils/default"
import {
EditArray,
EditBool,
EditEthAccount,
EditOption,
EditResult,
EditVoid,
@@ -45,11 +43,6 @@ export const CBool: EditBool = ({ value, onValueChanged, path }) => {
export const CVoid: EditVoid = () => null
// TODO
export const CEthAccount: EditEthAccount = ({ value }) => (
<span>{withDefault(value, "")}</span>
)
export const CArray: EditArray = ({ innerComponents, path }) => {
const focus = useSubtreeFocus()
const sub = focus.getNextPath(path)
@@ -7,3 +7,4 @@ export * from "./CSequence"
export * from "./CTuple"
export * from "./CStr"
export * from "./codec-components"
export * from "./CEthAccount"
+25
View File
@@ -0,0 +1,25 @@
import { accountDetail$ } from "@/state/extension-accounts.state"
import { useStateObservable } from "@react-rxjs/core"
import { HexString } from "polkadot-api"
import { FC } from "react"
import { twMerge } from "tailwind-merge"
export const EthAccountDisplay: FC<{
value: HexString
className?: string
}> = ({ value, className }) => {
const { name } = useStateObservable(accountDetail$(value)) ?? {}
return (
<div
className={twMerge(
"flex flex-col justify-center text-foreground leading-tight overflow-hidden",
className,
)}
>
{name && <span className="inline-flex items-center gap-1">{name}</span>}
<span className="text-foreground/50 text-ellipsis overflow-hidden">
{value}
</span>
</div>
)
}