feat: one-click edit extrinsic call data

This commit is contained in:
Victor Oliva
2026-05-13 17:52:50 +02:00
parent d5ee5bb463
commit b0d8179ce4
2 changed files with 157 additions and 69 deletions

View File

@@ -1,91 +1,182 @@
import { BinaryViewCodec } from "@/codec-components/BinaryViewCodec"
import { BinaryEditButton } from "@/components/BinaryEditButton"
import { CopyText } from "@/components/Copy"
import { ExpandBtn } from "@/components/Expand"
import { CodecComponentType, NOTIN } from "@polkadot-api/react-builder"
import { HexString } from "@polkadot-api/substrate-bindings"
import { fromHex, toHex } from "polkadot-api/utils"
import { ComponentProps, FC, useState } from "react"
import { Binary, HexString } from "@polkadot-api/substrate-bindings"
import { ComponentProps, FC, useMemo, useState } from "react"
import { twMerge } from "tailwind-merge"
import { EditCodec } from "../EditCodec"
import "./binaryDisplay.css"
type BinaryCodec = {
enc: (value: any | NOTIN) => Uint8Array
dec: (value: Uint8Array | HexString) => any | NOTIN
}
const toHex = (value: Uint8Array | HexString) =>
typeof value === "string" ? value : Binary.toHex(value)
export const BinaryDisplay: FC<
ComponentProps<typeof EditCodec> & {
codec: {
enc: (value: any | NOTIN) => Uint8Array
dec: (value: Uint8Array | HexString) => any | NOTIN
}
codec: BinaryCodec
className?: string
}
> = ({ codecType, metadata, value, onUpdate, codec, className }) => {
const [wrap, setWrap] = useState(false)
const encoded = (() => {
if (value.type === CodecComponentType.Initial) {
if (!value.value) return null
return typeof value.value === "string"
? fromHex(value.value)
: value.value
const [draftInputValue, setDraftInputValue] = useState<string | null>(null)
const { hex, isEmpty, updatedInputValue } = useMemo(() => {
const encoded = (() => {
if (value.type === CodecComponentType.Initial) {
return value.value ?? null
}
if (value.value.empty || !value.value.encoded) return null
return value.value.encoded
})()
const updatedInputValue =
value.type === CodecComponentType.Initial
? value.value
? toHex(value.value)
: ""
: value.value.empty
? ""
: value.value.encoded
? toHex(value.value.encoded)
: null
return {
hex: encoded ? toHex(encoded) : null,
isEmpty:
(value.type === CodecComponentType.Initial && !value.value) ||
(value.type === CodecComponentType.Updated && value.value.empty),
updatedInputValue,
}
if (value.value.empty || !value.value.encoded) return null
return value.value.encoded
})()
const hex = encoded ? toHex(encoded) : null
const isEmpty =
(value.type === CodecComponentType.Initial && !value.value) ||
(value.type === CodecComponentType.Updated && value.value.empty)
}, [value])
const inputValue = draftInputValue ?? updatedInputValue ?? ""
const { validity } = useMemo(
() => validateInputValue(inputValue, codec),
[codec, inputValue],
)
return (
<div className={twMerge("px-2 w-full", className)}>
<div className="px-3 py-2 gap-2 flex flex-row border-border border items-start">
<CopyText text={hex ?? ""} disabled={!hex} className="h-5" />
<div
className={twMerge(
"binary-display-codec",
"text-sm tabular-nums overflow-hidden flex-1",
wrap ? "wrap-break-word" : "whitespace-nowrap text-ellipsis h-5",
)}
>
{isEmpty ? (
<div className="flex flex-row items-center gap-1 text-slate-400">
Start by filling out the value, or enter a binary using the edit
binary button at the end of this line.
</div>
) : (
<>
0x
<BinaryViewCodec
codecType={codecType}
metadata={metadata}
value={value}
/>
</>
)}
<div className="text-sm tabular-nums overflow-hidden flex-1 group relative">
<input
className={twMerge(
"w-full opacity-0 focus:opacity-100 absolute left-0 top-0",
validity === "invalid" && "outline-red-600",
validity === "partial" && "outline-orange-400",
)}
value={inputValue}
aria-invalid={validity === "invalid" ? true : undefined}
autoCapitalize="none"
spellCheck={false}
onKeyDown={(evt) => {
const value = evt.currentTarget.value
if (evt.key === "Escape") {
setDraftInputValue(null)
} else if (evt.key === "Enter") {
if (value.trim() === "") {
onUpdate?.({
empty: true,
})
} else {
const validation = validateInputValue(value, codec)
if (
validation.validity === "identic" ||
validation.validity === "partial"
) {
onUpdate?.({
empty: false,
decoded: validation.decoded,
encoded: validation.encoded,
})
}
}
setDraftInputValue(null)
}
}}
onChange={(evt) => {
const value = evt.target.value
const validation = validateInputValue(value, codec)
if (validation.validity === "identic") {
onUpdate?.({
empty: false,
decoded: validation.decoded,
encoded: validation.encoded,
})
}
setDraftInputValue(value)
}}
onBlur={() => setDraftInputValue(null)}
/>
<div
className={twMerge(
"binary-display-codec",
"overflow-hidden group-focus-within:invisible",
wrap ? "wrap-break-word" : "whitespace-nowrap text-ellipsis h-5",
)}
>
{isEmpty ? (
<div className="flex flex-row items-center gap-1 text-slate-400">
Start by filling out the value, or enter a binary clicking here
</div>
) : (
<>
0x
<BinaryViewCodec
codecType={codecType}
metadata={metadata}
value={value}
/>
</>
)}
</div>
</div>
<div className="flex gap-2 items-center h-5">
<ExpandBtn
expanded={wrap}
direction="vertical"
className={twMerge(
"cursor-pointer",
isEmpty && "opacity-50 pointer-events-none",
)}
<button
type="button"
disabled={isEmpty}
className="flex h-5 items-center justify-center cursor-pointer disabled:cursor-default disabled:opacity-50"
onClick={() => setWrap((v) => !v)}
/>
<BinaryEditButton
initialValue={encoded ?? undefined}
onValueChange={(decoded) => {
const encoded = codec.enc(decoded)
onUpdate?.({ empty: false, decoded, encoded })
return true
}}
decode={codec.dec}
iconProps={{
size: 24,
}}
/>
>
<ExpandBtn
expanded={wrap}
direction="vertical"
aria-hidden="true"
className="pointer-events-none"
/>
</button>
</div>
</div>
</div>
)
}
const validateInputValue = (
value: string,
codec: BinaryCodec,
):
| { validity: "invalid" | "empty" }
| {
validity: "partial" | "identic"
decoded: any
encoded: Uint8Array
} => {
value = value.trim()
if (value.length < 4) return { validity: "empty" }
try {
const decoded = codec.dec(value)
const encoded = codec.enc(decoded)
return {
validity: toHex(encoded) === value ? "identic" : "partial",
decoded,
encoded,
}
} catch {
return { validity: "invalid" }
}
}

View File

@@ -92,12 +92,9 @@ export const useSyncHashParam = <T extends any[]>(
useLayoutEffect(
() =>
setHashParams(
{
[key]: getFn(...dependencies),
},
location,
),
setHashParams({
[key]: getFn(...dependencies),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
dependencies,
)