diff --git a/CHANGELOG.md b/CHANGELOG.md index cc915df9d..cb8d425d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +- [#513](https://github.com/alleslabs/celatone-frontend/pull/513) Disable simulating fee when JSON input is invalid - [#510](https://github.com/alleslabs/celatone-frontend/pull/510) Support optional fields for array, boolean, and string - [#505](https://github.com/alleslabs/celatone-frontend/pull/505) Adjust attach funds form label and icon styling for schema section - [#501](https://github.com/alleslabs/celatone-frontend/pull/501) Add more JSON Schema state, e.g. empty object state, boolean field diff --git a/src/lib/components/InputWithIcon.tsx b/src/lib/components/InputWithIcon.tsx index f924571bd..d2d118850 100644 --- a/src/lib/components/InputWithIcon.tsx +++ b/src/lib/components/InputWithIcon.tsx @@ -11,6 +11,7 @@ interface InputWithIconProps { value: string; onChange: (e: ChangeEvent) => void; size?: InputProps["size"]; + autoFocus?: boolean; action?: string; } @@ -19,14 +20,16 @@ const InputWithIcon = ({ value, size, action, + autoFocus = false, onChange, }: InputWithIconProps) => ( AmpTrack(AmpEvent.USE_SEARCH_INPUT) : undefined} /> ( // Design system size: md = 40px, lg = 56px @@ -69,6 +71,7 @@ export const TextInput = ({ pr={status && "36px"} onChange={(e) => setInputState(e.target.value)} maxLength={maxLength} + autoFocus={autoFocus} /> {status && getStatusIcon(status.state, "16px")} diff --git a/src/lib/components/json-schema/JsonSchemaDrawer.tsx b/src/lib/components/json-schema/JsonSchemaDrawer.tsx index 7b32cfb8c..0e42ff07c 100644 --- a/src/lib/components/json-schema/JsonSchemaDrawer.tsx +++ b/src/lib/components/json-schema/JsonSchemaDrawer.tsx @@ -20,6 +20,7 @@ interface JsonSchemaDrawerProps { codeHash: string; isOpen: boolean; onClose: () => void; + onSchemaSave?: () => void; } export const JsonSchemaDrawer = ({ @@ -27,6 +28,7 @@ export const JsonSchemaDrawer = ({ codeHash, isOpen, onClose, + onSchemaSave, }: JsonSchemaDrawerProps) => ( diff --git a/src/lib/components/json-schema/UploadTemplate.tsx b/src/lib/components/json-schema/UploadTemplate.tsx index 0b251190d..5dbb1715b 100644 --- a/src/lib/components/json-schema/UploadTemplate.tsx +++ b/src/lib/components/json-schema/UploadTemplate.tsx @@ -173,12 +173,14 @@ interface UploadTemplateInterface { codeHash: string; codeId: string; closeDrawer: () => void; + onSchemaSave?: () => void; } export const UploadTemplate = ({ codeHash, codeId, closeDrawer, + onSchemaSave, }: UploadTemplateInterface) => { const { saveNewSchema } = useSchemaStore(); const [method, setMethod] = useState(Method.UPLOAD_FILE); @@ -230,9 +232,18 @@ export const UploadTemplate = ({ } saveNewSchema(codeHash, codeId, JSON.parse(schemaString)); setUrlLoading(false); + onSchemaSave?.(); closeDrawer(); return dispatchJsonState({ type: ActionType.RESET, method }); - }, [closeDrawer, codeHash, codeId, jsonState, method, saveNewSchema]); + }, [ + closeDrawer, + codeHash, + codeId, + jsonState, + method, + onSchemaSave, + saveNewSchema, + ]); const disabledState = useMemo(() => { const methodSchemaString = jsonState[method].schemaString; diff --git a/src/lib/components/json-schema/form/index.tsx b/src/lib/components/json-schema/form/index.tsx index 31c918ab7..3b46f5e87 100644 --- a/src/lib/components/json-schema/form/index.tsx +++ b/src/lib/components/json-schema/form/index.tsx @@ -82,7 +82,7 @@ function fixOneOfKeys( // if the entry is supposed to be a oneof, then check that it only has one key if (valueSchema.oneOf) { - console.log("Found oneOf", key, value); + // console.log("Found oneOf", key, value); deleteExtraneousOneOfKeys(value as Record); } diff --git a/src/lib/components/json-schema/section/SchemaInputSection.tsx b/src/lib/components/json-schema/section/SchemaInputSection.tsx index 46ae9780c..04e563ddc 100644 --- a/src/lib/components/json-schema/section/SchemaInputSection.tsx +++ b/src/lib/components/json-schema/section/SchemaInputSection.tsx @@ -1,5 +1,5 @@ import { Button, Flex, Text, useDisclosure } from "@chakra-ui/react"; -import type { RJSFSchema } from "@rjsf/utils"; +import type { RJSFSchema, RJSFValidationError } from "@rjsf/utils"; import { capitalize } from "lodash"; import { observer } from "mobx-react-lite"; @@ -16,7 +16,8 @@ interface SchemaSectionProps { codeId: string; jsonSchema: Option; initialFormData?: Record; - setSchemaInput: (input: string) => void; + handleChange: (data: unknown, errors: RJSFValidationError[]) => void; + onSchemaSave?: () => void; } export const SchemaInputSection = observer( @@ -26,7 +27,8 @@ export const SchemaInputSection = observer( codeId, jsonSchema, initialFormData, - setSchemaInput, + handleChange, + onSchemaSave, }: SchemaSectionProps) => { const { isOpen, onClose, onOpen } = useDisclosure(); const msgSchema = jsonSchema?.[type]; @@ -61,7 +63,7 @@ export const SchemaInputSection = observer( schema={jsonSchema[type] as RJSFSchema} formId={type} initialFormData={initialFormData} - onChange={(data) => setSchemaInput(JSON.stringify(data))} + onChange={handleChange} /> ) : ( @@ -112,6 +114,7 @@ export const SchemaInputSection = observer( onClose={onClose} codeHash={codeHash} codeId={codeId} + onSchemaSave={onSchemaSave} /> ); diff --git a/src/lib/components/json/JsonInput.tsx b/src/lib/components/json/JsonInput.tsx index dfc514e17..81f04ae7d 100644 --- a/src/lib/components/json/JsonInput.tsx +++ b/src/lib/components/json/JsonInput.tsx @@ -81,7 +81,7 @@ const JsonInput = ({ }: JsonInputProps) => { const [jsonState, setJsonState] = useState({ state: "empty" }); - const handleOnChange = (value: string) => { + const handleChange = (value: string) => { setJsonState({ state: "loading" }); setText(value); }; @@ -124,7 +124,7 @@ const JsonInput = ({ > diff --git a/src/lib/components/select-code/CodeSelectDrawerButton.tsx b/src/lib/components/select-code/CodeSelectDrawerButton.tsx index b2ac05d49..29998829f 100644 --- a/src/lib/components/select-code/CodeSelectDrawerButton.tsx +++ b/src/lib/components/select-code/CodeSelectDrawerButton.tsx @@ -100,6 +100,7 @@ export const CodeSelectDrawerButton = ({ ) => setValue("keyword", e.target.value) } diff --git a/src/lib/components/select-contract/ContractListDetail.tsx b/src/lib/components/select-contract/ContractListDetail.tsx index ee47d85ad..ba1c60910 100644 --- a/src/lib/components/select-contract/ContractListDetail.tsx +++ b/src/lib/components/select-contract/ContractListDetail.tsx @@ -123,6 +123,7 @@ export const ContractListDetail = ({ value={searchKeyword} setInputState={setSearchKeyword} placeholder="Search with Contract Address, Name, or Description" + autoFocus size={!isReadOnly ? "lg" : "md"} /> {!isReadOnly && ( diff --git a/src/lib/pages/execute/components/schema-execute/ExecuteBox.tsx b/src/lib/pages/execute/components/schema-execute/ExecuteBox.tsx index 5affac249..0ee0a8b8c 100644 --- a/src/lib/pages/execute/components/schema-execute/ExecuteBox.tsx +++ b/src/lib/pages/execute/components/schema-execute/ExecuteBox.tsx @@ -174,7 +174,7 @@ export const ExecuteBox = ({ // ------------------------------------------// // ------------------CALLBACKS---------------// // ------------------------------------------// - const handleOnChange = useCallback( + const handleChange = useCallback( (data: unknown, errors: RJSFValidationError[]) => { setIsValidForm(errors.length === 0); setMsg(JSON.stringify(data)); @@ -314,7 +314,7 @@ export const ExecuteBox = ({ formId={`execute-${msgSchema.title}`} schema={msgSchema.schema} initialFormData={initialMsg} - onChange={handleOnChange} + onChange={handleChange} /> {simulateFeeError && ( diff --git a/src/lib/pages/instantiate/instantiate.tsx b/src/lib/pages/instantiate/instantiate.tsx index 6da4595b0..e6c3b5fe3 100644 --- a/src/lib/pages/instantiate/instantiate.tsx +++ b/src/lib/pages/instantiate/instantiate.tsx @@ -1,6 +1,7 @@ import { Flex, Heading, Text } from "@chakra-ui/react"; import type { InstantiateResult } from "@cosmjs/cosmwasm-stargate"; import type { StdFee } from "@cosmjs/stargate"; +import type { RJSFValidationError } from "@rjsf/utils"; import Long from "long"; import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -84,24 +85,26 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { const msgQuery = (router.query.msg as string) ?? ""; const codeIdQuery = (router.query["code-id"] as string) ?? ""; const { user: exampleUserAddress } = useExampleAddresses(); - const { address = "" } = useCurrentChain(); - const postInstantiateTx = useInstantiateTx(); const fabricateFee = useFabricateFee(); const { broadcast } = useTxBroadcast(); const { validateUserAddress, validateContractAddress } = useValidateAddress(); const getAttachFunds = useAttachFunds(); const { getSchemaByCodeHash } = useSchemaStore(); + // ------------------------------------------// // ------------------STATES------------------// // ------------------------------------------// + const [tab, setTab] = useState(); const [status, setStatus] = useState({ state: "init" }); const [composedTxMsg, setComposedTxMsg] = useState([]); const [estimatedFee, setEstimatedFee] = useState(); const [simulateError, setSimulateError] = useState(""); const [processing, setProcessing] = useState(false); - const [tab, setTab] = useState(); + const [isValidJsonInput, setIsValidJsonInput] = useState(false); + const [hasInitMsg, setHasInitMsg] = useState(false); + // ------------------------------------------// // ----------------FORM HOOKS----------------// // ------------------------------------------// @@ -140,18 +143,40 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { }); const { assetsSelect, assetsJsonStr, attachFundsOption } = watchAssets(); + // ------------------------------------------// + // ------------------LOGICS------------------// + // ------------------------------------------// const jsonSchema = getSchemaByCodeHash(codeHash); const funds = getAttachFunds(attachFundsOption, assetsJsonStr, assetsSelect); - const enableInstantiate = useMemo( - () => + const enableInstantiate = useMemo(() => { + const generalChecks = Boolean(address) && Boolean(label) && isCodeId(codeId) && - jsonValidate(currentInput) === null && - status.state === "success", - [address, label, codeId, currentInput, status.state] - ); + status.state === "success"; + + switch (tab) { + case MessageTabs.JSON_INPUT: + return generalChecks && jsonValidate(currentInput) === null; + case MessageTabs.YOUR_SCHEMA: + return generalChecks && (hasInitMsg || isValidJsonInput); + default: + return false; + } + }, [ + address, + label, + codeId, + status.state, + tab, + currentInput, + hasInitMsg, + isValidJsonInput, + ]); + // ------------------------------------------// + // ---------------SIMUATE FEE----------------// + // ------------------------------------------// const { isFetching: isSimulating } = useSimulateFeeQuery({ enabled: composedTxMsg.length > 0, messages: composedTxMsg, @@ -191,12 +216,36 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { }, onError() { setStatus({ state: "error", message: "This code ID does not exist" }); + setSimulateError(""); }, }); // ------------------------------------------// - // ----------------FUNCTIONS-----------------// + // ----------------CALLBACKS-----------------// // ------------------------------------------// + const resetMsgInputSchema = useCallback(() => { + setValue(`msgInput.${yourSchemaInputFormKey}`, "{}"); + }, [setValue]); + + const handleChange = useCallback( + (data: unknown, errors: RJSFValidationError[]) => { + setIsValidJsonInput(errors.length === 0); + setValue(`msgInput.${yourSchemaInputFormKey}`, JSON.stringify(data)); + + // whenever user change the input, we set hasInitMsg to false + setHasInitMsg(false); + }, + [setValue] + ); + + const validateAdmin = useCallback( + (input: string) => + input && !!validateContractAddress(input) && !!validateUserAddress(input) + ? "Invalid Address." + : undefined, + [validateContractAddress, validateUserAddress] + ); + const proceed = useCallback(async () => { AmpTrackAction(AmpEvent.ACTION_EXECUTE, funds.length, attachFundsOption); const stream = await postInstantiateTx({ @@ -233,10 +282,44 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { // ------------------------------------------// // --------------SIDE EFFECTS----------------// // ------------------------------------------// + /** Remark: parsing initial msg, initial funds from query params */ + useEffect(() => { + if (codeIdQuery) setValue("codeId", codeIdQuery); + if (msgQuery) { + const decodedMsg = libDecode(msgQuery); + try { + const msgObject = JSON.parse(decodedMsg) as InstantiateRedoMsg; + setValue("codeId", msgObject.code_id.toString()); + setValue("label", msgObject.label); + setValue("adminAddress", msgObject.admin); + setValue( + `msgInput.${jsonInputFormKey}`, + JSON.stringify(msgObject.msg, null, 2) + ); + setValue( + `msgInput.${yourSchemaInputFormKey}`, + JSON.stringify(msgObject.msg) + ); + + if (msgObject.funds.length) { + setAssets("assetsSelect", defaultAsset); + setAssets( + "assetsJsonStr", + jsonPrettify(JSON.stringify(msgObject.funds)) + ); + setAssets("attachFundsOption", AttachFundsType.ATTACH_FUNDS_JSON); + } + + setHasInitMsg(true); + } catch { + // comment just to avoid eslint no-empty + } + } + }, [codeIdQuery, msgQuery, setAssets, setValue]); + useEffect(() => { setValue("codeHash", ""); setTab(MessageTabs.JSON_INPUT); - setValue(`msgInput.${yourSchemaInputFormKey}`, "{}"); if (codeId.length) { setStatus({ state: "loading" }); const timer = setTimeout(() => { @@ -247,7 +330,7 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { setStatus({ state: "init" }); return () => {}; - }, [address, codeId, refetch, setValue, setTab]); + }, [codeId, refetch, setValue, setTab]); useEffect(() => { if (enableInstantiate) { @@ -265,6 +348,11 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { }, 1000); return () => clearTimeout(timeoutId); } + + // reset estimated fee and error when user change the input + setSimulateError(""); + setEstimatedFee(undefined); + return () => {}; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -279,53 +367,15 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { ]); useEffect(() => { - if (codeIdQuery) setValue("codeId", codeIdQuery); - if (msgQuery) { - const decodedMsg = libDecode(msgQuery); - try { - const msgObject = JSON.parse(decodedMsg) as InstantiateRedoMsg; - setValue("codeId", msgObject.code_id.toString()); - setValue("label", msgObject.label); - setValue("adminAddress", msgObject.admin); - setValue( - `msgInput.${jsonInputFormKey}`, - JSON.stringify(msgObject.msg, null, 2) - ); - setValue( - `msgInput.${yourSchemaInputFormKey}`, - JSON.stringify(msgObject.msg) - ); - - if (msgObject.funds.length) { - setAssets("assetsSelect", defaultAsset); - setAssets( - "assetsJsonStr", - jsonPrettify(JSON.stringify(msgObject.funds)) - ); - setAssets("attachFundsOption", AttachFundsType.ATTACH_FUNDS_JSON); - } - } catch { - // comment just to avoid eslint no-empty - } + if (jsonSchema) { + setTab(MessageTabs.YOUR_SCHEMA); } - }, [codeIdQuery, msgQuery, setAssets, setValue]); - - useEffect(() => { - if (jsonSchema) setTab(MessageTabs.YOUR_SCHEMA); - }, [jsonSchema]); + }, [jsonSchema, setValue]); useEffect(() => { if (router.isReady) AmpTrackToInstantiate(!!msgQuery, !!codeIdQuery); }, [router.isReady, msgQuery, codeIdQuery]); - const validateAdmin = useCallback( - (input: string) => - input && !!validateContractAddress(input) && !!validateUserAddress(input) - ? "Invalid Address." - : undefined, - [validateContractAddress, validateUserAddress] - ); - return ( <> @@ -345,7 +395,10 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { control={control} status={status} error={formErrors.codeId?.message} - onCodeSelect={(code: string) => setValue("codeId", code)} + onCodeSelect={(code: string) => { + setValue("codeId", code); + resetMsgInputSchema(); + }} setCodeHash={(data: CodeIdInfoResponse) => { setValue("codeHash", data.code_info.data_hash.toLowerCase()); }} @@ -408,10 +461,9 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => { codeHash={codeHash} codeId={codeId} jsonSchema={jsonSchema} - setSchemaInput={(msg: string) => - setValue(`msgInput.${yourSchemaInputFormKey}`, msg) - } initialFormData={JSON.parse(msgInput[yourSchemaInputFormKey])} + handleChange={handleChange} + onSchemaSave={resetMsgInputSchema} /> } /> @@ -453,7 +505,7 @@ const Instantiate = ({ onComplete }: InstantiatePageProps) => {