start view-component

This commit is contained in:
Victor Oliva
2024-10-25 12:10:57 +02:00
parent 646cf7e1cd
commit 9a06ffad8c
18 changed files with 360 additions and 86 deletions
+1
View File
@@ -38,6 +38,7 @@ export default tseslint.config(
ignoreRestSiblings: true,
},
],
"react-refresh/only-export-components": "off",
},
},
)
+8 -80
View File
@@ -8,33 +8,19 @@ import {
ComboboxOption,
ComboboxOptions,
} from "@headlessui/react"
import {
connectInjectedExtension,
getInjectedExtensions,
InjectedPolkadotAccount,
} from "polkadot-api/pjs-signer"
import {
getSs58AddressInfo,
SS58String,
} from "@polkadot-api/substrate-bindings"
import { state, useStateObservable } from "@react-rxjs/core"
import { combineKeys } from "@react-rxjs/utils"
import { useStateObservable } from "@react-rxjs/core"
import { FC, useState } from "react"
import {
concat,
filter,
finalize,
from,
fromEventPattern,
interval,
map,
retry,
startWith,
switchMap,
take,
timer,
} from "rxjs"
import { twMerge } from "tailwind-merge"
import {
accountDetail$,
accounts$,
getAccountMapKey,
getPublicKey,
} from "../common/accounts.state"
export const CAccountId: EditAccountId = ({ value, onValueChanged }) => {
const [localInput, setLocalInput] = useSynchronizeInput(
@@ -81,7 +67,7 @@ export const CAccountId: EditAccountId = ({ value, onValueChanged }) => {
className="bg-transparent outline-none w-full"
aria-label="AccountId"
displayValue={(address: string) =>
accounts.get(address)?.name ?? address
accounts.get(getAccountMapKey(address))?.name ?? address
}
onChange={(event) => setQuery(event.target.value)}
/>
@@ -138,11 +124,6 @@ const AccountOption: FC<{ account: string; publicKey: Uint8Array | null }> = ({
)
}
const getPublicKey = (address: string) => {
const info = getSs58AddressInfo(address)
return info.isValid ? info.publicKey : null
}
const getValidationError = (value: string) => {
if (value === "") return null
const { isValid } = getSs58AddressInfo(value)
@@ -153,56 +134,3 @@ const parseValue = (value: string): SS58String | NOTIN => {
if (getValidationError(value)) return NOTIN
return value
}
const extensions$ = state(
concat(
timer(0, 100).pipe(
map(getInjectedExtensions),
filter((v) => v.length > 0),
take(1),
),
interval(2000).pipe(map(getInjectedExtensions)),
),
[],
)
const extensionAccounts$ = combineKeys(extensions$, (extension) =>
from(connectInjectedExtension(extension)).pipe(
switchMap((extension) =>
fromEventPattern<InjectedPolkadotAccount[]>(
extension.subscribe,
(_, fn) => fn(),
).pipe(
startWith(extension.getAccounts()),
finalize(extension.disconnect),
),
),
retry({
delay: 100,
}),
),
)
const accounts$ = state(
extensionAccounts$.pipe(
map(
(extensionAccounts) =>
new Map(
Array.from(extensionAccounts.entries()).flatMap(
([extension, accounts]) =>
accounts.map((account) => [
account.address,
{
...account,
extension,
},
]),
),
),
),
),
new Map<string, InjectedPolkadotAccount & { extension: string }>(),
)
const accountDetail$ = state(
(account: string) => accounts$.pipe(map((v) => v.get(account) ?? null)),
null,
)
+1 -1
View File
@@ -1,8 +1,8 @@
import { EditSequence, NOTIN } from "@codec-components"
import { CirclePlus } from "lucide-react"
import { twMerge as clsx } from "tailwind-merge"
import { ListItem } from "../common/ListItem"
import { useSubtreeFocus } from "../common/SubtreeFocus"
import { ListItem } from "./ListItem"
export const CSequence: EditSequence = ({
innerComponents,
+1
View File
@@ -1,6 +1,7 @@
import { withDefault } from "@/utils/default"
import { EditStr } from "@codec-components"
// TODO
export const CStr: EditStr = ({ value, onValueChanged }) => {
return (
<div>
@@ -90,7 +90,10 @@ export const CEnum: EditEnum = ({
)
}
const useAppendTitle = (container: HTMLElement | null, className: string) => {
export const useAppendTitle = (
container: HTMLElement | null,
className: string,
) => {
const [element, setElement] = useState<HTMLElement | null>(null)
useLayoutEffect(() => {
@@ -11,8 +11,8 @@ import {
} from "@codec-components"
import { FC, ReactNode, useState } from "react"
import { twMerge } from "tailwind-merge"
import { ListItem } from "../common/ListItem"
import { useSubtreeFocus } from "../common/SubtreeFocus"
import { ListItem } from "./ListItem"
export const CBool: EditBool = ({ value, onValueChanged, path }) => {
return (
@@ -45,6 +45,7 @@ export const CBool: EditBool = ({ value, onValueChanged, path }) => {
export const CVoid: EditVoid = () => null
// TODO
export const CEthAccount: EditEthAccount = ({ value }) => (
<span>{withDefault(value, "")}</span>
)
@@ -87,6 +88,7 @@ export const COption: EditOption = ({ value, inner, onValueChanged }) => {
)
}
// TODO
export const CResult: EditResult = ({ value, inner, type }) => {
return type === "blank" ? null : (
<>
@@ -0,0 +1,24 @@
import { PolkadotIdenticon } from "@/components/PolkadotIdenticon"
import { ViewAccountId } from "@codec-components"
import { useStateObservable } from "@react-rxjs/core"
import { accountDetail$, getPublicKey } from "../common/accounts.state"
export const CAccountId: ViewAccountId = ({ value }) => {
const details = useStateObservable(accountDetail$(value))
return (
<div>
<PolkadotIdenticon
className="flex-shrink-0"
publicKey={getPublicKey(value)}
size={32}
/>
<div className="flex flex-col justify-center text-white leading-tight overflow-hidden">
{details?.name && <span>{details.name}</span>}
<span className="text-slate-400 text-ellipsis overflow-hidden">
{value}
</span>
</div>
</div>
)
}
@@ -0,0 +1,8 @@
import { bytesToString } from "@/components/BinaryInput"
import { ViewBytes } from "@codec-components"
export const CBytes: ViewBytes = ({ value }) => (
<div className="min-w-80 border-none p-0 outline-none bg-transparent flex-1">
{bytesToString(value)}
</div>
)
+24
View File
@@ -0,0 +1,24 @@
import { isEnumComplex } from "@/utils/shape"
import { ViewEnum } from "@codec-components"
import { Marker } from "../common/Markers"
import { useSubtreeFocus } from "../common/SubtreeFocus"
export const CEnum: ViewEnum = ({ value, inner, shape, path }) => {
const focus = useSubtreeFocus()
const sub = focus.getNextPath(path)
if (sub) {
return inner
}
const isComplexShape = isEnumComplex(shape, value.type)
return (
<div className="flex flex-col">
<Marker id={[...path, value.type]} />
<div className="flex flex-row flex-wrap text-sm items-center gap-2">
<div>{value.type}</div>
{!isComplexShape && inner}
</div>
{isComplexShape && <div className="flex flex-col pt-1 ">{inner}</div>}
</div>
)
}
@@ -0,0 +1,21 @@
import { ViewSequence } from "@codec-components"
import { useSubtreeFocus } from "../common/SubtreeFocus"
import { ListItem } from "../common/ListItem"
export const CSequence: ViewSequence = ({ innerComponents, path }) => {
const focus = useSubtreeFocus()
const sub = focus.getNextPath(path)
if (sub) {
return innerComponents[Number(sub)]
}
return (
<ul>
{innerComponents.map((item, idx) => (
<ListItem key={idx} idx={idx} path={[...path, String(idx)]}>
{item}
</ListItem>
))}
</ul>
)
}
@@ -0,0 +1,84 @@
import { ExpandBtn } from "@/components/Expand"
import { getFinalType } from "@/utils/shape"
import { ViewStruct } from "@codec-components"
import { useStateObservable } from "@react-rxjs/core"
import React from "react"
import { twMerge as clsx, twMerge } from "tailwind-merge"
import { Marker } from "../common/Markers"
import { useSubtreeFocus } from "../common/SubtreeFocus"
import {
isActive$,
isCollapsed$,
setHovered,
toggleCollapsed,
} from "../common/paths.state"
import { ItemMarker } from "../EditCodec/Tree/codec-components"
const StructItem: React.FC<{
name: string
children: React.ReactNode
path: string[]
type?: string
}> = ({ name, children, path, type }) => {
const pathStr = path.join(".")
const isActive = useStateObservable(isActive$(pathStr))
const isExpanded = !useStateObservable(isCollapsed$(pathStr))
return (
<li
className={twMerge(
"flex flex-col transition-all duration-300",
isActive && "backdrop-brightness-150",
)}
onMouseEnter={() => setHovered({ id: pathStr, hover: true })}
onMouseLeave={() => setHovered({ id: pathStr, hover: false })}
>
<Marker id={path} />
<span
onClick={() => toggleCollapsed(pathStr)}
className="cursor-pointer flex select-none items-center py-1 gap-1"
>
<ItemMarker />
<ExpandBtn expanded={isExpanded} />
{name}
{type && (
<div className="ml-1">
<span className="text-slate-400 text-xs">({type})</span>
</div>
)}
</span>
<div
className={clsx(
"flex flex-row pl-4 pr-2 pb-2",
isExpanded ? "" : "hidden",
)}
>
{children}
</div>
</li>
)
}
export const CStruct: ViewStruct = ({ innerComponents, path, shape }) => {
const focus = useSubtreeFocus()
const sub = focus.getNextPath(path)
if (sub) {
const field = Object.entries(innerComponents).find(([key]) => key === sub)
return field?.[1]
}
return (
<ul className="flex flex-col border-l border-polkadot-700">
{Object.entries(innerComponents).map(([name, jsx]) => (
<StructItem
name={name}
key={name}
path={[...path, name]}
type={getFinalType(shape, name)}
>
{jsx}
</StructItem>
))}
</ul>
)
}
+18
View File
@@ -0,0 +1,18 @@
import { ViewTuple } from "@codec-components"
import { useSubtreeFocus } from "../common/SubtreeFocus"
export const CTuple: ViewTuple = ({ innerComponents, path }) => {
const focus = useSubtreeFocus()
const sub = focus.getNextPath(path)
if (sub) {
return innerComponents[Number(sub)]
}
return (
<ul className="flex flex-col ml-5">
{innerComponents.map((jsx, idx) => (
<li key={idx}>{jsx}</li>
))}
</ul>
)
}
@@ -0,0 +1,63 @@
import {
ViewArray,
ViewBigNumber,
ViewBool,
ViewEthAccount,
ViewNumber,
ViewOption,
ViewResult,
ViewStr,
ViewVoid,
} from "@codec-components"
import { ListItem } from "../common/ListItem"
import { useSubtreeFocus } from "../common/SubtreeFocus"
export const CBool: ViewBool = ({ value }) => {
return <div className="flex gap-4">{value ? "Yes" : "No"}</div>
}
export const CVoid: ViewVoid = () => null
export const CEthAccount: ViewEthAccount = ({ value }) => <span>{value}</span>
export const CArray: ViewArray = ({ innerComponents, path }) => {
const focus = useSubtreeFocus()
const sub = focus.getNextPath(path)
if (sub) {
return innerComponents[Number(sub)]
}
return (
<ul>
{innerComponents.map((jsx, idx) => (
<ListItem key={idx} idx={idx} path={[...path, String(idx)]}>
{jsx}
</ListItem>
))}
</ul>
)
}
export const COption: ViewOption = ({ value, inner }) => {
const selected = value !== undefined
return (
<div>
{selected ? inner : <span className="text-slate-400">Not selected</span>}
</div>
)
}
export const CResult: ViewResult = ({ value, inner }) => {
return (
<div>
<div>{value.success ? "OK" : "KO"}</div>
{inner}
</div>
)
}
export const CStr: ViewStr = ({ value }) => <div>{value}</div>
export const CNumber: ViewNumber = ({ value }) => <div>{value}</div>
export const CBigNumber: ViewBigNumber = ({ value }) => (
<div>{String(value)}</div>
)
@@ -0,0 +1,7 @@
export * from "./CAccountId"
export * from "./CEnum"
export * from "./CBytes"
export * from "./CStruct"
export * from "./CSequence"
export * from "./CTuple"
export * from "./codec-components"
+4
View File
@@ -0,0 +1,4 @@
import { getCodecComponent } from "@codec-components"
import * as viewComponents from "./components"
export const ViewCodec = getCodecComponent(viewComponents)
@@ -2,8 +2,8 @@ import { ExpandBtn } from "@/components/Expand"
import { useStateObservable } from "@react-rxjs/core"
import { Trash2 } from "lucide-react"
import { twMerge as clsx } from "tailwind-merge"
import { Marker } from "../common/Markers"
import { isCollapsed$, toggleCollapsed } from "../common/paths.state"
import { Marker } from "./Markers"
import { isCollapsed$, toggleCollapsed } from "./paths.state"
export const ListItem: React.FC<{
idx: number
@@ -0,0 +1,85 @@
import { Binary, getSs58AddressInfo } from "@polkadot-api/substrate-bindings"
import { state } from "@react-rxjs/core"
import { combineKeys } from "@react-rxjs/utils"
import {
connectInjectedExtension,
getInjectedExtensions,
InjectedPolkadotAccount,
} from "polkadot-api/pjs-signer"
import {
concat,
filter,
finalize,
from,
fromEventPattern,
interval,
map,
retry,
startWith,
switchMap,
take,
timer,
} from "rxjs"
export const getPublicKey = (address: string) => {
const info = getSs58AddressInfo(address)
return info.isValid ? info.publicKey : null
}
export const getAccountMapKey = (address: string) => {
const pk = getPublicKey(address)
return pk ? Binary.fromBytes(pk).asHex() : address
}
const extensions$ = state(
concat(
timer(0, 100).pipe(
map(getInjectedExtensions),
filter((v) => v.length > 0),
take(1),
),
interval(2000).pipe(map(getInjectedExtensions)),
),
[],
)
const extensionAccounts$ = combineKeys(extensions$, (extension) =>
from(connectInjectedExtension(extension)).pipe(
switchMap((extension) =>
fromEventPattern<InjectedPolkadotAccount[]>(
extension.subscribe,
(_, fn) => fn(),
).pipe(
startWith(extension.getAccounts()),
finalize(extension.disconnect),
),
),
retry({
delay: 100,
}),
),
)
export const accounts$ = state(
extensionAccounts$.pipe(
map(
(extensionAccounts) =>
new Map(
Array.from(extensionAccounts.entries()).flatMap(
([extension, accounts]) =>
accounts.map((account) => [
getAccountMapKey(account.address),
{
...account,
extension,
},
]),
),
),
),
),
new Map<string, InjectedPolkadotAccount & { extension: string }>(),
)
export const accountDetail$ = state(
(account: string) =>
accounts$.pipe(map((v) => v.get(getAccountMapKey(account)) ?? null)),
null,
)
+2 -1
View File
@@ -13,6 +13,7 @@ import {
} from "@codec-components"
import { useEffect, useRef, useState } from "react"
import { FocusPath } from "./FocusPath"
import { ViewCodec } from "@/codec-components/ViewCodec"
export const EditMode: React.FC<{
codecType: number
@@ -66,7 +67,7 @@ export const EditMode: React.FC<{
</div>
<div className="flex-1">
<div className="p-2 rounded bg-polkadot-900">
<EditCodec {...props} />
<ViewCodec {...props} />
</div>
</div>
</div>