Skip to content

Commit

Permalink
Improve structs UI (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
technophile-04 authored Feb 25, 2024
1 parent de4a698 commit 55427e3
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 62 deletions.
33 changes: 27 additions & 6 deletions packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Dispatch, SetStateAction } from "react";
import { Tuple } from "./Tuple";
import { TupleArray } from "./TupleArray";
import { AbiParameter } from "abitype";
import {
AddressInput,
Expand All @@ -8,6 +10,7 @@ import {
IntegerInput,
IntegerVariant,
} from "~~/components/scaffold-eth";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";

type ContractInputProps = {
setForm: Dispatch<SetStateAction<Record<string, any>>>;
Expand All @@ -32,28 +35,46 @@ export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: Cont
const renderInput = () => {
switch (paramType.type) {
case "address":
const addressInputProps = { ...inputProps, placeholder: "address or ENS" };
return <AddressInput {...addressInputProps} />;
return <AddressInput {...inputProps} />;
case "bytes32":
return <Bytes32Input {...inputProps} />;
case "bytes":
return <BytesInput {...inputProps} />;
case "string":
return <InputBase {...inputProps} />;
case "tuple":
return (
<Tuple
setParentForm={setForm}
parentForm={form}
abiTupleParameter={paramType as AbiParameterTuple}
parentStateObjectKey={stateObjectKey}
/>
);
default:
// Handling 'int' types and 'tuple[]' types
if (paramType.type.includes("int") && !paramType.type.includes("[")) {
return <IntegerInput {...inputProps} variant={paramType.type as IntegerVariant} />;
} else if (paramType.type.startsWith("tuple[")) {
return (
<TupleArray
setParentForm={setForm}
parentForm={form}
abiTupleParameter={paramType as AbiParameterTuple}
parentStateObjectKey={stateObjectKey}
/>
);
} else {
return <InputBase {...inputProps} />;
}
}
};

return (
<div className="flex flex-col gap-1 w-full">
<div className="flex items-center">
{paramType.name && <span className="text-xs font-medium mr-2">{paramType.name}</span>}
<span className="block text-xs font-extralight">{paramType.type}</span>
<div className="flex flex-col gap-1.5 w-full">
<div className="flex items-center ml-2">
{paramType.name && <span className="text-xs font-medium mr-2 leading-none">{paramType.name}</span>}
<span className="block text-xs font-extralight leading-none">{paramType.type}</span>
</div>
{renderInput()}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getInitialFormState,
getParsedContractFunctionArgs,
getParsedError,
transformAbiFunction,
} from "~~/components/scaffold-eth";
import { useAbiNinjaState } from "~~/services/store/store";
import { notification } from "~~/utils/scaffold-eth";
Expand Down Expand Up @@ -44,7 +45,8 @@ export const ReadOnlyFunctionForm = ({
},
});

const inputElements = abiFunction.inputs.map((input, inputIndex) => {
const transformedFunction = transformAbiFunction(abiFunction);
const inputElements = transformedFunction.inputs.map((input, inputIndex) => {
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
return (
<ContractInput
Expand Down
44 changes: 44 additions & 0 deletions packages/nextjs/components/scaffold-eth/Contract/Tuple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { ContractInput } from "./ContractInput";
import { getFunctionInputKey, getInitalTupleFormState } from "./utilsContract";
import { replacer } from "~~/utils/scaffold-eth/common";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";

type TupleProps = {
abiTupleParameter: AbiParameterTuple;
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
parentStateObjectKey: string;
parentForm: Record<string, any> | undefined;
};

export const Tuple = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleProps) => {
const [form, setForm] = useState<Record<string, any>>(() => getInitalTupleFormState(abiTupleParameter));

useEffect(() => {
const values = Object.values(form);
const argsStruct: Record<string, any> = {};
abiTupleParameter.components.forEach((component, componentIndex) => {
argsStruct[component.name || `input_${componentIndex}_`] = values[componentIndex];
});

setParentForm(parentForm => ({ ...parentForm, [parentStateObjectKey]: JSON.stringify(argsStruct, replacer) }));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(form, replacer)]);

return (
<div>
<div className="collapse collapse-arrow pl-4 py-1.5 border-2 border-secondary overflow-x-auto">
<input type="checkbox" className="min-h-fit peer" />
<div className="collapse-title p-0 min-h-fit peer-checked:mb-2 text-secondary-content/70">
<p className="m-0 p-0 text-[1rem]">{abiTupleParameter.internalType}</p>
</div>
<div className="ml-3 flex-col space-y-4 border-secondary/80 border-l-2 pl-4 collapse-content">
{abiTupleParameter?.components?.map((param, index) => {
const key = getFunctionInputKey(abiTupleParameter.name || "tuple", param, index);
return <ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />;
})}
</div>
</div>
</div>
);
};
139 changes: 139 additions & 0 deletions packages/nextjs/components/scaffold-eth/Contract/TupleArray.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { ContractInput } from "./ContractInput";
import { getFunctionInputKey, getInitalTupleArrayFormState } from "./utilsContract";
import { replacer } from "~~/utils/scaffold-eth/common";
import { AbiParameterTuple } from "~~/utils/scaffold-eth/contract";

type TupleArrayProps = {
abiTupleParameter: AbiParameterTuple & { isVirtual?: true };
setParentForm: Dispatch<SetStateAction<Record<string, any>>>;
parentStateObjectKey: string;
parentForm: Record<string, any> | undefined;
};

export const TupleArray = ({ abiTupleParameter, setParentForm, parentStateObjectKey }: TupleArrayProps) => {
const [form, setForm] = useState<Record<string, any>>(() => getInitalTupleArrayFormState(abiTupleParameter));
const [additionalInputs, setAdditionalInputs] = useState<Array<typeof abiTupleParameter.components>>([
abiTupleParameter.components,
]);

const depth = (abiTupleParameter.type.match(/\[\]/g) || []).length;

useEffect(() => {
// Extract and group fields based on index prefix
const groupedFields = Object.keys(form).reduce((acc, key) => {
const [indexPrefix, ...restArray] = key.split("_");
const componentName = restArray.join("_");
if (!acc[indexPrefix]) {
acc[indexPrefix] = {};
}
acc[indexPrefix][componentName] = form[key];
return acc;
}, {} as Record<string, Record<string, any>>);

let argsArray: Array<Record<string, any>> = [];

Object.keys(groupedFields).forEach(key => {
const currentKeyValues = Object.values(groupedFields[key]);

const argsStruct: Record<string, any> = {};
abiTupleParameter.components.forEach((component, componentIndex) => {
argsStruct[component.name || `input_${componentIndex}_`] = currentKeyValues[componentIndex];
});

argsArray.push(argsStruct);
});

if (depth > 1) {
argsArray = argsArray.map(args => {
return args[abiTupleParameter.components[0].name || "tuple"];
});
}

setParentForm(parentForm => {
return { ...parentForm, [parentStateObjectKey]: JSON.stringify(argsArray, replacer) };
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(form, replacer)]);

const addInput = () => {
setAdditionalInputs(previousValue => {
const newAdditionalInputs = [...previousValue, abiTupleParameter.components];

// Add the new inputs to the form
setForm(form => {
const newForm = { ...form };
abiTupleParameter.components.forEach((component, componentIndex) => {
const key = getFunctionInputKey(
`${newAdditionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
component,
componentIndex,
);
newForm[key] = "";
});
return newForm;
});

return newAdditionalInputs;
});
};

const removeInput = () => {
// Remove the last inputs from the form
setForm(form => {
const newForm = { ...form };
abiTupleParameter.components.forEach((component, componentIndex) => {
const key = getFunctionInputKey(
`${additionalInputs.length - 1}_${abiTupleParameter.name || "tuple"}`,
component,
componentIndex,
);
delete newForm[key];
});
return newForm;
});
setAdditionalInputs(inputs => inputs.slice(0, -1));
};

return (
<div>
<div className="collapse collapse-arrow pl-4 py-1.5 border-2 border-secondary overflow-x-auto">
<input type="checkbox" className="min-h-fit peer" />
<div className="collapse-title p-0 min-h-fit peer-checked:mb-1 text-secondary-content/70">
<p className="m-0 text-[1rem]">{abiTupleParameter.internalType}</p>
</div>
<div className="ml-3 flex-col space-y-2 border-secondary/70 border-l-2 pl-4 collapse-content">
{additionalInputs.map((additionalInput, additionalIndex) => (
<div key={additionalIndex} className="space-y-1">
<span className="badge bg-secondary/60 badge-sm">
{depth > 1 ? `${additionalIndex}` : `tuple[${additionalIndex}]`}
</span>
<div className="space-y-4">
{additionalInput.map((param, index) => {
const key = getFunctionInputKey(
`${additionalIndex}_${abiTupleParameter.name || "tuple"}`,
param,
index,
);
return (
<ContractInput setForm={setForm} form={form} key={key} stateObjectKey={key} paramType={param} />
);
})}
</div>
</div>
))}
<div className="flex space-x-2">
<button className="btn btn-sm btn-secondary" onClick={addInput}>
+
</button>
{additionalInputs.length > 0 && (
<button className="btn btn-sm btn-secondary" onClick={removeInput}>
-
</button>
)}
</div>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getInitialFormState,
getParsedContractFunctionArgs,
getParsedError,
transformAbiFunction,
} from "~~/components/scaffold-eth";
import { useTransactor } from "~~/hooks/scaffold-eth";
import { useAbiNinjaState } from "~~/services/store/store";
Expand Down Expand Up @@ -75,7 +76,8 @@ export const WriteOnlyFunctionForm = ({
}, [txResult]);

// TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm
const inputs = abiFunction.inputs.map((input, inputIndex) => {
const transformedFunction = transformAbiFunction(abiFunction);
const inputs = transformedFunction.inputs.map((input, inputIndex) => {
const key = getFunctionInputKey(abiFunction.name, input, inputIndex);
return (
<ContractInput
Expand All @@ -101,18 +103,18 @@ export const WriteOnlyFunctionForm = ({
</p>
{inputs}
{abiFunction.stateMutability === "payable" ? (
<div className="flex flex-col gap-1 w-full pl-2">
<div className="flex items-center">
<span className="text-xs font-medium mr-2">value</span>
<span className="block text-xs font-thin">(wei)</span>
<div className="flex flex-col gap-1.5 w-full">
<div className="flex items-center ml-2">
<span className="text-xs font-medium mr-2 leading-none">payable value</span>
<span className="block text-xs font-extralight leading-none">wei</span>
</div>
<IntegerInput
value={txValue}
onChange={updatedTxValue => {
setDisplayedTxResult(undefined);
setTxValue(updatedTxValue);
}}
placeholder="wei"
placeholder="value (wei)"
/>
</div>
) : null}
Expand Down
Loading

0 comments on commit 55427e3

Please sign in to comment.