Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial abi form #524

Merged
merged 13 commits into from
Sep 22, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Features

- [#524](https://github.com/alleslabs/celatone-frontend/pull/524) Add initia abi form
- [#530](https://github.com/alleslabs/celatone-frontend/pull/530) Publish module component state wireup and add leaflet
- [#521](https://github.com/alleslabs/celatone-frontend/pull/521) Initia module interaction function panel and selected function info accordion
- [#515](https://github.com/alleslabs/celatone-frontend/pull/515) Initia select module drawer wireup
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@emotion/styled": "^11",
"@graphql-codegen/cli": "^2.13.12",
"@graphql-codegen/client-preset": "^1.1.4",
"@initia/initia.js": "^0.1.8",
"@rjsf/chakra-ui": "v5.0.0-beta.10",
"@rjsf/core": "v5.0.0-beta.10",
"@rjsf/utils": "v5.0.0-beta.10",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/app-provider/hooks/useAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const validateAddress = (
getAddressTypeByLength: GetAddressTypeByLengthFn
) => {
if (!bech32Prefix)
return "Can not retrieve bech32 prefix of the current network.";
return "Cannot retrieve bech32 prefix of the current network.";

const prefix = getPrefix(bech32Prefix, addressType);

Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/AddressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const AddressInput = <T extends FieldValues>({
label={label}
placeholder={placeholder ?? exampleUserAddress}
type="text"
variant="floating"
variant="fixed-floating"
status={status}
labelBgColor={labelBgColor}
helperText={helperText}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/OffChainForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const OffChainForm = <T extends OffchainDetail>({
label="Name"
placeholder={contractLabel}
helperText="Set name for your contract"
variant="floating"
variant="fixed-floating"
rules={{
maxLength: constants.maxContractNameLength,
}}
Expand All @@ -60,7 +60,7 @@ export const OffChainForm = <T extends OffchainDetail>({
control={control}
label="Description"
placeholder="Help understanding what this contract do and how it works ..."
variant="floating"
variant="fixed-floating"
rules={{
maxLength: constants.maxContractDescriptionLength,
}}
Expand Down
53 changes: 53 additions & 0 deletions src/lib/components/abi/AbiForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Flex } from "@chakra-ui/react";
import { useForm } from "react-hook-form";

import type { AbiFormData, ExposedFunction } from "lib/types";

import { ArgsForm } from "./args-form";
import { TypesForm } from "./types-form";

interface AbiFormProps {
fn: ExposedFunction;
initialData: AbiFormData;
propsOnChange?: (data: AbiFormData) => void;
propsOnErrors?: (errors: [string, string][]) => void;
}

export const AbiForm = ({
fn,
initialData,
propsOnChange,
propsOnErrors,
}: AbiFormProps) => {
const { setValue, watch, getValues } = useForm<AbiFormData>({
defaultValues: initialData,
mode: "all",
});
const { typeArgs, args } = watch();

return (
<Flex direction="column" gap={4}>
{Object.keys(typeArgs).length > 0 && (
<TypesForm
genericTypeParams={fn.generic_type_params}
initialData={typeArgs}
propsOnChange={(value) => {
setValue("typeArgs", value);
propsOnChange?.(getValues());
}}
/>
)}
{Object.keys(args).length > 0 && (
<ArgsForm
params={fn.params}
initialData={args}
propsOnChange={(value) => {
setValue("args", value);
propsOnChange?.(getValues());
}}
propsOnErrors={propsOnErrors}
/>
)}
</Flex>
);
};
91 changes: 91 additions & 0 deletions src/lib/components/abi/args-form/field/ArgFieldWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Input, Textarea } from "@chakra-ui/react";
import { Select } from "chakra-react-select";
import type { ControllerRenderProps } from "react-hook-form";

import type { Option } from "lib/types";

import { UintTypes } from "./utils";

const getInputPlaceholder = (type: string, isNull: boolean) => {
if (type === "0x1::string::String" && !isNull)
return "Left blank to send as empty string";
if (type === "&signer") return "Signer is auto-filled when signing a tx";
return " ";
};

const boolOptions = [
{ label: "True", value: "true" },
{ label: "False", value: "false" },
];

interface ArgFieldWidgetProps {
type: string;
value: Option<string>;
onChange: ControllerRenderProps["onChange"];
}

export const ArgFieldWidget = ({
type,
value,
onChange,
}: ArgFieldWidgetProps) => {
if (
UintTypes.includes(type) ||
type === "address" ||
type === "0x1::string::String" ||
type === "&signer"
)
return (
<Input
size="md"
placeholder={getInputPlaceholder(type, value === undefined)}
value={value ?? ""}
onChange={onChange}
/>
);

if (type === "bool")
return (
<Select
classNamePrefix="chakra-react-select"
size="md"
options={boolOptions}
placeholder={" "}
value={boolOptions.find(
({ value: optionValue }) => optionValue === value
)}
onChange={(e) => onChange(e?.value)}
menuPosition="fixed"
chakraStyles={{
control: (provided) => ({
...provided,
_disabled: {
color: "text.main",
},
}),
dropdownIndicator: (provided, state) => ({
...provided,
color: state.isDisabled ? "gray.700" : undefined,
}),
option: (provided, state) => ({
...provided,
bg: state.isSelected ? "gray.800" : undefined,
color: "text.main",
_hover: {
bg: "gray.700",
},
}),
}}
/>
);

return (
<Textarea
minH="112px"
h="fit-content"
placeholder={" "}
value={value ?? ""}
onChange={onChange}
/>
);
};
100 changes: 100 additions & 0 deletions src/lib/components/abi/args-form/field/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/* eslint-disable sonarjs/cognitive-complexity */
import {
Box,
Checkbox,
FormControl,
FormErrorMessage,
FormLabel,
Text,
} from "@chakra-ui/react";
import { useCallback } from "react";
import { type Control, useController } from "react-hook-form";

import { useValidateAddress } from "lib/app-provider";
import type { AbiFormData } from "lib/types";

import { ArgFieldWidget } from "./ArgFieldWidget";
import { getRules } from "./utils";

interface ArgFieldTemplateProps {
index: number;
param: string;
control: Control<AbiFormData["args"]>;
error?: string;
isReadOnly?: boolean;
}

export const ArgFieldTemplate = ({
index,
param,
control,
error,
isReadOnly = false,
}: ArgFieldTemplateProps) => {
const { validateUserAddress, validateContractAddress, validateHexAddress } =
useValidateAddress();

const isValidArgAddress = useCallback(
(input: string) =>
validateUserAddress(input) === null ||
validateContractAddress(input) === null ||
validateHexAddress(input),
[validateContractAddress, validateHexAddress, validateUserAddress]
);

const isOptional = param.startsWith("0x1::option::Option");
const type = isOptional ? param.split(/<(.*)>/)[1] : param;
const rules = getRules(type, isOptional, isReadOnly, isValidArgAddress);

const {
field: { value, onChange, ...fieldProps },
fieldState: { isTouched },
} = useController({
name: `${index}`,
control,
rules,
});
const isError = isTouched && !!error;

const size = "md";
const isSigner = type === "&signer";
const isNull = value === undefined;
return (
<Box>
<FormControl
className={`${size}-form`}
variant={
isSigner || (type === "0x1::string::String" && !isNull)
? "fixed-floating"
: "floating"
}
size="md"
songwongtp marked this conversation as resolved.
Show resolved Hide resolved
isInvalid={isError}
isReadOnly={isReadOnly}
isDisabled={(isSigner && !isReadOnly) || isNull}
{...fieldProps}
>
<ArgFieldWidget type={type} value={value} onChange={onChange} />
<FormLabel className={`${size}-label`} bgColor="background.main">
{param}
</FormLabel>

{isError && (
<FormErrorMessage className="error-text" mt={1}>
{error}
</FormErrorMessage>
)}
</FormControl>
{isOptional && (
<Checkbox
pt="2px"
pl={2}
isChecked={value === undefined}
onChange={(e) => onChange(e.target.checked ? undefined : "")}
>
<Text variant="body3">Send as null</Text>
</Checkbox>
)}
</Box>
);
};
Loading