Skip to content

Commit

Permalink
Contract write: unable to add elements to a top level array argument …
Browse files Browse the repository at this point in the history
…field (blockscout#1848)

* fix form for top level nested arrays

* fix labels for fixed size arrays

* refactoring

* adjust test
  • Loading branch information
tom2drum authored May 1, 2024
1 parent 5c3e1be commit 67c6404
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 91 deletions.
4 changes: 2 additions & 2 deletions ui/address/contract/methodForm/ContractMethodFieldInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import ClearButton from 'ui/shared/ClearButton';

import ContractMethodFieldLabel from './ContractMethodFieldLabel';
import ContractMethodMultiplyButton from './ContractMethodMultiplyButton';
import useArgTypeMatchInt from './useArgTypeMatchInt';
import useFormatFieldValue from './useFormatFieldValue';
import useValidateField from './useValidateField';
import { matchInt } from './utils';

interface Props {
data: SmartContractMethodInput;
Expand All @@ -28,7 +28,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
const isNativeCoin = data.fieldType === 'native_coin';
const isOptional = isNativeCoin;

const argTypeMatchInt = useArgTypeMatchInt({ argType: data.type });
const argTypeMatchInt = React.useMemo(() => matchInt(data.type), [ data.type ]);
const validate = useValidateField({ isOptional, argType: data.type, argTypeMatchInt });
const format = useFormatFieldValue({ argType: data.type, argTypeMatchInt });

Expand Down
122 changes: 69 additions & 53 deletions ui/address/contract/methodForm/ContractMethodFieldInputArray.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,43 @@ import { Flex } from '@chakra-ui/react';
import React from 'react';
import { useFormContext } from 'react-hook-form';

import type { SmartContractMethodInput, SmartContractMethodArgType } from 'types/api/contract';
import type { SmartContractMethodInput } from 'types/api/contract';

import ContractMethodArrayButton from './ContractMethodArrayButton';
import type { Props as AccordionProps } from './ContractMethodFieldAccordion';
import ContractMethodFieldAccordion from './ContractMethodFieldAccordion';
import ContractMethodFieldInput from './ContractMethodFieldInput';
import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple';
import ContractMethodFieldLabel from './ContractMethodFieldLabel';
import { getFieldLabel } from './utils';
import { getFieldLabel, matchArray, transformDataForArrayItem } from './utils';

interface Props extends Pick<AccordionProps, 'onAddClick' | 'onRemoveClick' | 'index'> {
data: SmartContractMethodInput;
level: number;
basePath: string;
isDisabled: boolean;
isArrayElement?: boolean;
size?: number;
}

const ContractMethodFieldInputArray = ({ data, level, basePath, onAddClick, onRemoveClick, index: parentIndex, isDisabled }: Props) => {
const ContractMethodFieldInputArray = ({
data,
level,
basePath,
onAddClick,
onRemoveClick,
index: parentIndex,
isDisabled,
isArrayElement,
}: Props) => {
const { formState: { errors } } = useFormContext();
const fieldsWithErrors = Object.keys(errors);
const isInvalid = fieldsWithErrors.some((field) => field.startsWith(basePath));

const [ registeredIndices, setRegisteredIndices ] = React.useState([ 0 ]);
const arrayMatch = matchArray(data.type);
const hasFixedSize = arrayMatch !== null && arrayMatch.size !== Infinity;

const [ registeredIndices, setRegisteredIndices ] = React.useState(hasFixedSize ? Array(arrayMatch.size).fill(0).map((_, i) => i) : [ 0 ]);

const handleAddButtonClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
Expand All @@ -39,52 +53,69 @@ const ContractMethodFieldInputArray = ({ data, level, basePath, onAddClick, onRe
}
}, [ ]);

const getItemData = (index: number) => {
const childrenType = data.type.slice(0, -2) as SmartContractMethodArgType;
const childrenInternalType = data.internalType?.slice(0, parentIndex !== undefined ? -4 : -2).replaceAll('struct ', '');

const namePostfix = childrenInternalType ? ' ' + childrenInternalType : '';
const nameParentIndex = parentIndex !== undefined ? `${ parentIndex + 1 }.` : '';
const nameIndex = index + 1;
if (arrayMatch?.isNested) {
return (
<>
{
registeredIndices.map((registeredIndex, index) => {
const itemData = transformDataForArrayItem(data, index);
const itemBasePath = `${ basePath }:${ registeredIndex }`;
const itemIsInvalid = fieldsWithErrors.some((field) => field.startsWith(itemBasePath));

return (
<ContractMethodFieldAccordion
key={ registeredIndex }
level={ level + 1 }
label={ getFieldLabel(itemData) }
isInvalid={ itemIsInvalid }
onAddClick={ !hasFixedSize && index === registeredIndices.length - 1 ? handleAddButtonClick : undefined }
onRemoveClick={ !hasFixedSize && registeredIndices.length > 1 ? handleRemoveButtonClick : undefined }
index={ registeredIndex }
>
<ContractMethodFieldInputArray
key={ registeredIndex }
data={ itemData }
basePath={ itemBasePath }
level={ level + 1 }
isDisabled={ isDisabled }
isArrayElement
/>
</ContractMethodFieldAccordion>
);
})
}
</>
);
}

return {
...data,
type: childrenType,
name: `#${ nameParentIndex + nameIndex }${ namePostfix }`,
};
};
const isNestedArray = data.type.includes('[][]');
const isTupleArray = arrayMatch?.itemType.includes('tuple');

if (isNestedArray) {
return (
<ContractMethodFieldAccordion
level={ level }
label={ getFieldLabel(data) }
isInvalid={ isInvalid }
>
if (isTupleArray) {
const content = (
<>
{ registeredIndices.map((registeredIndex, index) => {
const itemData = getItemData(index);
const itemData = transformDataForArrayItem(data, index);

return (
<ContractMethodFieldInputArray
<ContractMethodFieldInputTuple
key={ registeredIndex }
data={ itemData }
basePath={ `${ basePath }:${ registeredIndex }` }
level={ level + 1 }
onAddClick={ index === registeredIndices.length - 1 ? handleAddButtonClick : undefined }
onRemoveClick={ registeredIndices.length > 1 ? handleRemoveButtonClick : undefined }
onAddClick={ !hasFixedSize && index === registeredIndices.length - 1 ? handleAddButtonClick : undefined }
onRemoveClick={ !hasFixedSize && registeredIndices.length > 1 ? handleRemoveButtonClick : undefined }
index={ registeredIndex }
isDisabled={ isDisabled }
/>
);
}) }
</ContractMethodFieldAccordion>
</>
);
}

const isTupleArray = data.type.includes('tuple');
if (isArrayElement) {
return content;
}

if (isTupleArray) {
return (
<ContractMethodFieldAccordion
level={ level }
Expand All @@ -94,33 +125,18 @@ const ContractMethodFieldInputArray = ({ data, level, basePath, onAddClick, onRe
index={ parentIndex }
isInvalid={ isInvalid }
>
{ registeredIndices.map((registeredIndex, index) => {
const itemData = getItemData(index);

return (
<ContractMethodFieldInputTuple
key={ registeredIndex }
data={ itemData }
basePath={ `${ basePath }:${ registeredIndex }` }
level={ level + 1 }
onAddClick={ index === registeredIndices.length - 1 ? handleAddButtonClick : undefined }
onRemoveClick={ registeredIndices.length > 1 ? handleRemoveButtonClick : undefined }
index={ registeredIndex }
isDisabled={ isDisabled }
/>
);
}) }
{ content }
</ContractMethodFieldAccordion>
);
}

// primitive value array
return (
<Flex flexDir={{ base: 'column', md: 'row' }} alignItems="flex-start" columnGap={ 3 } px="6px">
<ContractMethodFieldLabel data={ data } level={ level }/>
{ !isArrayElement && <ContractMethodFieldLabel data={ data } level={ level }/> }
<Flex flexDir="column" rowGap={ 1 } w="100%">
{ registeredIndices.map((registeredIndex, index) => {
const itemData = getItemData(index);
const itemData = transformDataForArrayItem(data, index);

return (
<Flex key={ registeredIndex } alignItems="flex-start" columnGap={ 3 }>
Expand All @@ -132,9 +148,9 @@ const ContractMethodFieldInputArray = ({ data, level, basePath, onAddClick, onRe
px={ 0 }
isDisabled={ isDisabled }
/>
{ registeredIndices.length > 1 &&
{ !hasFixedSize && registeredIndices.length > 1 &&
<ContractMethodArrayButton index={ registeredIndex } onClick={ handleRemoveButtonClick } type="remove" my="6px"/> }
{ index === registeredIndices.length - 1 &&
{ !hasFixedSize && index === registeredIndices.length - 1 &&
<ContractMethodArrayButton index={ registeredIndex } onClick={ handleAddButtonClick } type="add" my="6px"/> }
</Flex>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Props as AccordionProps } from './ContractMethodFieldAccordion';
import ContractMethodFieldAccordion from './ContractMethodFieldAccordion';
import ContractMethodFieldInput from './ContractMethodFieldInput';
import ContractMethodFieldInputArray from './ContractMethodFieldInputArray';
import { ARRAY_REGEXP, getFieldLabel } from './utils';
import { getFieldLabel, matchArray } from './utils';

interface Props extends Pick<AccordionProps, 'onAddClick' | 'onRemoveClick' | 'index'> {
data: SmartContractMethodInput;
Expand Down Expand Up @@ -41,15 +41,14 @@ const ContractMethodFieldInputTuple = ({ data, basePath, level, isDisabled, ...a
);
}

const arrayMatch = component.type.match(ARRAY_REGEXP);
const arrayMatch = matchArray(component.type);
if (arrayMatch) {
const [ , itemType ] = arrayMatch;
return (
<ContractMethodFieldInputArray
key={ index }
data={ component }
basePath={ `${ basePath }:${ index }` }
level={ itemType === 'tuple' ? level + 1 : level }
level={ arrayMatch.itemType === 'tuple' ? level + 1 : level }
isDisabled={ isDisabled }
/>
);
Expand Down
13 changes: 12 additions & 1 deletion ui/address/contract/methodForm/ContractMethodForm.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ const data: SmartContractWriteMethod = {
type: 'tuple[][]',
},

// TOP LEVEL NESTED ARRAY
{
internalType: 'int256[2][][3]',
name: 'ParentArray',
type: 'int256[2][][3]',
},

// LITERALS
{ internalType: 'bytes32', name: 'fulfillerConduitKey', type: 'bytes32' },
{ internalType: 'address', name: 'recipient', type: 'address' },
Expand Down Expand Up @@ -125,9 +132,13 @@ test('base view +@mobile +@dark-mode', async({ mount }) => {
await component.getByText('struct FulfillmentComponent[][]').click();
await component.getByRole('button', { name: 'add' }).nth(1).click();
await component.getByText('#1 FulfillmentComponent[]').click();
await component.getByText('#1.1 FulfillmentComponent').click();
await component.getByLabel('#1 FulfillmentComponent[] (tuple[])').getByText('#1 FulfillmentComponent (tuple)').click();
await component.getByRole('button', { name: 'add' }).nth(1).click();

await component.getByText('ParentArray (int256[2][][3])').click();
await component.getByText('#1 int256[2][] (int256[2][])').click();
await component.getByLabel('#1 int256[2][] (int256[2][])').getByText('#1 int256[2] (int256[2])').click();

// submit form
await component.getByRole('button', { name: 'Write' }).click();

Expand Down
22 changes: 20 additions & 2 deletions ui/address/contract/methodForm/ContractMethodForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import type { SmartContractMethod, SmartContractMethodInput } from 'types/api/co
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index';

import ContractMethodFieldAccordion from './ContractMethodFieldAccordion';
import ContractMethodFieldInput from './ContractMethodFieldInput';
import ContractMethodFieldInputArray from './ContractMethodFieldInputArray';
import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple';
import ContractMethodFormOutputs from './ContractMethodFormOutputs';
import { ARRAY_REGEXP, transformFormDataToMethodArgs } from './utils';
import { getFieldLabel, matchArray, transformFormDataToMethodArgs } from './utils';
import type { ContractMethodFormFields } from './utils';

interface Props<T extends SmartContractMethod> {
Expand Down Expand Up @@ -97,8 +98,25 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res
return <ContractMethodFieldInputTuple key={ index } data={ input } basePath={ `${ index }` } level={ 0 } isDisabled={ isLoading }/>;
}

const arrayMatch = input.type.match(ARRAY_REGEXP);
const arrayMatch = matchArray(input.type);

if (arrayMatch) {
if (arrayMatch.isNested) {
const fieldsWithErrors = Object.keys(formApi.formState.errors);
const isInvalid = fieldsWithErrors.some((field) => field.startsWith(index + ':'));

return (
<ContractMethodFieldAccordion
key={ index }
level={ 0 }
label={ getFieldLabel(input) }
isInvalid={ isInvalid }
>
<ContractMethodFieldInputArray data={ input } basePath={ `${ index }` } level={ 0 } isDisabled={ isLoading }/>
</ContractMethodFieldAccordion>
);
}

return <ContractMethodFieldInputArray key={ index } data={ input } basePath={ `${ index }` } level={ 0 } isDisabled={ isLoading }/>;
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 0 additions & 26 deletions ui/address/contract/methodForm/useArgTypeMatchInt.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion ui/address/contract/methodForm/useFormatFieldValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';

import type { SmartContractMethodArgType } from 'types/api/contract';

import type { MatchInt } from './useArgTypeMatchInt';
import type { MatchInt } from './utils';

interface Params {
argType: SmartContractMethodArgType;
Expand Down
2 changes: 1 addition & 1 deletion ui/address/contract/methodForm/useValidateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getAddress, isAddress, isHex } from 'viem';

import type { SmartContractMethodArgType } from 'types/api/contract';

import type { MatchInt } from './useArgTypeMatchInt';
import type { MatchInt } from './utils';
import { BYTES_REGEXP } from './utils';

interface Params {
Expand Down
Loading

0 comments on commit 67c6404

Please sign in to comment.