diff --git a/packages/page-rpc/src/Rpc/Selection.tsx b/packages/page-rpc/src/Rpc/Selection.tsx index 0457a21bb5..631e4b385b 100644 --- a/packages/page-rpc/src/Rpc/Selection.tsx +++ b/packages/page-rpc/src/Rpc/Selection.tsx @@ -72,7 +72,7 @@ function Selection ({ queueRpc }: Props): React.ReactElement { (): void => queueRpc({ rpc, values: values - .filter(({ value }) => !isNull(value)) + .filter(({ value }, idx) => !rpc.params[idx].isOptional || !isNull(value)) .map(({ value }): any => value) }), [queueRpc, rpc, values] diff --git a/packages/react-params/src/Holder.tsx b/packages/react-params/src/Holder.tsx index e378694780..1ebec55078 100644 --- a/packages/react-params/src/Holder.tsx +++ b/packages/react-params/src/Holder.tsx @@ -98,6 +98,10 @@ const StyledDiv = styled.div` text-align: right; } + .ui--Param-BTreeMap-buttons { + text-align: right; + } + .ui--Param-composite { position: relative; diff --git a/packages/react-params/src/Param/BTreeMap.tsx b/packages/react-params/src/Param/BTreeMap.tsx new file mode 100644 index 0000000000..b551738889 --- /dev/null +++ b/packages/react-params/src/Param/BTreeMap.tsx @@ -0,0 +1,160 @@ +// Copyright 2017-2024 @polkadot/react-params authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ParamDef, Props, RawParam } from '../types.js'; + +import React, { useCallback, useEffect, useState } from 'react'; + +import { Button } from '@polkadot/react-components'; +import { BTreeMap } from '@polkadot/types'; +import { isCodec, isUndefined } from '@polkadot/util'; + +import Params from '../index.js'; +import getInitValue from '../initValue.js'; +import { useTranslation } from '../translate.js'; +import Base from './Base.js'; +import useParamDefs from './useParamDefs.js'; + +function getParamType ([key, value]: ParamDef[]): ParamDef[] { + return [{ + name: '(Key, Value)', + type: { + info: 17, + sub: [key.type, value.type], + type: `(${key.type.type}, ${value.type.type})` + } + }]; +} + +function getParam ([{ name, type }]: ParamDef[], index: number): ParamDef { + return { + name: `${index}: ${name || type.type}`, + type + }; +} + +export function getParams (keyValueParam: ParamDef[], prev: ParamDef[], max: number): ParamDef[] { + if (prev.length === max) { + return prev; + } + + const params: ParamDef[] = []; + + for (let index = 0; index < max; index++) { + params.push(getParam(keyValueParam, index)); + } + + return params; +} + +export function getValues ({ isValid, value }: RawParam): RawParam[] { + return (isValid && isCodec(value) && value instanceof BTreeMap) + ? [...value.entries()].map(([key, value]: RawParam[]) => { + return { + isValid: true, + value: [{ isValid: true, value: key }, { isValid: true, value }] + }; + }) + : []; +} + +function BTreeMapParam ({ className = '', defaultValue, isDisabled = false, label, onChange, overrides, registry, type, withLabel }: Props): React.ReactElement | null { + const { t } = useTranslation(); + const keyValueParam = getParamType(useParamDefs(registry, type)); + const [values, setValues] = useState(() => getValues(defaultValue)); + const [count, setCount] = useState(() => values.length); + const [params, setParams] = useState(() => getParams(keyValueParam, [], count)); + + // build up the list of parameters we are using + useEffect((): void => { + keyValueParam.length && + setParams((prev) => + getParams(keyValueParam, prev, isDisabled ? values.length : count) + ); + }, [count, values, isDisabled, keyValueParam]); + + // when !isDisable, generating an input list based on count + useEffect((): void => { + !isDisabled && keyValueParam.length && + setValues((values): RawParam[] => { + if (values.length === count) { + return values; + } + + while (values.length < count) { + const value = getInitValue(registry, keyValueParam[0].type); + + values.push({ isValid: !isUndefined(value), value }); + } + + return values.slice(0, count); + }); + }, [count, defaultValue, keyValueParam, isDisabled, registry]); + + // when our values has changed, alert upstream + useEffect((): void => { + const output = new Map(); + let isValid = true; + + for (const entry of values) { + const [key, value] = entry.value as RawParam[]; + + if (output.has(key)) { + isValid = false; + console.error('BTreeMap: Duplicate key ', key); + } + + output.set(key, value); + isValid = isValid && entry.isValid; + } + + onChange && onChange({ + isValid, + value: output + }); + }, [values, onChange]); + + const _rowAdd = useCallback( + (): void => setCount((count) => count + 1), + [] + ); + const _rowRemove = useCallback( + (): void => setCount((count) => count - 1), + [] + ); + + return ( + + {!isDisabled && ( +
+
+ )} + + + ); +} + +export default React.memo(BTreeMapParam); diff --git a/packages/react-params/src/Param/Vector.tsx b/packages/react-params/src/Param/Vector.tsx index 5dc30a9824..07658b83f0 100644 --- a/packages/react-params/src/Param/Vector.tsx +++ b/packages/react-params/src/Param/Vector.tsx @@ -22,8 +22,7 @@ function getParam ([{ name, type }]: ParamDef[], index: number): ParamDef { } export function getParams (inputParams: ParamDef[], prev: ParamDef[], max: number): ParamDef[] { - // HACK: I don't know why `max` changes to `undefined`. - if (prev.length === max || isUndefined(max)) { + if (prev.length === max) { return prev; } @@ -61,9 +60,9 @@ function Vector ({ className = '', defaultValue, isDisabled = false, label, onCh useEffect((): void => { inputParams.length && setParams((prev) => - getParams(inputParams, prev, isDisabled ? (defaultValue.value as RawParam[] || []).length : count) + getParams(inputParams, prev, isDisabled ? values.length : count) ); - }, [count, defaultValue, isDisabled, inputParams]); + }, [count, values, isDisabled, inputParams]); // when !isDisable, generating an input list based on count useEffect((): void => { diff --git a/packages/react-params/src/Param/findComponent.ts b/packages/react-params/src/Param/findComponent.ts index 2ee2ad1d2b..f569dce528 100644 --- a/packages/react-params/src/Param/findComponent.ts +++ b/packages/react-params/src/Param/findComponent.ts @@ -15,6 +15,7 @@ import Balance from './Balance.js'; import AccountId20 from './BasicAccountId20.js'; import AccountId32 from './BasicAccountId32.js'; import Bool from './Bool.js'; +import BTreeMap from './BTreeMap.js'; import Bytes from './Bytes.js'; import Call from './Call.js'; import Cid from './Cid.js'; @@ -76,6 +77,7 @@ const componentDef: TypeToComponent[] = [ { c: Text, t: ['String', 'Text'] }, { c: Struct, t: ['Struct'] }, { c: Tuple, t: ['Tuple'] }, + { c: BTreeMap, t: ['BTreeMap'] }, { c: Vector, t: ['Vec', 'BTreeSet'] }, { c: VectorFixed, t: ['VecFixed'] }, { c: Vote, t: ['Vote'] }, @@ -126,6 +128,9 @@ function fromDef ({ displayName, info, lookupName, sub, type }: TypeDef): string case TypeDefInfo.BTreeSet: return 'BTreeSet'; + case TypeDefInfo.BTreeMap: + return 'BTreeMap'; + case TypeDefInfo.Tuple: return components[type] === Account ? type