From 1ed040e3ef57090a2d848b7c02b3597ac35519c5 Mon Sep 17 00:00:00 2001 From: Fran Domovic Date: Wed, 11 Dec 2024 14:46:45 +0100 Subject: [PATCH 1/8] feat: rewrite form arhitecture --- .../components/proposals/ActionsDropdown.tsx | 62 +++ .../proposals/ChangeApprovalsNeededForm.tsx | 29 ++ .../proposals/CreateProposalPopup.tsx | 272 ++++++++++ .../proposals/CrossContractCallForm.tsx | 133 +++++ .../proposals/MaxActiveProposalsForm.tsx | 29 ++ .../proposals/SetContextVariableForm.tsx | 68 +++ app/src/components/proposals/TransferForm.tsx | 41 ++ app/src/pages/home/index.tsx | 472 +----------------- 8 files changed, 646 insertions(+), 460 deletions(-) create mode 100644 app/src/components/proposals/ActionsDropdown.tsx create mode 100644 app/src/components/proposals/ChangeApprovalsNeededForm.tsx create mode 100644 app/src/components/proposals/CreateProposalPopup.tsx create mode 100644 app/src/components/proposals/CrossContractCallForm.tsx create mode 100644 app/src/components/proposals/MaxActiveProposalsForm.tsx create mode 100644 app/src/components/proposals/SetContextVariableForm.tsx create mode 100644 app/src/components/proposals/TransferForm.tsx diff --git a/app/src/components/proposals/ActionsDropdown.tsx b/app/src/components/proposals/ActionsDropdown.tsx new file mode 100644 index 0000000..a66dd3f --- /dev/null +++ b/app/src/components/proposals/ActionsDropdown.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { FormGroup } from './CreateProposalPopup'; + +interface ActionsDropdownProps { + actionType: string; + handleInputChange: (e: React.ChangeEvent) => void; +} + +export enum ActionTypes { + CROSS_CONTRACT_CALL = 'Cross contract call', + TRANSFER = 'Transfer', + SET_CONTEXT_VARIABLE = 'Set context variable', + CHANGE_APPROVALS_NEEDED = 'Change number of approvals needed', + CHANGE_MAX_ACTIVE_PROPOSALS = 'Change number of maximum active proposals', +} + +export const actionTypes = [ + { + id: 'CROSS_CONTRACT_CALL', + label: 'Cross contract call', + }, + { + id: 'TRANSFER', + label: 'Transfer', + }, + { + id: 'SET_CONTEXT_VARIABLE', + label: 'Set context variable', + }, + { + id: 'CHANGE_APPROVALS_NEEDED', + label: 'Change number of approvals needed', + }, + { + id: 'CHANGE_MAX_ACTIVE_PROPOSALS', + label: 'Change number of maximum active proposals', + }, +]; + +export default function ActionsDropdown({ + actionType, + handleInputChange, +}: ActionsDropdownProps) { + return ( + + + + + ); +} diff --git a/app/src/components/proposals/ChangeApprovalsNeededForm.tsx b/app/src/components/proposals/ChangeApprovalsNeededForm.tsx new file mode 100644 index 0000000..24cbf8f --- /dev/null +++ b/app/src/components/proposals/ChangeApprovalsNeededForm.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { FormGroup, ProposalData } from './CreateProposalPopup'; + +interface ChangeApprovalsNeededFormProps { + proposalForm: ProposalData; + handleInputChange: ( + e: React.ChangeEvent, + ) => void; +} + +export default function ChangeApprovalsNeededForm({ + proposalForm, + handleInputChange, +}: ChangeApprovalsNeededFormProps) { + return ( + + + + + ); +} diff --git a/app/src/components/proposals/CreateProposalPopup.tsx b/app/src/components/proposals/CreateProposalPopup.tsx new file mode 100644 index 0000000..b656987 --- /dev/null +++ b/app/src/components/proposals/CreateProposalPopup.tsx @@ -0,0 +1,272 @@ +import React, { useState } from 'react'; +import { styled } from 'styled-components'; +import ActionsDropdown, { ActionTypes } from './ActionsDropdown'; +import CrossContractCallForm from './CrossContractCallForm'; +import TransferForm from './TransferForm'; +import SetContextVariableForm from './SetContextVariableForm'; +import ChangeApprovalsNeededForm from './ChangeApprovalsNeededForm'; +import MaxActiveProposalsForm from './MaxActiveProposalsForm'; + +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +`; + +const ModalContent = styled.div` + background-color: #1e1e1e; + padding: 2rem; + border-radius: 8px; + width: 90%; + max-width: 500px; + color: white; +`; + +export const FormGroup = styled.div` + margin-bottom: 1rem; + + label { + display: block; + margin-bottom: 0.5rem; + } + + input, + textarea { + width: 100%; + padding: 0.5rem; + border-radius: 4px; + border: 1px solid #444; + background-color: #333; + color: white; + } +`; + +const ButtonGroup = styled.div` + display: flex; + justify-content: flex-end; + gap: 1rem; + margin-top: 1rem; +`; + +export const ButtonSm = styled.button` + color: white; + padding: 0.25em 1em; + margin: 0.25em; + border-radius: 8px; + font-size: 1rem; + background: #5dbb63; + cursor: pointer; + justify-content: center; + display: flex; + border: none; + outline: none; +`; + +export interface ProposalData { + actionType: string; + protocol: string; + contractId: string; + methodName: string; + arguments: { key: string; value: string }[]; + deposit: string; + gas: string; + receiverId: string; + amount: string; + contextVariables: { key: string; value: string }[]; + minApprovals: string; + maxActiveProposals: string; +} + +interface CreateProposalPopupProps { + setIsModalOpen: (isModalOpen: boolean) => void; + createProposal: (proposalForm: ProposalData) => Promise; +} + +export default function CreateProposalPopup({ + setIsModalOpen, + createProposal, +}: CreateProposalPopupProps) { + const [proposalForm, setProposalForm] = useState({ + actionType: 'Cross contract call', + protocol: 'NEAR', + contractId: '', + methodName: '', + arguments: [{ key: '', value: '' }], + deposit: '', + gas: '', + receiverId: '', + amount: '', + contextVariables: [{ key: '', value: '' }], + minApprovals: '', + maxActiveProposals: '', + }); + + const handleInputChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + >, + ) => { + const { name, value } = e.target; + + setProposalForm((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleArgumentChange = ( + index: number, + field: 'key' | 'value', + value: string, + ) => { + setProposalForm((prev) => { + const newArgs = [...prev.arguments]; + newArgs[index] = { + ...newArgs[index], + [field]: value, + }; + return { + ...prev, + arguments: newArgs, + }; + }); + }; + + const addContextVariable = () => { + setProposalForm((prev) => ({ + ...prev, + contextVariables: [...prev.contextVariables, { key: '', value: '' }], + })); + }; + + const removeContextVariable = (index: number) => { + setProposalForm((prev) => ({ + ...prev, + contextVariables: prev.contextVariables.filter((_, i) => i !== index), + })); + }; + + const addArgument = () => { + setProposalForm((prev) => ({ + ...prev, + arguments: [...prev.arguments, { key: '', value: '' }], + })); + }; + + const removeArgument = (index: number) => { + setProposalForm((prev) => ({ + ...prev, + arguments: prev.arguments.filter((_, i) => i !== index), + })); + }; + + const handleContextVariableChange = ( + index: number, + field: 'key' | 'value', + value: string, + ) => { + setProposalForm((prev: any) => { + const newVariables = [...prev.contextVariables]; + newVariables[index] = { + ...newVariables[index], + [field]: value, + }; + return { + ...prev, + contextVariables: newVariables, + }; + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsModalOpen(false); + await createProposal(proposalForm); + + setProposalForm({ + actionType: 'Cross contract call', + protocol: 'NEAR', + contractId: '', + methodName: '', + arguments: [{ key: '', value: '' }], + deposit: '', + gas: '', + receiverId: '', + amount: '', + contextVariables: [{ key: '', value: '' }], + minApprovals: '', + maxActiveProposals: '', + }); + }; + + return ( + setIsModalOpen(false)}> + e.stopPropagation()}> +

Create New Proposal

+
+ + {proposalForm.actionType === ActionTypes.CROSS_CONTRACT_CALL && ( + + )} + {proposalForm.actionType === ActionTypes.TRANSFER && ( + + )} + + {proposalForm.actionType === ActionTypes.SET_CONTEXT_VARIABLE && ( + + )} + + {proposalForm.actionType === ActionTypes.CHANGE_APPROVALS_NEEDED && ( + + )} + + {proposalForm.actionType === + ActionTypes.CHANGE_MAX_ACTIVE_PROPOSALS && ( + + )} + + + setIsModalOpen(false)} + style={{ background: '#666' }} + > + Cancel + + Create Proposal + + +
+
+ ); +} diff --git a/app/src/components/proposals/CrossContractCallForm.tsx b/app/src/components/proposals/CrossContractCallForm.tsx new file mode 100644 index 0000000..5d9a85e --- /dev/null +++ b/app/src/components/proposals/CrossContractCallForm.tsx @@ -0,0 +1,133 @@ +import React from "react"; +import { ButtonSm, FormGroup, ProposalData } from "./CreateProposalPopup"; + +interface CrossContractCallFormProps { + proposalForm: ProposalData; + handleInputChange: (e: React.ChangeEvent) => void; + handleArgumentChange: (index: number, field: "key" | "value", value: string) => void; + removeArgument: (index: number) => void; + addArgument: () => void; +} + +export default function CrossContractCallForm({ + proposalForm, + handleInputChange, + handleArgumentChange, + removeArgument, + addArgument +}: CrossContractCallFormProps) { + return ( + <> + + + + + + + + + + + + + + + + + {proposalForm.protocol === 'NEAR' && ( + + + + + )} + + + {proposalForm.arguments.map((arg: {key: string, value: string}, index: number) => ( +
+ + + + handleArgumentChange(index, 'key', e.target.value) + } + required + /> + + + + + handleArgumentChange(index, 'value', e.target.value) + } + required + /> + + removeArgument(index)} + style={{ background: '#666', marginBottom: '1rem' }} + > + Remove + +
+ ))} + + Add Argument + +
+ + ); +} diff --git a/app/src/components/proposals/MaxActiveProposalsForm.tsx b/app/src/components/proposals/MaxActiveProposalsForm.tsx new file mode 100644 index 0000000..b704329 --- /dev/null +++ b/app/src/components/proposals/MaxActiveProposalsForm.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { FormGroup, ProposalData } from './CreateProposalPopup'; + +interface MaxActiveProposalsFormProps { + proposalForm: ProposalData; + handleInputChange: ( + e: React.ChangeEvent, + ) => void; +} + +export default function MaxActiveProposalsForm({ + proposalForm, + handleInputChange, +}: MaxActiveProposalsFormProps) { + return ( + + + + + ); +} diff --git a/app/src/components/proposals/SetContextVariableForm.tsx b/app/src/components/proposals/SetContextVariableForm.tsx new file mode 100644 index 0000000..f7fd876 --- /dev/null +++ b/app/src/components/proposals/SetContextVariableForm.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { ButtonSm, FormGroup, ProposalData } from './CreateProposalPopup'; + +interface SetContextVariableFormProps { + proposalForm: ProposalData; + handleContextVariableChange: ( + index: number, + field: 'key' | 'value', + value: string, + ) => void, + removeContextVariable: (index: number) => void, + addContextVariable: () => void, +} + +export default function SetContextVariableForm({ + proposalForm, + handleContextVariableChange, + removeContextVariable, + addContextVariable, +}: SetContextVariableFormProps) { + return ( + <> + {proposalForm.contextVariables.map((variable: { key: string; value: string }, index: number) => ( +
+ + + + handleContextVariableChange(index, 'key', e.target.value) + } + required + /> + + + + + handleContextVariableChange(index, 'value', e.target.value) + } + required + /> + + removeContextVariable(index)} + style={{ background: '#666', marginBottom: '1rem' }} + > + Remove + +
+ ))} + + Add Variable + + + ); +} diff --git a/app/src/components/proposals/TransferForm.tsx b/app/src/components/proposals/TransferForm.tsx new file mode 100644 index 0000000..9a953c9 --- /dev/null +++ b/app/src/components/proposals/TransferForm.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { FormGroup, ProposalData } from './CreateProposalPopup'; + +interface TransferFormProps { + proposalForm: ProposalData; + handleInputChange: ( + e: React.ChangeEvent, + ) => void; +} + +export default function TransferForm({ + proposalForm, + handleInputChange, +}: TransferFormProps) { + return ( + <> + + + + + + + + + + ); +} diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index b57663c..9641ee5 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -38,6 +38,9 @@ import { ContextApiDataSource } from '../../api/dataSource/ContractApiDataSource import { ApprovalsCount, ContractProposal } from '../../api/contractApi'; import { Buffer } from 'buffer'; import bs58 from 'bs58'; +import CreateProposalPopup, { + ProposalData, +} from '../../components/proposals/CreateProposalPopup'; const FullPageCenter = styled.div` display: flex; @@ -83,18 +86,6 @@ const ButtonSm = styled.button` outline: none; `; -const ButtonReset = styled.div` - color: white; - padding: 0.25em 1em; - border-radius: 8px; - font-size: 1em; - background: #ffa500; - cursor: pointer; - justify-content: center; - display: flex; - margin-top: 1rem; -`; - const LogoutButton = styled.div` color: black; margin-top: 2rem; @@ -161,54 +152,6 @@ const ProposalsWrapper = styled.div` } `; -const ModalOverlay = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.7); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -`; - -const ModalContent = styled.div` - background-color: #1e1e1e; - padding: 2rem; - border-radius: 8px; - width: 90%; - max-width: 500px; - color: white; -`; - -const FormGroup = styled.div` - margin-bottom: 1rem; - - label { - display: block; - margin-bottom: 0.5rem; - } - - input, - textarea { - width: 100%; - padding: 0.5rem; - border-radius: 4px; - border: 1px solid #444; - background-color: #333; - color: white; - } -`; - -const ButtonGroup = styled.div` - display: flex; - justify-content: flex-end; - gap: 1rem; - margin-top: 1rem; -`; - export default function HomePage() { const navigate = useNavigate(); const url = getAppEndpointKey(); @@ -226,20 +169,6 @@ export default function HomePage() { const [hasAlerted, setHasAlerted] = useState(false); const lastExecutedProposalRef = useRef(null); const [isModalOpen, setIsModalOpen] = useState(false); - const [proposalForm, setProposalForm] = useState({ - actionType: 'Cross contract call', - protocol: 'NEAR', - contractId: '', - methodName: '', - arguments: [{ key: '', value: '' }], - deposit: '', - gas: '', - receiverId: '', - amount: '', - contextVariables: [{ key: '', value: '' }], - minApprovals: '', - maxActiveProposals: '', - }); useEffect(() => { if (!url || !applicationId || !accessToken || !refreshToken) { @@ -280,7 +209,7 @@ export default function HomePage() { } } - async function createProposal(formData: typeof proposalForm) { + async function createProposal(formData: ProposalData) { setCreateProposalLoading(true); let request: CreateProposalRequest; @@ -387,7 +316,7 @@ export default function HomePage() { } else { throw new Error('Invalid response from server'); } - } catch (error) { + } catch (error: any) { console.error('Error creating proposal:', error); window.alert(`Error creating proposal: ${error.message}`); } finally { @@ -410,7 +339,7 @@ export default function HomePage() { if (selectedProposal && proposals.length > 0) { const stillExists = proposalsData.some( - (proposal) => proposal.id === selectedProposal.id, + (proposal: any) => proposal.id === selectedProposal.id, ); if ( @@ -560,7 +489,7 @@ export default function HomePage() { } else { throw new Error('Invalid response from server'); } - } catch (error) { + } catch (error: any) { console.error('Error deleting proposal:', error); window.alert(`Error deleting proposal: ${error.message}`); } @@ -573,104 +502,6 @@ export default function HomePage() { navigate('/auth'); }; - const handleInputChange = ( - e: React.ChangeEvent< - HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement - >, - ) => { - const { name, value } = e.target; - setProposalForm((prev) => ({ - ...prev, - [name]: value, - })); - }; - - const handleContextVariableChange = ( - index: number, - field: 'key' | 'value', - value: string, - ) => { - setProposalForm((prev) => { - const newVariables = [...prev.contextVariables]; - newVariables[index] = { - ...newVariables[index], - [field]: value, - }; - return { - ...prev, - contextVariables: newVariables, - }; - }); - }; - - const addContextVariable = () => { - setProposalForm((prev) => ({ - ...prev, - contextVariables: [...prev.contextVariables, { key: '', value: '' }], - })); - }; - - const removeContextVariable = (index: number) => { - setProposalForm((prev) => ({ - ...prev, - contextVariables: prev.contextVariables.filter((_, i) => i !== index), - })); - }; - - const handleArgumentChange = ( - index: number, - field: 'key' | 'value', - value: string, - ) => { - setProposalForm((prev) => { - const newArgs = [...prev.arguments]; - newArgs[index] = { - ...newArgs[index], - [field]: value, - }; - return { - ...prev, - arguments: newArgs, - }; - }); - }; - - const addArgument = () => { - setProposalForm((prev) => ({ - ...prev, - arguments: [...prev.arguments, { key: '', value: '' }], - })); - }; - - const removeArgument = (index: number) => { - setProposalForm((prev) => ({ - ...prev, - arguments: prev.arguments.filter((_, i) => i !== index), - })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsModalOpen(false); - await createProposal(proposalForm); - - // Reset form after successful submission - setProposalForm({ - actionType: 'Cross contract call', - protocol: 'NEAR', - contractId: '', - methodName: '', - arguments: [{ key: '', value: '' }], - deposit: '', - gas: '', - receiverId: '', - amount: '', - contextVariables: [{ key: '', value: '' }], - minApprovals: '', - maxActiveProposals: '', - }); - }; - // Add this helper function to check if current user is the author const isCurrentUserAuthor = (proposal: ContractProposal): boolean => { const currentUserKey = getJWTObject()?.executor_public_key; @@ -680,7 +511,7 @@ export default function HomePage() { return ( - Welcome to home page! + Blockchain proposals demo application
Proposals
@@ -693,290 +524,11 @@ export default function HomePage() { {isModalOpen && ( - setIsModalOpen(false)}> - e.stopPropagation()}> -

Create New Proposal

-
- - - - - - {proposalForm.actionType === 'Cross contract call' && ( - <> - - - - - - - - - - - - - - - - - {proposalForm.protocol === 'NEAR' && ( - - - - - )} - - - {proposalForm.arguments.map((arg, index) => ( -
- - - - handleArgumentChange(index, 'key', e.target.value) - } - required - /> - - - - - handleArgumentChange( - index, - 'value', - e.target.value, - ) - } - required - /> - - removeArgument(index)} - style={{ background: '#666', marginBottom: '1rem' }} - > - Remove - -
- ))} - - Add Argument - -
- - )} - - {proposalForm.actionType === 'Transfer' && ( - <> - - - - - - - - - - )} - - {proposalForm.actionType === 'Set context variable' && ( - <> - {proposalForm.contextVariables.map((variable, index) => ( -
- - - - handleContextVariableChange( - index, - 'key', - e.target.value, - ) - } - required - /> - - - - - handleContextVariableChange( - index, - 'value', - e.target.value, - ) - } - required - /> - - removeContextVariable(index)} - style={{ background: '#666', marginBottom: '1rem' }} - > - Remove - -
- ))} - - Add Variable - - - )} - - {proposalForm.actionType === - 'Change number of approvals needed' && ( - - - - - )} - - {proposalForm.actionType === - 'Change number of maximum active proposals' && ( - - - - - )} - - - setIsModalOpen(false)} - style={{ background: '#666' }} - > - Cancel - - Create Proposal - -
-
-
+ )} -

Number of proposals:

From 253d19cb37d80f9c34aac99e9ab8c7df27d18ffd Mon Sep 17 00:00:00 2001 From: Fran Domovic Date: Wed, 11 Dec 2024 20:17:00 +0100 Subject: [PATCH 2/8] fix: placeholders, gas, scroll container --- .../proposals/ChangeApprovalsNeededForm.tsx | 1 + .../proposals/CreateProposalPopup.tsx | 10 +- .../proposals/CrossContractCallForm.tsx | 136 +++++++++--------- .../proposals/MaxActiveProposalsForm.tsx | 1 + .../proposals/SetContextVariableForm.tsx | 12 +- app/src/components/proposals/TransferForm.tsx | 2 + app/src/pages/home/index.tsx | 2 +- 7 files changed, 91 insertions(+), 73 deletions(-) diff --git a/app/src/components/proposals/ChangeApprovalsNeededForm.tsx b/app/src/components/proposals/ChangeApprovalsNeededForm.tsx index 24cbf8f..74a495d 100644 --- a/app/src/components/proposals/ChangeApprovalsNeededForm.tsx +++ b/app/src/components/proposals/ChangeApprovalsNeededForm.tsx @@ -19,6 +19,7 @@ export default function ChangeApprovalsNeededForm({ type="number" id="minApprovals" name="minApprovals" + placeholder='2' value={proposalForm.minApprovals} onChange={handleInputChange} min="1" diff --git a/app/src/components/proposals/CreateProposalPopup.tsx b/app/src/components/proposals/CreateProposalPopup.tsx index b656987..c6a76ef 100644 --- a/app/src/components/proposals/CreateProposalPopup.tsx +++ b/app/src/components/proposals/CreateProposalPopup.tsx @@ -22,11 +22,16 @@ const ModalOverlay = styled.div` const ModalContent = styled.div` background-color: #1e1e1e; - padding: 2rem; + padding: 1rem; border-radius: 8px; width: 90%; max-width: 500px; color: white; + + h2 { + padding-bottom: 0.5rem; + margin: 0; + } `; export const FormGroup = styled.div` @@ -76,7 +81,6 @@ export interface ProposalData { methodName: string; arguments: { key: string; value: string }[]; deposit: string; - gas: string; receiverId: string; amount: string; contextVariables: { key: string; value: string }[]; @@ -100,7 +104,6 @@ export default function CreateProposalPopup({ methodName: '', arguments: [{ key: '', value: '' }], deposit: '', - gas: '', receiverId: '', amount: '', contextVariables: [{ key: '', value: '' }], @@ -197,7 +200,6 @@ export default function CreateProposalPopup({ methodName: '', arguments: [{ key: '', value: '' }], deposit: '', - gas: '', receiverId: '', amount: '', contextVariables: [{ key: '', value: '' }], diff --git a/app/src/components/proposals/CrossContractCallForm.tsx b/app/src/components/proposals/CrossContractCallForm.tsx index 5d9a85e..0145cc0 100644 --- a/app/src/components/proposals/CrossContractCallForm.tsx +++ b/app/src/components/proposals/CrossContractCallForm.tsx @@ -1,20 +1,32 @@ -import React from "react"; -import { ButtonSm, FormGroup, ProposalData } from "./CreateProposalPopup"; +import React from 'react'; +import { ButtonSm, FormGroup, ProposalData } from './CreateProposalPopup'; +import { styled } from 'styled-components'; + +const ScrollWrapper = styled.div` + max-height: 150px; + overflow-y: auto; +`; interface CrossContractCallFormProps { - proposalForm: ProposalData; - handleInputChange: (e: React.ChangeEvent) => void; - handleArgumentChange: (index: number, field: "key" | "value", value: string) => void; - removeArgument: (index: number) => void; - addArgument: () => void; + proposalForm: ProposalData; + handleInputChange: ( + e: React.ChangeEvent, + ) => void; + handleArgumentChange: ( + index: number, + field: 'key' | 'value', + value: string, + ) => void; + removeArgument: (index: number) => void; + addArgument: () => void; } export default function CrossContractCallForm({ - proposalForm, - handleInputChange, - handleArgumentChange, - removeArgument, - addArgument + proposalForm, + handleInputChange, + handleArgumentChange, + removeArgument, + addArgument, }: CrossContractCallFormProps) { return ( <> @@ -37,6 +49,7 @@ export default function CrossContractCallForm({ type="text" id="contractId" name="contractId" + placeholder="contract address" value={proposalForm.contractId} onChange={handleInputChange} required @@ -48,6 +61,7 @@ export default function CrossContractCallForm({ type="text" id="methodName" name="methodName" + placeholder="create_post" value={proposalForm.methodName} onChange={handleInputChange} required @@ -68,62 +82,52 @@ export default function CrossContractCallForm({ required /> - {proposalForm.protocol === 'NEAR' && ( - - - - - )} - {proposalForm.arguments.map((arg: {key: string, value: string}, index: number) => ( -
- - - - handleArgumentChange(index, 'key', e.target.value) - } - required - /> - - - - - handleArgumentChange(index, 'value', e.target.value) - } - required - /> - - removeArgument(index)} - style={{ background: '#666', marginBottom: '1rem' }} - > - Remove - -
- ))} + + {proposalForm.arguments.map( + (arg: { key: string; value: string }, index: number) => ( +
+ + + handleArgumentChange(index, 'key', e.target.value) + } + required + /> + + + + handleArgumentChange(index, 'value', e.target.value) + } + required + /> + + removeArgument(index)} + style={{ background: '#666', marginBottom: '1rem' }} + > + Remove + +
+ ), + )} +
Add Argument diff --git a/app/src/components/proposals/MaxActiveProposalsForm.tsx b/app/src/components/proposals/MaxActiveProposalsForm.tsx index b704329..8a8d235 100644 --- a/app/src/components/proposals/MaxActiveProposalsForm.tsx +++ b/app/src/components/proposals/MaxActiveProposalsForm.tsx @@ -19,6 +19,7 @@ export default function MaxActiveProposalsForm({ type="number" id="maxActiveProposals" name="maxActiveProposals" + placeholder='10' value={proposalForm.maxActiveProposals} onChange={handleInputChange} min="1" diff --git a/app/src/components/proposals/SetContextVariableForm.tsx b/app/src/components/proposals/SetContextVariableForm.tsx index f7fd876..2e445df 100644 --- a/app/src/components/proposals/SetContextVariableForm.tsx +++ b/app/src/components/proposals/SetContextVariableForm.tsx @@ -1,5 +1,11 @@ import React from 'react'; import { ButtonSm, FormGroup, ProposalData } from './CreateProposalPopup'; +import { styled } from 'styled-components'; + +const ScrollWrapper = styled.div` + max-height: 150px; + overflow-y: auto; +` interface SetContextVariableFormProps { proposalForm: ProposalData; @@ -20,6 +26,7 @@ export default function SetContextVariableForm({ }: SetContextVariableFormProps) { return ( <> + {proposalForm.contextVariables.map((variable: { key: string; value: string }, index: number) => (
- handleContextVariableChange(index, 'key', e.target.value) @@ -41,9 +48,9 @@ export default function SetContextVariableForm({ /> - handleContextVariableChange(index, 'value', e.target.value) @@ -60,6 +67,7 @@ export default function SetContextVariableForm({
))} +
Add Variable diff --git a/app/src/components/proposals/TransferForm.tsx b/app/src/components/proposals/TransferForm.tsx index 9a953c9..74a2533 100644 --- a/app/src/components/proposals/TransferForm.tsx +++ b/app/src/components/proposals/TransferForm.tsx @@ -20,6 +20,7 @@ export default function TransferForm({ type="text" id="receiverId" name="receiverId" + placeholder='account address' value={proposalForm.receiverId} onChange={handleInputChange} required @@ -31,6 +32,7 @@ export default function TransferForm({ type="text" id="amount" name="amount" + placeholder='10' value={proposalForm.amount} onChange={handleInputChange} required diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 9641ee5..1c4d14b 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -242,7 +242,7 @@ export default function HomePage() { deposit: formData.deposit || '0', gas: formData.protocol === 'NEAR' - ? formData.gas || '30000000000000' + ? '30000000000000' : '0', }, }; From 09dd86062ccb8e7420ead1081acf90810e41418b Mon Sep 17 00:00:00 2001 From: Fran Domovic Date: Mon, 16 Dec 2024 17:03:17 +0100 Subject: [PATCH 3/8] feat: add parsing for each proposal type --- app/src/components/proposal/Actions.tsx | 185 ++++++++++++++++++++++++ app/src/pages/home/index.tsx | 16 +- 2 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 app/src/components/proposal/Actions.tsx diff --git a/app/src/components/proposal/Actions.tsx b/app/src/components/proposal/Actions.tsx new file mode 100644 index 0000000..f2723bc --- /dev/null +++ b/app/src/components/proposal/Actions.tsx @@ -0,0 +1,185 @@ +import React from 'react'; +import { styled } from 'styled-components'; + +interface FunctionCallProps { + receiver_id: string; + method_name: string; + args: string; + deposit: number; + gas: number; +} + +interface ContextValueProps { + key: number[]; + value: number[]; +} + +interface ApprovalsParams { + num_approvals: number; +} + +interface LimitParams { + active_proposals_limit: number; +} + +interface TransferParams { + receiver_id: string; + amount: number; +} + +interface Action { + scope: string; + params: + | LimitParams + | TransferParams + | ApprovalsParams + | ContextValueProps + | FunctionCallProps; +} + +interface ActionsProps { + actions: Action[]; +} + +interface TableHeaderProps { + columns: number; + bgColor?: boolean; +} + +const GridList = styled.div` + display: grid; + grid-template-columns: repeat(${(props) => props.columns}, 1fr); + gap: 0.5rem; + background: ${(props) => (props.bgColor ? '#ffa500' : 'transparent')}; +`; + +export default function Actions({ actions }: ActionsProps) { + const getColumnCount = (scope: string) => { + switch (scope) { + case 'Transfer': + case 'SetContextValue': + return 3; + case 'SetActiveProposalsLimit': + case 'SetNumApprovals': + return 2; + case 'ExternalFunctionCall': + return 5; + default: + return 1; + } + }; + + const renderActionContent = (action: Action) => { + switch (action.scope) { + case 'Transfer': + return ( + <> +
Scope
+
Amount
+
Receiver ID
+ + ); + case 'SetContextValue': + return ( + <> +
Scope
+
Key
+
Vaue ID
+ + ); + case 'SetActiveProposalsLimit': + return ( + <> +
Scope
+
Active Proposals Limit
+ + ); + case 'SetNumApprovals': + return ( + <> +
Scope
+
Number of Approvals
+ + ); + case 'ExternalFunctionCall': + return ( + <> +
Scope
+
Receiver ID
+
Method
+
Deposit
+
Gas
+ + ); + default: + return
Scope
; + } + }; + + const renderActionValues = (action: Action) => { + switch (action.scope) { + case 'Transfer': + const transferParams = action.params as TransferParams; + return ( + <> +
{action.scope}
+
{transferParams.amount}
+
{transferParams.receiver_id}
+ + ); + case 'SetContextValue': + const contextValueParams = action.params as ContextValueProps; + return ( + <> +
{action.scope}
+
{String.fromCharCode(...contextValueParams.key)}
+
{String.fromCharCode(...contextValueParams.value)}
+ + ); + case 'SetActiveProposalsLimit': + const limitParams = action.params as LimitParams; + return ( + <> +
{action.scope}
+
{limitParams.active_proposals_limit}
+ + ); + case 'SetNumApprovals': + const approvalParams = action.params as ApprovalsParams; + return ( + <> +
{action.scope}
+
{approvalParams.num_approvals}
+ + ); + case 'ExternalFunctionCall': + const functionParams = action.params as FunctionCallProps; + return ( + <> +
{action.scope}
+
{functionParams.receiver_id}
+
{functionParams.method_name}
+
{functionParams.deposit}
+
{functionParams.gas}
+ + ); + default: + return
{action.scope}
; + } + }; + + return ( + <> + + {renderActionContent(actions[0])} + +
+ {actions.map((action, index) => ( + + {renderActionValues(action)} + + ))} +
+ + ); +} diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 1c4d14b..aecc645 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -41,6 +41,7 @@ import bs58 from 'bs58'; import CreateProposalPopup, { ProposalData, } from '../../components/proposals/CreateProposalPopup'; +import Actions from '../../components/proposal/Actions'; const FullPageCenter = styled.div` display: flex; @@ -579,20 +580,7 @@ export default function HomePage() { )}

Actions

-
-
Scope
-
Amount
-
Receiver ID
-
-
- {selectedProposal.actions.map((action, index) => ( -
-
{action.scope}
-
{action.params.amount}
-
{action.params.receiver_id}
-
- ))} -
+
approveProposal(selectedProposal.id)}> {approveProposalLoading ? 'Loading...' : 'Approve proposal'} From 384e992a08fc0f39dbaed2ac325e17eefb003ab8 Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Wed, 18 Dec 2024 20:35:16 +0100 Subject: [PATCH 4/8] feat: removed protocol select field and argument, removed multiple variable adding in context variable selection --- .../proposals/CreateProposalPopup.tsx | 3 --- .../proposals/CrossContractCallForm.tsx | 18 +----------------- .../proposals/SetContextVariableForm.tsx | 10 ---------- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/app/src/components/proposals/CreateProposalPopup.tsx b/app/src/components/proposals/CreateProposalPopup.tsx index c6a76ef..a638b5f 100644 --- a/app/src/components/proposals/CreateProposalPopup.tsx +++ b/app/src/components/proposals/CreateProposalPopup.tsx @@ -76,7 +76,6 @@ export const ButtonSm = styled.button` export interface ProposalData { actionType: string; - protocol: string; contractId: string; methodName: string; arguments: { key: string; value: string }[]; @@ -99,7 +98,6 @@ export default function CreateProposalPopup({ }: CreateProposalPopupProps) { const [proposalForm, setProposalForm] = useState({ actionType: 'Cross contract call', - protocol: 'NEAR', contractId: '', methodName: '', arguments: [{ key: '', value: '' }], @@ -195,7 +193,6 @@ export default function CreateProposalPopup({ setProposalForm({ actionType: 'Cross contract call', - protocol: 'NEAR', contractId: '', methodName: '', arguments: [{ key: '', value: '' }], diff --git a/app/src/components/proposals/CrossContractCallForm.tsx b/app/src/components/proposals/CrossContractCallForm.tsx index 0145cc0..956e723 100644 --- a/app/src/components/proposals/CrossContractCallForm.tsx +++ b/app/src/components/proposals/CrossContractCallForm.tsx @@ -30,19 +30,6 @@ export default function CrossContractCallForm({ }: CrossContractCallFormProps) { return ( <> - - - - - + - removeContextVariable(index)} - style={{ background: '#666', marginBottom: '1rem' }} - > - Remove -
))} - - Add Variable - ); } From d03aff7b5a21f4df60025b2f7249ea9e707ad0ee Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Wed, 18 Dec 2024 20:35:40 +0100 Subject: [PATCH 5/8] fix: lint --- .../proposals/ChangeApprovalsNeededForm.tsx | 2 +- .../proposals/CreateProposalPopup.tsx | 4 +- .../proposals/MaxActiveProposalsForm.tsx | 2 +- .../proposals/SetContextVariableForm.tsx | 96 ++++++++++--------- app/src/components/proposals/TransferForm.tsx | 4 +- app/src/pages/home/index.tsx | 5 +- 6 files changed, 56 insertions(+), 57 deletions(-) diff --git a/app/src/components/proposals/ChangeApprovalsNeededForm.tsx b/app/src/components/proposals/ChangeApprovalsNeededForm.tsx index 74a495d..4d2c71b 100644 --- a/app/src/components/proposals/ChangeApprovalsNeededForm.tsx +++ b/app/src/components/proposals/ChangeApprovalsNeededForm.tsx @@ -19,7 +19,7 @@ export default function ChangeApprovalsNeededForm({ type="number" id="minApprovals" name="minApprovals" - placeholder='2' + placeholder="2" value={proposalForm.minApprovals} onChange={handleInputChange} min="1" diff --git a/app/src/components/proposals/CreateProposalPopup.tsx b/app/src/components/proposals/CreateProposalPopup.tsx index a638b5f..6d96125 100644 --- a/app/src/components/proposals/CreateProposalPopup.tsx +++ b/app/src/components/proposals/CreateProposalPopup.tsx @@ -29,8 +29,8 @@ const ModalContent = styled.div` color: white; h2 { - padding-bottom: 0.5rem; - margin: 0; + padding-bottom: 0.5rem; + margin: 0; } `; diff --git a/app/src/components/proposals/MaxActiveProposalsForm.tsx b/app/src/components/proposals/MaxActiveProposalsForm.tsx index 8a8d235..371cb18 100644 --- a/app/src/components/proposals/MaxActiveProposalsForm.tsx +++ b/app/src/components/proposals/MaxActiveProposalsForm.tsx @@ -19,7 +19,7 @@ export default function MaxActiveProposalsForm({ type="number" id="maxActiveProposals" name="maxActiveProposals" - placeholder='10' + placeholder="10" value={proposalForm.maxActiveProposals} onChange={handleInputChange} min="1" diff --git a/app/src/components/proposals/SetContextVariableForm.tsx b/app/src/components/proposals/SetContextVariableForm.tsx index 1ab7f85..80c275d 100644 --- a/app/src/components/proposals/SetContextVariableForm.tsx +++ b/app/src/components/proposals/SetContextVariableForm.tsx @@ -5,61 +5,63 @@ import { styled } from 'styled-components'; const ScrollWrapper = styled.div` max-height: 150px; overflow-y: auto; -` +`; interface SetContextVariableFormProps { - proposalForm: ProposalData; - handleContextVariableChange: ( - index: number, - field: 'key' | 'value', - value: string, - ) => void, - removeContextVariable: (index: number) => void, - addContextVariable: () => void, + proposalForm: ProposalData; + handleContextVariableChange: ( + index: number, + field: 'key' | 'value', + value: string, + ) => void; + removeContextVariable: (index: number) => void; + addContextVariable: () => void; } export default function SetContextVariableForm({ - proposalForm, - handleContextVariableChange, - removeContextVariable, - addContextVariable, + proposalForm, + handleContextVariableChange, + removeContextVariable, + addContextVariable, }: SetContextVariableFormProps) { return ( <> - - {proposalForm.contextVariables.map((variable: { key: string; value: string }, index: number) => ( -
- - - handleContextVariableChange(index, 'key', e.target.value) - } - required - /> - - - - handleContextVariableChange(index, 'value', e.target.value) - } - required - /> - -
- ))} + + {proposalForm.contextVariables.map( + (variable: { key: string; value: string }, index: number) => ( +
+ + + handleContextVariableChange(index, 'key', e.target.value) + } + required + /> + + + + handleContextVariableChange(index, 'value', e.target.value) + } + required + /> + +
+ ), + )}
); diff --git a/app/src/components/proposals/TransferForm.tsx b/app/src/components/proposals/TransferForm.tsx index 74a2533..9c937cb 100644 --- a/app/src/components/proposals/TransferForm.tsx +++ b/app/src/components/proposals/TransferForm.tsx @@ -20,7 +20,7 @@ export default function TransferForm({ type="text" id="receiverId" name="receiverId" - placeholder='account address' + placeholder="account address" value={proposalForm.receiverId} onChange={handleInputChange} required @@ -32,7 +32,7 @@ export default function TransferForm({ type="text" id="amount" name="amount" - placeholder='10' + placeholder="10" value={proposalForm.amount} onChange={handleInputChange} required diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index aecc645..693ed57 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -241,10 +241,7 @@ export default function HomePage() { method_name: formData.methodName, args: JSON.stringify(argsObject), deposit: formData.deposit || '0', - gas: - formData.protocol === 'NEAR' - ? '30000000000000' - : '0', + gas: formData.protocol === 'NEAR' ? '30000000000000' : '0', }, }; From 7a07a4110f0b3cb9b0e51d209c8acb1587de7576 Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Wed, 18 Dec 2024 20:51:27 +0100 Subject: [PATCH 6/8] feat: added fetching of context variables --- app/src/api/contractApi.ts | 5 ++ .../api/dataSource/ContractApiDataSource.ts | 60 ++++++++++++----- app/src/pages/home/index.tsx | 67 ++++++++++++++++++- 3 files changed, 113 insertions(+), 19 deletions(-) diff --git a/app/src/api/contractApi.ts b/app/src/api/contractApi.ts index 8b3b4b5..ea2d2e5 100644 --- a/app/src/api/contractApi.ts +++ b/app/src/api/contractApi.ts @@ -39,6 +39,11 @@ export interface ApprovalsCount { num_approvals: number; } +export interface ContextVariables { + key: string; + value: string; +} + export interface ContractApi { //Contract getContractProposals( diff --git a/app/src/api/dataSource/ContractApiDataSource.ts b/app/src/api/dataSource/ContractApiDataSource.ts index e1df33c..c15a131 100644 --- a/app/src/api/dataSource/ContractApiDataSource.ts +++ b/app/src/api/dataSource/ContractApiDataSource.ts @@ -3,6 +3,7 @@ import { ApiResponse } from '@calimero-is-near/calimero-p2p-sdk'; import { ApprovalsCount, ContextDetails, + ContextVariables, ContractApi, ContractProposal, Members, @@ -103,23 +104,50 @@ export class ContextApiDataSource implements ContractApi { } } - getContextDetails(): ApiResponse { - // try { - // const headers: Header | null = await createAuthHeader( - // contextId, - // getNearEnvironment(), - // ); - // const response = await this.client.get( - // `${getAppEndpointKey()}/admin-api/contexts/${contextId}`, - // headers ?? {}, - // ); - // return response; - // } catch (error) { - // console.error('Error fetching context:', error); - // return { error: { code: 500, message: 'Failed to fetch context data.' } }; - // } - throw new Error('Method not implemented.'); + async getContextVariables(): ApiResponse { + try { + const { jwtObject, error } = getConfigAndJwt(); + if (error) { + return { error }; + } + + const apiEndpoint = `${getStorageAppEndpointKey()}/admin-api/contexts/${jwtObject.context_id}/proposals/context-storage-entries`; + const body = { + offset: 0, + limit: 10, + }; + + const response = await axios.post(apiEndpoint, body, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.data.data) { + return { + data: [], + error: null, + }; + } + + // Convert both key and value from Vec to string + const parsedData = response.data.data.map((item: any) => ({ + key: new TextDecoder().decode(new Uint8Array(item.key)), + value: new TextDecoder().decode(new Uint8Array(item.value)) + })); + + return { + data: parsedData ?? [], + error: null, + }; + } catch (error) { + return { + data: null, + error: error as Error, + }; + } } + getContextMembers(): ApiResponse { throw new Error('Method not implemented.'); } diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 693ed57..c156514 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -35,7 +35,7 @@ import { } from '../../utils/storage'; import { useNavigate } from 'react-router-dom'; import { ContextApiDataSource } from '../../api/dataSource/ContractApiDataSource'; -import { ApprovalsCount, ContractProposal } from '../../api/contractApi'; +import { ApprovalsCount, ContextVariables, ContractProposal } from '../../api/contractApi'; import { Buffer } from 'buffer'; import bs58 from 'bs58'; import CreateProposalPopup, { @@ -153,6 +153,25 @@ const ProposalsWrapper = styled.div` } `; +const StyledTable = styled.table` + th, td { + text-align: center; + padding: 8px; + } +`; + +const ContextVariablesContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + .context-variables { + padding-left: 1rem; + padding-right: 1rem; + text-align: center; + } +`; + export default function HomePage() { const navigate = useNavigate(); const url = getAppEndpointKey(); @@ -170,7 +189,7 @@ export default function HomePage() { const [hasAlerted, setHasAlerted] = useState(false); const lastExecutedProposalRef = useRef(null); const [isModalOpen, setIsModalOpen] = useState(false); - + const [contextVariables, setContextVariables] = useState([]); useEffect(() => { if (!url || !applicationId || !accessToken || !refreshToken) { navigate('/auth'); @@ -398,6 +417,17 @@ export default function HomePage() { } } + async function getContextVariables() { + const result: ResponseData = + await new ContextApiDataSource().getContextVariables(); + if (result?.error) { + console.error('Error:', result.error); + } else { + // @ts-ignore + setContextVariables(result.data); + } + } + useEffect(() => { const setProposalData = async () => { await getProposalApprovals(); @@ -511,7 +541,38 @@ export default function HomePage() { Blockchain proposals demo application - + +
+ getContextVariables()}> + Get Context Variables + +
+
+

Context variables:

+ {contextVariables.length > 0 ? ( +
+ + + + Key + Value + + + + {contextVariables.map((variable) => ( + + {variable.key} + {variable.value} + + ))} + + +
+ ) : ( +
No context variables
+ )} +
+
Proposals