From 730542f2d42910e7580c94f547301e8824cd5782 Mon Sep 17 00:00:00 2001 From: Hemanth Sai Date: Mon, 6 May 2024 11:09:12 +0530 Subject: [PATCH] feat: allow user to enter query input fields (#1227) * feat: allow user to enter message inputs * chore: ui changes * refactor * chore: review changes --- frontend/public/expand-icon.svg | 5 + .../components/DialogSearchContract.tsx | 21 +- .../components/MessageInputFields.tsx | 68 +++++ .../cosmwasm/components/QueryContract.tsx | 120 ++++----- .../components/QueryContractInputs.tsx | 241 ++++++++++++++++++ .../src/app/(routes)/cosmwasm/cosmwasm.css | 27 +- frontend/src/custom-hooks/useContracts.ts | 76 +++++- frontend/src/types/cosmwasm.d.ts | 25 ++ 8 files changed, 502 insertions(+), 81 deletions(-) create mode 100644 frontend/public/expand-icon.svg create mode 100644 frontend/src/app/(routes)/cosmwasm/components/MessageInputFields.tsx create mode 100644 frontend/src/app/(routes)/cosmwasm/components/QueryContractInputs.tsx diff --git a/frontend/public/expand-icon.svg b/frontend/public/expand-icon.svg new file mode 100644 index 000000000..0627f069d --- /dev/null +++ b/frontend/public/expand-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/app/(routes)/cosmwasm/components/DialogSearchContract.tsx b/frontend/src/app/(routes)/cosmwasm/components/DialogSearchContract.tsx index ba4047fb9..7d3f07676 100644 --- a/frontend/src/app/(routes)/cosmwasm/components/DialogSearchContract.tsx +++ b/frontend/src/app/(routes)/cosmwasm/components/DialogSearchContract.tsx @@ -23,6 +23,8 @@ const DialogSearchContract = (props: DialogSearchContractI) => { const dispatch = useAppDispatch(); const handleClose = () => { onClose(); + setSearchTerm(''); + setSearchResult(null); }; const [isEnterManually, setIsEnterManually] = useState(true); const [searchResult, setSearchResult] = useState( @@ -34,7 +36,8 @@ const DialogSearchContract = (props: DialogSearchContractI) => { const [searchTerm, setSearchTerm] = useState(''); const { getContractInfo, contractLoading, contractError } = useContracts(); - const onSearchContract = async () => { + const onSearchContract = async (e: React.FormEvent) => { + e.preventDefault(); const { data } = await getContractInfo({ address: searchTerm, baseURLs: restURLs, @@ -56,6 +59,8 @@ const DialogSearchContract = (props: DialogSearchContractI) => { searchResult?.contract_info?.label ); onClose(); + setSearchTerm(''); + setSearchResult(null); } }; @@ -102,18 +107,18 @@ const DialogSearchContract = (props: DialogSearchContractI) => { {isEnterManually ? ( <> -
+
onSearchContract(e)} + className="w-full flex justify-between gap-4" + > setSearchTerm(value)} /> - -
+
{contractLoading ? (
@@ -127,7 +132,7 @@ const DialogSearchContract = (props: DialogSearchContractI) => { <> {searchResult ? (
-
Contract found:
+
Search Result:
, index: number) => void; + onQuery: (index: number) => void; + expandField: (index: number) => void; + queryLoading: TxStatus; +}) => { + return ( +
+ {fields.map((field, index) => ( +
+
+
{field.name}
+ expandField(index)} + className="cursor-pointer" + src={'/expand-icon.svg'} + height={24} + width={24} + alt="Expand" + /> +
+ {field?.open ? ( +
+
+ handleChange(e, index)} + autoFocus={true} + /> +
+ +
+ ) : null} +
+ ))} +
+ ); +}; + +export default MessageInputFields; diff --git a/frontend/src/app/(routes)/cosmwasm/components/QueryContract.tsx b/frontend/src/app/(routes)/cosmwasm/components/QueryContract.tsx index dbbbd4412..627db21db 100644 --- a/frontend/src/app/(routes)/cosmwasm/components/QueryContract.tsx +++ b/frontend/src/app/(routes)/cosmwasm/components/QueryContract.tsx @@ -1,11 +1,9 @@ import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; import useContracts from '@/custom-hooks/useContracts'; import { queryContractInfo } from '@/store/features/cosmwasm/cosmwasmSlice'; -import { TxStatus } from '@/types/enums'; -import { CircularProgress, TextField } from '@mui/material'; import React, { useEffect, useState } from 'react'; -import { queryInputStyles } from '../styles'; import { setError } from '@/store/features/common/commonSlice'; +import QueryContractInputs from './QueryContractInputs'; interface QueryContractI { address: string; @@ -20,13 +18,25 @@ const QueryContract = (props: QueryContractI) => { // ---------------DEPENDENCIES---------------// // ------------------------------------------// const dispatch = useAppDispatch(); - const { getContractMessages, getQueryContract } = useContracts(); + const { + getContractMessages, + getQueryContract, + getContractMessageInputs, + messagesLoading, + messageInputsLoading, + messageInputsError, + messagesError, + } = useContracts(); // ------------------------------------------// // ------------------STATES------------------// // ------------------------------------------// const [queryText, setQueryText] = useState(''); const [contractMessages, setContractMessages] = useState([]); + const [contractMessageInputs, setContractMessageInputs] = useState( + [] + ); + const [selectedMessage, setSelectedMessage] = useState(''); const queryOutput = useAppSelector( (state) => state.cosmwasm.chains?.[chainID]?.query.queryOutput @@ -44,8 +54,29 @@ const QueryContract = (props: QueryContractI) => { setQueryText(e.target.value); }; - const handleSelectMessage = (msg: string) => { + const handleSelectMessage = async (msg: string) => { setQueryText(`{\n\t"${msg}": {}\n}`); + setSelectedMessage(msg); + const { messages } = await getContractMessageInputs({ + address, + baseURLs, + queryMsg: { [msg]: {} }, + }); + setContractMessageInputs(messages); + }; + + const handleSelectedMessageInputChange = (value: string) => { + setQueryText( + JSON.stringify( + { + [selectedMessage]: { + [value]: '', + }, + }, + undefined, + 2 + ) + ); }; const formatJSON = () => { @@ -69,8 +100,8 @@ const QueryContract = (props: QueryContractI) => { // --------------------------------------// // -----------------QUERY----------------// // --------------------------------------// - const onQuery = () => { - if (!queryText?.length) { + const onQuery = (queryInput: string) => { + if (!queryInput?.length) { dispatch( setError({ type: 'error', @@ -87,7 +118,7 @@ const QueryContract = (props: QueryContractI) => { queryContractInfo({ address, baseURLs, - queryData: queryText, + queryData: queryInput, chainID, getQueryContract, }) @@ -106,62 +137,23 @@ const QueryContract = (props: QueryContractI) => { }, [address]); return ( -
-
-
-
Suggested Messages:
-
- {contractMessages?.map((msg) => ( -
handleSelectMessage(msg)} - key={msg} - className="query-shortcut-msg" - > - {msg} -
- ))} -
-
-
- - - -
-
+
+
{JSON.stringify(queryOutput, undefined, 2)}
diff --git a/frontend/src/app/(routes)/cosmwasm/components/QueryContractInputs.tsx b/frontend/src/app/(routes)/cosmwasm/components/QueryContractInputs.tsx new file mode 100644 index 000000000..b1414c9c8 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/QueryContractInputs.tsx @@ -0,0 +1,241 @@ +import { CircularProgress, TextField } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { queryInputStyles } from '../styles'; +import { TxStatus } from '@/types/enums'; +import MessageInputFields from './MessageInputFields'; + +const QueryContractInputs = (props: QueryContractInputsI) => { + const { + contractMessageInputs, + contractMessages, + handleQueryChange, + handleSelectMessage, + handleSelectedMessageInputChange, + messagesLoading, + queryText, + selectedMessage, + onQuery, + formatJSON, + queryLoading, + messageInputsLoading, + messageInputsError, + messagesError, + } = props; + + // ------------------------------------------// + // ------------------STATES------------------// + // ------------------------------------------// + const [isJSONInput, setIsJSONInput] = useState(false); + const [messageInputFields, setMessageInputFields] = useState< + MessageInputField[] + >([]); + + // ------------------------------------------------// + // -----------------CHANGE HANDLERS----------------// + // ------------------------------------------------// + const handleInputMessageChange = ( + e: React.ChangeEvent, + index: number + ) => { + const input = e.target.value; + const updatedFields = messageInputFields.map((value, key) => { + if (index === key) { + value.value = input; + } + return value; + }); + setMessageInputFields(updatedFields); + }; + + const expandField = (index: number) => { + const updatedFields = messageInputFields.map((field, i) => { + if (i === index) { + return { ...field, open: !field.open }; + } else { + return { ...field, open: false }; + } + }); + + setMessageInputFields(updatedFields); + }; + + const queryContract = (index: number) => { + const queryInput = JSON.stringify( + { + [selectedMessage]: { + [messageInputFields[index].name]: messageInputFields[index].value, + }, + }, + undefined, + 2 + ); + onQuery(queryInput); + }; + + // ------------------------------------------// + // ---------------SIDE EFFECT----------------// + // ------------------------------------------// + useEffect(() => { + const inputFields: MessageInputField[] = []; + contractMessageInputs.forEach((messageInput) => { + inputFields.push({ name: messageInput, open: false, value: '' }); + }); + setMessageInputFields(inputFields); + }, [contractMessageInputs]); + + return ( +
+
+
+ Suggested Messages: + {messagesLoading ? ( + + Fetching messages{' '} + + ) : contractMessages?.length ? null : ( + {' '}No messages found + )} +
+
+ {contractMessages?.map((msg) => ( +
handleSelectMessage(msg)} + key={msg} + className={`query-shortcut-msg ${!isJSONInput && selectedMessage === msg ? 'primary-gradient' : ''}`} + > + {msg} +
+ ))} +
+
+ {contractMessageInputs?.length ? ( +
+
+ Suggested Inputs for{' '} + {selectedMessage}: +
+
+ {contractMessageInputs?.map((msg) => ( +
handleSelectedMessageInputChange(msg)} + key={msg} + className="query-shortcut-msg" + > + {msg} +
+ ))} +
+
+ ) : null} +
+
+ {isJSONInput + ? 'Enter query in JSON format:' + : messageInputFields.length + ? 'Enter field value to query:' + : 'Query:'} +
+
+ +
+
+ {isJSONInput ? ( +
+ + + +
+ ) : messageInputsLoading ? ( +
+ + Fetching message inputs + +
+ ) : messageInputsError ? ( +
+ Couldn't fetch message inputs, Please switch to JSON format +
+ ) : !messageInputFields.length ? ( +
+ {selectedMessage?.length ? ( +
+
+
{selectedMessage}
+
+ +
+ ) : ( +
+ {messagesError ? ( +
+ Couldn't fetch messages, Please switch to JSON format +
+ ) : ( +
- Select a message to query -
+ )} +
+ )} +
+ ) : ( + + )} +
+ ); +}; + +export default QueryContractInputs; diff --git a/frontend/src/app/(routes)/cosmwasm/cosmwasm.css b/frontend/src/app/(routes)/cosmwasm/cosmwasm.css index c14f0df5d..3a7d752ef 100644 --- a/frontend/src/app/(routes)/cosmwasm/cosmwasm.css +++ b/frontend/src/app/(routes)/cosmwasm/cosmwasm.css @@ -47,7 +47,7 @@ .query-input-wrapper, .query-output-box { - @apply w-[50%] bg-[#FFFFFF0D] rounded-2xl min-h-[380px] p-4 flex flex-col gap-4; + @apply bg-[#FFFFFF0D] rounded-2xl min-h-[380px] p-4 flex flex-col gap-4; } .execute-field-wrapper, @@ -144,3 +144,28 @@ .upload-btn { @apply flex justify-center items-center text-[12px] font-medium px-3 py-[6px] rounded-lg h-10 w-full; } + +.message-input-field { + @apply w-full border-none bg-transparent placeholder:text-[16px] placeholder:text-[#FFFFFFBF] focus:border-[1px] focus:outline-none; +} + +.message-input-wrapper { + @apply h-12 flex bg-transparent items-center px-6 py-2 rounded-lg flex-1 border-[1px] border-[#ffffff1e] hover:border-[#ffffff50]; +} + +.search-contract-wrapper:focus-within { + border: 1px solid #ffffff4a; +} + +.change-input-type-btn-wrapper { + @apply rounded-lg p-[1px]; + background: linear-gradient(to right, #4aa29c, #8b3da7); +} + +.change-input-type-btn-wrapper:hover { + background: linear-gradient(to right, #8b3da7, #4aa29c); +} + +.change-input-type-btn { + @apply bg-[#232034] text-[12px] font-semibold px-3 py-[10px] flex justify-center items-center rounded-lg; +} diff --git a/frontend/src/custom-hooks/useContracts.ts b/frontend/src/custom-hooks/useContracts.ts index 2597b41e1..397c38cda 100644 --- a/frontend/src/custom-hooks/useContracts.ts +++ b/frontend/src/custom-hooks/useContracts.ts @@ -44,13 +44,21 @@ const getCodeId = (txData: any) => { }; const useContracts = () => { + // ------------------------------------------// + // ---------------DEPENDENCIES---------------// + // ------------------------------------------// + const { getDummyWallet } = useDummyWallet(); + const { getChainInfo } = useGetChainInfo(); + + // ------------------------------------------// + // ------------------STATES------------------// + // ------------------------------------------// const [contractLoading, setContractLoading] = useState(false); const [contractError, setContractError] = useState(''); - const [messagesLoading, setMessagesLoading] = useState(false); - - const { getDummyWallet } = useDummyWallet(); - const { getChainInfo } = useGetChainInfo(); + const [messagesError, setMessagesError] = useState(''); + const [messageInputsLoading, setMessageInputsLoading] = useState(false); + const [messageInputsError, setMessageInputsError] = useState(''); const getContractInfo = async ({ address, @@ -81,21 +89,32 @@ const useContracts = () => { const getContractMessages = async ({ address, baseURLs, + queryMsg = dummyQuery, }: { address: string; baseURLs: string[]; + queryMsg?: any; }) => { - let messages = []; + let messages: string[] = []; try { setMessagesLoading(true); - setContractError(''); - await queryContract(baseURLs, address, btoa(JSON.stringify(dummyQuery))); + setMessagesError(''); + await queryContract(baseURLs, address, btoa(JSON.stringify(queryMsg))); return { messages: [], }; /* eslint-disable @typescript-eslint/no-explicit-any */ } catch (error: any) { - messages = extractContractMessages(error.message); + const errMsg = error.message; + if ( + errMsg?.includes('expected one of') || + errMsg?.includes('missing field') + ) { + messages = extractContractMessages(error.message); + } else { + messages = []; + setMessagesError('Failed to fetch messages'); + } } finally { setMessagesLoading(false); } @@ -104,6 +123,43 @@ const useContracts = () => { }; }; + const getContractMessageInputs = async ({ + address, + baseURLs, + queryMsg, + }: { + address: string; + baseURLs: string[]; + queryMsg: any; + }) => { + let messages: string[] = []; + try { + setMessageInputsLoading(true); + setMessageInputsError(''); + await queryContract(baseURLs, address, btoa(JSON.stringify(queryMsg))); + return { + messages: [], + }; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + const errMsg = error.message; + if ( + errMsg?.includes('expected one of') || + errMsg?.includes('missing field') + ) { + messages = extractContractMessages(error.message); + } else { + messages = []; + setMessageInputsError('Failed to fetch message inputs'); + } + } finally { + setMessageInputsLoading(false); + } + return { + messages, + }; + }; + const getQueryContract = async ({ address, baseURLs, @@ -324,6 +380,10 @@ const useContracts = () => { getChainAssets, uploadContract, instantiateContract, + getContractMessageInputs, + messageInputsLoading, + messageInputsError, + messagesError, }; }; diff --git a/frontend/src/types/cosmwasm.d.ts b/frontend/src/types/cosmwasm.d.ts index 68fe3a635..ac54364b7 100644 --- a/frontend/src/types/cosmwasm.d.ts +++ b/frontend/src/types/cosmwasm.d.ts @@ -154,3 +154,28 @@ interface InstantiateContractInputs { txHash: string; }>; } + +interface QueryContractInputsI { + messagesLoading: boolean; + contractMessages: string[]; + handleSelectMessage: (msg: string) => Promise; + contractMessageInputs: string[]; + selectedMessage: string; + handleSelectedMessageInputChange: (value: string) => void; + queryText: string; + handleQueryChange: ( + e: React.ChangeEvent + ) => void; + onQuery: (queryInput: string) => void; + queryLoading: TxStatus; + formatJSON: () => boolean; + messageInputsLoading: boolean; + messageInputsError: string; + messagesError: string; +} + +interface MessageInputField { + name: string; + value: string; + open: boolean; +}