From ff934cf047673f25d329722813037acd57a65cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ti=E1=BA=BFn=20Nguy=E1=BB=85n=20Kh=E1=BA=AFc?= Date: Wed, 27 Nov 2024 22:14:56 +1300 Subject: [PATCH] feat: sequence item duplication --- src/components/account-select.tsx | 8 +++- src/components/param/sequence.tsx | 53 ++++++++++++++++++++----- src/routes/_layout/extrinsics.tsx | 64 +++++++++++++++++++------------ 3 files changed, 90 insertions(+), 35 deletions(-) diff --git a/src/components/account-select.tsx b/src/components/account-select.tsx index 838f46d..beb2035 100644 --- a/src/components/account-select.tsx +++ b/src/components/account-select.tsx @@ -2,7 +2,7 @@ import { AccountListItem } from "./account-list-item"; import { Select } from "./select"; import type { WalletAccount } from "@reactive-dot/core/wallets.js"; import { useAccounts } from "@reactive-dot/react"; -import { useState, type ReactNode } from "react"; +import { useEffect, useState, type ReactNode } from "react"; type ControlledAccountSelectProps = { accounts: WalletAccount[]; @@ -35,6 +35,12 @@ export function UnControlledAccountSelect({ const accounts = useAccounts(); const [account, setAccount] = useState(accounts.at(0)); + useEffect(() => { + if (account === undefined && accounts.length > 0) { + setAccount(accounts.at(0)); + } + }, [account, accounts]); + return ( <> {children({ diff --git a/src/components/param/sequence.tsx b/src/components/param/sequence.tsx index b8f0e69..01357ec 100644 --- a/src/components/param/sequence.tsx +++ b/src/components/param/sequence.tsx @@ -25,11 +25,13 @@ import { } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import type { + Decoded, SequenceDecoded, SequenceShape, } from "@polkadot-api/view-builder"; import AddIcon from "@w3f/polkadot-icons/solid/Add"; import CloseIcon from "@w3f/polkadot-icons/solid/Close"; +import CopyIcon from "@w3f/polkadot-icons/solid/Copy"; import MoreMenuIcon from "@w3f/polkadot-icons/solid/MoreMenu"; import { type PropsWithChildren, useEffect, useMemo, useState } from "react"; import { css } from "styled-system/css"; @@ -42,6 +44,7 @@ export type SequenceParamProps = ParamProps & { type SortableValue = { id: string; value: T; + defaultValue?: Decoded | undefined; }; export function SequenceParam({ @@ -51,15 +54,20 @@ export function SequenceParam({ }: SequenceParamProps) { const [length, setLength] = useState(defaultValue?.value.length ?? 1); - const [sortableValues, setSortableValues] = useState( - Array.from({ length }).map( - (): SortableValue> => ({ - id: globalThis.crypto.randomUUID(), - value: INCOMPLETE, - }), - ), + const defaultValues = useMemo( + () => + Array.from({ length }).map( + (_, index): SortableValue> => ({ + id: globalThis.crypto.randomUUID(), + value: INCOMPLETE, + defaultValue: defaultValue?.value.at(index), + }), + ), + [defaultValue?.value, length], ); + const [sortableValues, setSortableValues] = useState(defaultValues); + const values = useMemo( () => sortableValues.map((value) => value.value), [sortableValues], @@ -129,10 +137,22 @@ export function SequenceParam({ values.filter((value) => value.id !== item.id), ) } + onRequestDuplicate={() => { + setLength((length) => length + 1); + setSortableValues((values) => [ + ...values.slice(0, index + 1), + { + id: globalThis.crypto.randomUUID(), + value: values.at(index)?.value ?? INCOMPLETE, + defaultValue: defaultValues.at(index)?.defaultValue, + }, + ...values.slice(index + 1), + ]); + }} > setSortableValues((array) => array.with(index, { @@ -165,12 +185,14 @@ type SortableItemProps = PropsWithChildren<{ id: string; sortable?: boolean; onRequestRemove: () => void; + onRequestDuplicate: () => void; }>; export function SortableItem({ id, sortable, onRequestRemove, + onRequestDuplicate, children, }: SortableItemProps) { const { attributes, listeners, setNodeRef, transform, transition } = @@ -214,9 +236,22 @@ export function SortableItem({ )} - + + + + ); diff --git a/src/routes/_layout/extrinsics.tsx b/src/routes/_layout/extrinsics.tsx index 703c883..d9bd6e6 100644 --- a/src/routes/_layout/extrinsics.tsx +++ b/src/routes/_layout/extrinsics.tsx @@ -12,7 +12,7 @@ import { SignerProvider, useMutation, useSigner } from "@reactive-dot/react"; import { createFileRoute } from "@tanstack/react-router"; import SignATransactionIcon from "@w3f/polkadot-icons/solid/SignATransaction"; import { Binary } from "polkadot-api"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { css } from "styled-system/css"; import { Select } from "~/components/select"; import { Button } from "~/components/ui/button"; @@ -69,11 +69,6 @@ function CallParam({ const dynamicBuilder = useDynamicBuilder(); const viewBuilder = useViewBuilder(); - const [defaultArgs, setDefaultArgs] = useState<{ - id: string; - decoded: Decoded; - }>(); - const callData = useMemo(() => { if (args === INCOMPLETE || args === INVALID) { return undefined; @@ -95,24 +90,51 @@ function CallParam({ const callDataHex = callData?.asHex(); + const [defaultArgs, setDefaultArgs] = useState(); + const [argsRenderCount, setArgsRenderCount] = useState(0); + const [draftCallDataInput, setDraftCallDataInput] = useState( - callData?.asHex() ?? "", + callDataHex ?? "", ); - const [callDataInput, setCallDataInput] = useState(draftCallDataInput); - useEffect(() => { - if (callDataHex !== undefined) { - setDraftCallDataInput(callDataHex); - setCallDataInput(callDataHex); - } - }, [callDataHex]); + const [callDataInput, _setCallDataInput] = useState(draftCallDataInput); + + const setCallDataInput = useCallback( + (callDataInput: string, forceRerender?: boolean) => { + if (callDataInput.trim() === "") { + return; + } + + const decodedCall = viewBuilder.callDecoder(callDataInput); + + onChangePallet(decodedCall.pallet.value.idx); + onChangeCall(decodedCall.call.value.name); + setDefaultArgs(decodedCall.args.value); + + if (forceRerender) { + setArgsRenderCount((count) => count + 1); + } + }, + [onChangeCall, onChangePallet, viewBuilder], + ); + + useEffect( + () => { + if (callDataHex !== undefined) { + setDraftCallDataInput(callDataHex); + setCallDataInput(callDataHex); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [callDataHex], + ); return (

@@ -124,15 +146,7 @@ function CallParam({ onValueRevert={() => setDraftCallDataInput(callDataInput)} onValueCommit={(event) => { try { - const decodedCall = viewBuilder.callDecoder(event.value); - - onChangePallet(decodedCall.pallet.value.idx); - onChangeCall(decodedCall.call.value.name); - setDefaultArgs({ - id: globalThis.crypto.randomUUID(), - decoded: decodedCall.args.value, - }); - setCallDataInput(draftCallDataInput); + setCallDataInput(event.value, true); } catch { setDraftCallDataInput(callDataInput); toaster.error({ title: "Invalid call data" });