start view-component
This commit is contained in:
@@ -38,6 +38,7 @@ export default tseslint.config(
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
"react-refresh/only-export-components": "off",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,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,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>
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,4 @@
|
||||
import { getCodecComponent } from "@codec-components"
|
||||
import * as viewComponents from "./components"
|
||||
|
||||
export const ViewCodec = getCodecComponent(viewComponents)
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user