diff --git a/src/components/CreateProposalTemplate/ProposalTemplateMetadata.tsx b/src/components/CreateProposalTemplate/ProposalTemplateMetadata.tsx deleted file mode 100644 index 3d58b366e3..0000000000 --- a/src/components/CreateProposalTemplate/ProposalTemplateMetadata.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Button, Divider, VStack } from '@chakra-ui/react'; -import { FormikProps } from 'formik'; -import { useTranslation } from 'react-i18next'; -import { - CreateProposalTemplateForm, - CreateProposalTemplateFormState, -} from '../../types/createProposalTemplate'; -import { InputComponent, TextareaComponent } from '../ui/forms/InputComponent'; - -export interface ProposalTemplateMetadataProps extends FormikProps { - setFormState: (state: CreateProposalTemplateFormState) => void; -} - -export default function ProposalTemplateMetadata({ - values: { proposalTemplateMetadata }, - setFieldValue, - errors: { proposalTemplateMetadata: proposalTemplateMetadataError }, - setFormState, -}: ProposalTemplateMetadataProps) { - const { t } = useTranslation(['proposalTemplate', 'common']); - - return ( - <> - - setFieldValue('proposalTemplateMetadata.title', e.target.value)} - disabled={false} - testId="metadata.title" - maxLength={50} - /> - setFieldValue('proposalTemplateMetadata.description', e.target.value)} - disabled={false} - rows={12} - /> - - - - - ); -} diff --git a/src/components/CreateProposalTemplate/constants.ts b/src/components/CreateProposalTemplate/constants.ts deleted file mode 100644 index f309684a4a..0000000000 --- a/src/components/CreateProposalTemplate/constants.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CreateProposalTemplateTransaction } from '../../types/createProposalTemplate'; - -export const DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION: CreateProposalTemplateTransaction = { - targetAddress: '', - ethValue: { value: '', bigintValue: undefined }, - functionName: '', - parameters: [ - { - signature: '', - label: '', - value: '', - }, - ], -}; - -export const DEFAULT_PROPOSAL_TEMPLATE = { - nonce: undefined, - proposalTemplateMetadata: { - title: '', - description: '', - }, - transactions: [DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION], -}; diff --git a/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx b/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx index 67d484c7d3..bd73002b2a 100644 --- a/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx +++ b/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx @@ -10,7 +10,7 @@ interface ITokenAllocations { setFieldValue: (field: string, value: any) => void; addressErrorMessage: string | null; amountErrorMessage: string | null; - amountInputValue: bigint | undefined; + amountInputValue?: bigint; allocationLength: number; } diff --git a/src/components/CreateProposalTemplate/ProposalTemplateDetails.tsx b/src/components/ProposalBuilder/ProposalDetails.tsx similarity index 82% rename from src/components/CreateProposalTemplate/ProposalTemplateDetails.tsx rename to src/components/ProposalBuilder/ProposalDetails.tsx index 851bf4c0b0..69a743d4ca 100644 --- a/src/components/CreateProposalTemplate/ProposalTemplateDetails.tsx +++ b/src/components/ProposalBuilder/ProposalDetails.tsx @@ -3,7 +3,7 @@ import { FormikProps } from 'formik'; import { Fragment, PropsWithChildren } from 'react'; import { useTranslation } from 'react-i18next'; import { BACKGROUND_SEMI_TRANSPARENT } from '../../constants/common'; -import { CreateProposalTemplateForm } from '../../types/createProposalTemplate'; +import { CreateProposalForm, ProposalBuilderMode } from '../../types/proposalBuilder'; import Markdown from '../ui/proposal/Markdown'; import '../../assets/css/Markdown.css'; @@ -29,10 +29,11 @@ export function TransactionValueContainer({ } export default function ProposalTemplateDetails({ - values: { proposalTemplateMetadata, transactions }, -}: FormikProps) { + values: { proposalMetadata, transactions }, + mode, +}: FormikProps & { mode: ProposalBuilderMode }) { const { t } = useTranslation(['proposalTemplate', 'proposal']); - const trimmedTitle = proposalTemplateMetadata.title?.trim(); + const trimmedTitle = proposalMetadata.title?.trim(); return ( {t('previewTitle')} {trimmedTitle} - - {t('previewThumnbail')} - {trimmedTitle && ( - title.slice(0, 2)} - /> - )} - + {mode === ProposalBuilderMode.TEMPLATE && ( + + {t('previewThumnbail')} + {trimmedTitle && ( + title.slice(0, 2)} + /> + )} + + )} {t('proposalTemplateDescription')} diff --git a/src/components/ProposalBuilder/ProposalMetadata.tsx b/src/components/ProposalBuilder/ProposalMetadata.tsx new file mode 100644 index 0000000000..65bc1b51ff --- /dev/null +++ b/src/components/ProposalBuilder/ProposalMetadata.tsx @@ -0,0 +1,79 @@ +import { Button, Divider, VStack } from '@chakra-ui/react'; +import { FormikProps } from 'formik'; +import { useTranslation } from 'react-i18next'; +import { CreateProposalState } from '../../types'; +import { CreateProposalForm, ProposalBuilderMode } from '../../types/proposalBuilder'; +import { InputComponent, TextareaComponent } from '../ui/forms/InputComponent'; + +export interface ProposalMetadataProps extends FormikProps { + setFormState: (state: CreateProposalState) => void; + mode: ProposalBuilderMode; +} + +export default function ProposalMetadata({ + values: { proposalMetadata }, + setFieldValue, + errors: { proposalMetadata: proposalMetadataError }, + setFormState, + mode, +}: ProposalMetadataProps) { + const { t } = useTranslation(['proposalTemplate', 'proposal', 'common']); + const isProposalMode = mode === ProposalBuilderMode.PROPOSAL; + + return ( + <> + + setFieldValue('proposalMetadata.title', e.target.value)} + disabled={false} + testId="metadata.title" + maxLength={50} + /> + setFieldValue('proposalMetadata.description', e.target.value)} + disabled={false} + rows={12} + /> + + + + + ); +} diff --git a/src/components/CreateProposalTemplate/ProposalTemplateTransaction.tsx b/src/components/ProposalBuilder/ProposalTransaction.tsx similarity index 79% rename from src/components/CreateProposalTemplate/ProposalTemplateTransaction.tsx rename to src/components/ProposalBuilder/ProposalTransaction.tsx index ed3c1c85e6..6793d61d1f 100644 --- a/src/components/CreateProposalTemplate/ProposalTemplateTransaction.tsx +++ b/src/components/ProposalBuilder/ProposalTransaction.tsx @@ -2,29 +2,32 @@ import { VStack, HStack, Text, Box, Flex, IconButton } from '@chakra-ui/react'; import { AddPlus, Minus } from '@decent-org/fractal-ui'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { CreateProposalTemplateTransaction } from '../../types/createProposalTemplate'; +import { CreateProposalTransaction, ProposalBuilderMode } from '../../types/proposalBuilder'; import ABISelector, { ABIElement } from '../ui/forms/ABISelector'; import ExampleLabel from '../ui/forms/ExampleLabel'; import { BigIntComponent, InputComponent } from '../ui/forms/InputComponent'; -import { DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION } from './constants'; +import { DEFAULT_PROPOSAL_TRANSACTION } from './constants'; -interface ProposalTemplateTransactionProps { - transaction: CreateProposalTemplateTransaction; +interface ProposalTransactionProps { + transaction: CreateProposalTransaction; transactionIndex: number; transactionPending: boolean; txAddressError?: string; txFunctionError?: string; setFieldValue: (field: string, value: any, shouldValidate?: boolean | undefined) => void; + mode: ProposalBuilderMode; } -export default function ProposalTemplateTransaction({ +export default function ProposalTransaction({ transaction, transactionIndex, transactionPending, txAddressError, txFunctionError, setFieldValue, -}: ProposalTemplateTransactionProps) { + mode, +}: ProposalTransactionProps) { + const isProposalMode = mode === ProposalBuilderMode.PROPOSAL; const { t } = useTranslation(['proposal', 'proposalTemplate', 'common']); const handleABISelectorChange = useCallback( (value: ABIElement) => { @@ -123,7 +126,7 @@ export default function ProposalTemplateTransaction({ onClick={() => setFieldValue(`transactions.${transactionIndex}.parameters`, [ ...transaction.parameters, - DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION, + DEFAULT_PROPOSAL_TRANSACTION, ]) } > @@ -170,34 +173,38 @@ export default function ProposalTemplateTransaction({ alignItems="center" mt={4} > - - setFieldValue( - `transactions.${transactionIndex}.parameters.${i}.label`, - e.target.value, - ) - } - disabled={transactionPending || !!parameter.value} - testId={`transactions.${transactionIndex}.parameters.${i}.label`} - subLabel={ - - {t('helperParameterLabel', { ns: 'proposalTemplate' })} - - } - gridContainerProps={{ - display: 'inline-flex', - flexWrap: 'wrap', - width: '30%', - }} - inputContainerProps={{ - width: '100%', - }} - /> - {t('or', { ns: 'common' })} + {!isProposalMode && ( + <> + + setFieldValue( + `transactions.${transactionIndex}.parameters.${i}.label`, + e.target.value, + ) + } + disabled={transactionPending || !!parameter.value} + testId={`transactions.${transactionIndex}.parameters.${i}.label`} + subLabel={ + + {t('helperParameterLabel', { ns: 'proposalTemplate' })} + + } + gridContainerProps={{ + display: 'inline-flex', + flexWrap: 'wrap', + width: '30%', + }} + inputContainerProps={{ + width: '100%', + }} + /> + {t('or', { ns: 'common' })} + + )} {t('example', { ns: 'common' })}: value - - {t('proposalTemplateLeaveBlank', { ns: 'proposalTemplate' })} - + {!isProposalMode && ( + + {t('proposalTemplateLeaveBlank', { ns: 'proposalTemplate' })} + + )} } @@ -282,7 +291,9 @@ export default function ProposalTemplateTransaction({ {`${t('example', { ns: 'common' })}:`} {'1.2'} - {t('ethParemeterHelper', { ns: 'proposalTemplate' })} + {!isProposalMode && ( + {t('ethParemeterHelper', { ns: 'proposalTemplate' })} + )} } errorMessage={undefined} diff --git a/src/components/CreateProposalTemplate/ProposalTemplateTransactions.tsx b/src/components/ProposalBuilder/ProposalTransactions.tsx similarity index 86% rename from src/components/CreateProposalTemplate/ProposalTemplateTransactions.tsx rename to src/components/ProposalBuilder/ProposalTransactions.tsx index 6c7a794c8a..eac985a805 100644 --- a/src/components/CreateProposalTemplate/ProposalTemplateTransactions.tsx +++ b/src/components/ProposalBuilder/ProposalTransactions.tsx @@ -13,24 +13,27 @@ import { Dispatch, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; import { BigIntValuePair } from '../../types'; import { - CreateProposalTemplateForm, - CreateProposalTemplateTransaction, -} from '../../types/createProposalTemplate'; -import ProposalTemplateTransaction from './ProposalTemplateTransaction'; + CreateProposalForm, + CreateProposalTransaction, + ProposalBuilderMode, +} from '../../types/proposalBuilder'; +import ProposalTransaction from './ProposalTransaction'; -interface ProposalTemplateTransactionsProps extends FormikProps { +interface ProposalTransactionsProps extends FormikProps { pendingTransaction: boolean; expandedIndecies: number[]; setExpandedIndecies: Dispatch>; + mode: ProposalBuilderMode; } -export default function ProposalTemplateTransactions({ +export default function ProposalTransactions({ values: { transactions }, errors, setFieldValue, pendingTransaction, expandedIndecies, setExpandedIndecies, -}: ProposalTemplateTransactionsProps) { + mode, +}: ProposalTransactionsProps) { const { t } = useTranslation(['proposal', 'proposalTemplate', 'common']); const removeTransaction = (transactionIndex: number) => { @@ -46,7 +49,7 @@ export default function ProposalTemplateTransactions({ > {transactions.map((_, index) => { const txErrors = errors?.transactions?.[index] as - | FormikErrors> + | FormikErrors> | undefined; const txAddressError = txErrors?.targetAddress; const txFunctionError = txErrors?.functionName; @@ -101,13 +104,14 @@ export default function ProposalTemplateTransactions({ )} - diff --git a/src/components/CreateProposalTemplate/ProposalTemplateTransactionsForm.tsx b/src/components/ProposalBuilder/ProposalTransactionsForm.tsx similarity index 78% rename from src/components/CreateProposalTemplate/ProposalTemplateTransactionsForm.tsx rename to src/components/ProposalBuilder/ProposalTransactionsForm.tsx index 080570b312..25e267eb00 100644 --- a/src/components/CreateProposalTemplate/ProposalTemplateTransactionsForm.tsx +++ b/src/components/ProposalBuilder/ProposalTransactionsForm.tsx @@ -3,24 +3,21 @@ import { Info } from '@decent-org/fractal-ui'; import { FormikProps } from 'formik'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - CreateProposalTemplateForm, - CreateProposalTemplateFormState, -} from '../../types/createProposalTemplate'; +import { CreateProposalState } from '../../types'; +import { CreateProposalForm, ProposalBuilderMode } from '../../types/proposalBuilder'; import { scrollToBottom } from '../../utils/ui'; -import ProposalTemplateTransactions from './ProposalTemplateTransactions'; -import { DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION } from './constants'; +import ProposalTransactions from './ProposalTransactions'; +import { DEFAULT_PROPOSAL_TRANSACTION } from './constants'; -interface ProposalTemplateTransactionsFormProps extends FormikProps { +interface ProposalTransactionsFormProps extends FormikProps { pendingTransaction: boolean; - setFormState: (state: CreateProposalTemplateFormState) => void; + setFormState: (state: CreateProposalState) => void; canUserCreateProposal?: boolean; safeNonce?: number; + mode: ProposalBuilderMode; } -export default function ProposalTemplateTransactionsForm( - props: ProposalTemplateTransactionsFormProps, -) { +export default function ProposalTransactionsForm(props: ProposalTransactionsFormProps) { const { pendingTransaction, setFormState, @@ -41,7 +38,7 @@ export default function ProposalTemplateTransactionsForm( return ( - { - setFieldValue('transactions', [...transactions, DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION]); + setFieldValue('transactions', [...transactions, DEFAULT_PROPOSAL_TRANSACTION]); setExpandedIndecies([transactions.length]); scrollToBottom(); }} @@ -85,7 +82,7 @@ export default function ProposalTemplateTransactionsForm( textStyle="text-md-mono-regular" color="gold.500" cursor="pointer" - onClick={() => setFormState(CreateProposalTemplateFormState.METADATA_FORM)} + onClick={() => setFormState(CreateProposalState.METADATA_FORM)} mb={4} > {t('back', { ns: 'common' })} diff --git a/src/components/ProposalBuilder/constants.ts b/src/components/ProposalBuilder/constants.ts new file mode 100644 index 0000000000..644c703897 --- /dev/null +++ b/src/components/ProposalBuilder/constants.ts @@ -0,0 +1,23 @@ +import { CreateProposalTransaction } from '../../types/proposalBuilder'; + +export const DEFAULT_PROPOSAL_TRANSACTION: CreateProposalTransaction = { + targetAddress: '', + ethValue: { value: '', bigintValue: undefined }, + functionName: '', + parameters: [ + { + signature: '', + label: '', + value: '', + }, + ], +}; + +export const DEFAULT_PROPOSAL = { + nonce: undefined, + proposalMetadata: { + title: '', + description: '', + }, + transactions: [DEFAULT_PROPOSAL_TRANSACTION], +}; diff --git a/src/components/ProposalBuilder/index.tsx b/src/components/ProposalBuilder/index.tsx new file mode 100644 index 0000000000..57e2fc3038 --- /dev/null +++ b/src/components/ProposalBuilder/index.tsx @@ -0,0 +1,204 @@ +import { Box, Flex, Grid, GridItem, Text } from '@chakra-ui/react'; +import { Trash } from '@decent-org/fractal-ui'; +import { Formik, FormikProps } from 'formik'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { BACKGROUND_SEMI_TRANSPARENT } from '../../constants/common'; +import { DAO_ROUTES, BASE_ROUTES } from '../../constants/routes'; +import useSubmitProposal from '../../hooks/DAO/proposal/useSubmitProposal'; +import useCreateProposalSchema from '../../hooks/schemas/proposalBuilder/useCreateProposalSchema'; +import { useCanUserCreateProposal } from '../../hooks/utils/useCanUserSubmitProposal'; +import { useFractal } from '../../providers/App/AppProvider'; +import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { CreateProposalState, ProposalExecuteData } from '../../types'; +import { CreateProposalForm, ProposalBuilderMode } from '../../types/proposalBuilder'; +import { CustomNonceInput } from '../ui/forms/CustomNonceInput'; +import PageHeader from '../ui/page/Header/PageHeader'; +import ProposalDetails from './ProposalDetails'; +import ProposalMetadata from './ProposalMetadata'; +import ProposalTransactionsForm from './ProposalTransactionsForm'; + +interface IProposalBuilder { + mode: ProposalBuilderMode; + prepareProposalData: (values: CreateProposalForm) => Promise; + initialValues: CreateProposalForm; +} + +const templateAreaTwoCol = '"content details"'; +const templateAreaSingleCol = `"content" + "details"`; + +export default function ProposalBuilder({ + mode, + initialValues, + prepareProposalData, +}: IProposalBuilder) { + const [formState, setFormState] = useState(CreateProposalState.METADATA_FORM); + const { t } = useTranslation(['proposalTemplate', 'proposal']); + + const isProposalMode = mode === ProposalBuilderMode.PROPOSAL; + + const navigate = useNavigate(); + const { + node: { daoAddress, safe }, + } = useFractal(); + const { addressPrefix } = useNetworkConfig(); + const { submitProposal, pendingCreateTx } = useSubmitProposal(); + const { canUserCreateProposal } = useCanUserCreateProposal(); + const { createProposalValidation } = useCreateProposalSchema(); + + const successCallback = () => { + if (daoAddress) { + // Redirecting to proposals page so that user will see Proposal for Proposal Template creation + navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)); + } + }; + + return ( + + validationSchema={createProposalValidation} + initialValues={initialValues} + enableReinitialize + onSubmit={async values => { + if (canUserCreateProposal) { + const proposalData = await prepareProposalData(values); + if (proposalData) { + submitProposal({ + proposalData, + nonce: values?.nonce, + pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), + successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), + failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), + successCallback, + }); + } + } + }} + > + {(formikProps: FormikProps) => { + const { handleSubmit } = formikProps; + + if (!daoAddress) { + return; + } + + return ( +
+ + + navigate( + daoAddress + ? isProposalMode + ? DAO_ROUTES.proposals.relative(addressPrefix, daoAddress) + : DAO_ROUTES.proposalTemplates.relative(addressPrefix, daoAddress) + : BASE_ROUTES.landing, + ) + } + isButtonDisabled={pendingCreateTx} + /> + + + + + {formState === CreateProposalState.METADATA_FORM ? ( + + ) : ( + <> + + + {formikProps.values.proposalMetadata.title} + + formikProps.setFieldValue('nonce', newNonce)} + align="end" + /> + + + + )} + + + + + + + + +
+ ); + }} + + ); +} diff --git a/src/components/ProposalCreate/ProposalDetails.tsx b/src/components/ProposalCreate/ProposalDetails.tsx deleted file mode 100644 index 2592d2f3de..0000000000 --- a/src/components/ProposalCreate/ProposalDetails.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Box, Divider, Flex, HStack, Text } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; -import { BACKGROUND_SEMI_TRANSPARENT } from '../../constants/common'; -import { useFractal } from '../../providers/App/AppProvider'; -import { AzoriusGovernance, GovernanceType } from '../../types'; -import ContentBoxTitle from '../ui/containers/ContentBox/ContentBoxTitle'; -import { BarLoader } from '../ui/loaders/BarLoader'; - -export function ProposalDetails() { - const { - node: { daoAddress, safe }, - governance, - } = useFractal(); - const { type } = governance; - const azoriusGovernance = governance as AzoriusGovernance; - const { t } = useTranslation(['proposal']); - return ( - - {!type || !daoAddress || !safe ? ( - - - - ) : ( - - {t('proposalSummaryTitle')} - - {type === GovernanceType.MULTISIG ? ( - - {t('labelProposalSigners')} - - {safe.threshold}/{safe.owners?.length} - - - ) : ( - <> - - {t('labelProposalVotingPeriod')} - {azoriusGovernance.votingStrategy?.votingPeriod?.formatted} - - - {t('labelProposalQuorum')} - - {azoriusGovernance.votingStrategy?.quorumPercentage?.formatted || - azoriusGovernance.votingStrategy?.quorumThreshold?.formatted} - - - - {t('labelProposalTimelock')} - {azoriusGovernance.votingStrategy?.timeLockPeriod?.formatted} - - - )} - - )} - - ); -} diff --git a/src/components/ProposalCreate/ProposalHeader.tsx b/src/components/ProposalCreate/ProposalHeader.tsx deleted file mode 100644 index 38983b7773..0000000000 --- a/src/components/ProposalCreate/ProposalHeader.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { HStack, Spacer, Text, Tooltip } from '@chakra-ui/react'; -import { Alert } from '@decent-org/fractal-ui'; -import { useTranslation } from 'react-i18next'; -import { TOOLTIP_MAXW } from '../../constants/common'; -import { CustomNonceInput } from '../ui/forms/CustomNonceInput'; - -export function ProposalHeader({ - isAzorius, - metadataTitle, - nonce, - setNonce, -}: { - isAzorius?: boolean; - metadataTitle?: string; - nonce?: number; - setNonce: (nonce?: number) => void; -}) { - const { t } = useTranslation('proposal'); - - return ( - - - {metadataTitle ? metadataTitle : t('proposal', { ns: 'proposal' })} - - {!isAzorius && ( - - - - )} - - {!isAzorius && ( - - )} - - ); -} diff --git a/src/components/ProposalCreate/ProposalMetadata.tsx b/src/components/ProposalCreate/ProposalMetadata.tsx deleted file mode 100644 index a25642fc5c..0000000000 --- a/src/components/ProposalCreate/ProposalMetadata.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Button, Divider, VStack } from '@chakra-ui/react'; -import { FormikProps } from 'formik'; -import { useTranslation } from 'react-i18next'; -import { CreateProposalForm, CreateProposalState } from '../../types'; -import { InputComponent, TextareaComponent } from '../ui/forms/InputComponent'; - -export interface AzoriusMetadataProps extends FormikProps { - isVisible: boolean; - setFormState: (state: CreateProposalState) => void; -} - -function ProposalMetadata(props: AzoriusMetadataProps) { - const { - values: { proposalMetadata }, - setFieldValue, - errors: { proposalMetadata: proposalMetadataError }, - isVisible, - setFormState, - } = props; - const { t } = useTranslation(['proposal', 'common']); - - if (!isVisible) return null; - - return ( - <> - - setFieldValue('proposalMetadata.title', e.target.value)} - disabled={false} - placeholder={t('proposalTitlePlaceholder')} - testId="metadata.title" - /> - setFieldValue('proposalMetadata.description', e.target.value)} - disabled={false} - placeholder={t('proposalDescriptionPlaceholder')} - rows={9} - /> - setFieldValue('proposalMetadata.documentationUrl', e.target.value)} - value={proposalMetadata.documentationUrl} - disabled={false} - placeholder={t('proposalAdditionalResourcesPlaceholder')} - errorMessage={ - proposalMetadata.documentationUrl && proposalMetadataError?.documentationUrl - } - testId="metadata.documentationUrl" - /> - - - - - ); -} - -export default ProposalMetadata; diff --git a/src/components/ProposalCreate/Transaction.tsx b/src/components/ProposalCreate/Transaction.tsx deleted file mode 100644 index 59cbd7a6bf..0000000000 --- a/src/components/ProposalCreate/Transaction.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { VStack, HStack, Text } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; -import { CreateProposalTransaction } from '../../types/createProposal'; -import ExampleLabel from '../ui/forms/ExampleLabel'; -import { BigIntComponent, InputComponent } from '../ui/forms/InputComponent'; - -interface TransactionProps { - transaction: CreateProposalTransaction; - transactionIndex: number; - transactionPending: boolean; - txAddressError?: string; - txFunctionError?: string; - setFieldValue: (field: string, value: any, shouldValidate?: boolean | undefined) => void; -} - -function Transaction({ - transaction, - transactionIndex, - transactionPending, - txAddressError, - txFunctionError, - setFieldValue, -}: TransactionProps) { - const { t } = useTranslation(['proposal', 'common']); - - return ( - - - {`${t('example', { ns: 'common' })}:`} - 0x4168592... - - } - errorMessage={transaction.targetAddress && txAddressError ? txAddressError : undefined} - value={transaction.targetAddress} - testId="transaction.targetAddress" - onChange={e => - setFieldValue(`transactions.${transactionIndex}.targetAddress`, e.target.value) - } - /> - - - setFieldValue(`transactions.${transactionIndex}.functionName`, e.target.value) - } - disabled={transactionPending} - subLabel={ - - {`${t('example', { ns: 'common' })}:`} - transfer - - } - // @todo update withn new error messages - errorMessage={undefined} - testId="transaction.functionName" - /> - - setFieldValue(`transactions.${transactionIndex}.functionSignature`, e.target.value) - } - disabled={transactionPending} - subLabel={ - - {`${t('example', { ns: 'common' })}:`} - address, uint256 - - } - testId="transaction.functionSignature" - errorMessage={ - transaction.functionSignature && txFunctionError ? txFunctionError : undefined - } - /> - setFieldValue(`transactions.${transactionIndex}.parameters`, e.target.value)} - disabled={transactionPending} - subLabel={ - - {`${t('example', { ns: 'common' })}:`} - - {'0xADC74eE329a23060d3CB431Be0AB313740c191E7, 1000000000'} - - - } - testId="transaction.parameters" - errorMessage={transaction.parameters && txFunctionError ? txFunctionError : undefined} - /> - - - {`${t('example', { ns: 'common' })}:`} - {'1.2'} - - } - errorMessage={undefined} - value={transaction.ethValue.bigintValue} - onChange={e => { - setFieldValue(`transactions.${transactionIndex}.ethValue`, e); - }} - decimalPlaces={18} - /> - - ); -} - -export default Transaction; diff --git a/src/components/ProposalCreate/Transactions.tsx b/src/components/ProposalCreate/Transactions.tsx deleted file mode 100644 index 5598f5e5c9..0000000000 --- a/src/components/ProposalCreate/Transactions.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { - Box, - Accordion, - AccordionButton, - AccordionItem, - AccordionPanel, - HStack, - IconButton, -} from '@chakra-ui/react'; -import { ArrowDown, ArrowRight, Minus } from '@decent-org/fractal-ui'; -import { FormikErrors, FormikProps } from 'formik'; -import { Dispatch, SetStateAction } from 'react'; -import { useTranslation } from 'react-i18next'; -import { BigIntValuePair, CreateProposalForm, CreateProposalTransaction } from '../../types'; -import Transaction from './Transaction'; - -interface TransactionsProps extends FormikProps { - isVisible: boolean; - pendingTransaction: boolean; - expandedIndecies: number[]; - setExpandedIndecies: Dispatch>; -} -function Transactions({ - values: { transactions }, - errors, - setFieldValue, - pendingTransaction, - expandedIndecies, - setExpandedIndecies, -}: TransactionsProps) { - const { t } = useTranslation(['proposal', 'common']); - - const removeTransaction = (transactionIndex: number) => { - const transactionsArr = [...transactions]; - transactionsArr.splice(transactionIndex, 1); - setFieldValue('transactions', transactionsArr); - }; - return ( - - {transactions.map((_, index) => { - const txErrors = errors?.transactions?.[index] as - | FormikErrors> - | undefined; - const txAddressError = txErrors?.targetAddress; - const txFunctionError = txErrors?.encodedFunctionData; - - return ( - - {({ isExpanded }) => ( - - - { - setExpandedIndecies(indexArray => { - if (indexArray.includes(index)) { - const newTxArr = [...indexArray]; - newTxArr.splice(newTxArr.indexOf(index), 1); - return newTxArr; - } else { - return [...indexArray, index]; - } - }); - }} - p={0} - textStyle="text-button-md-semibold" - color="grayscale.100" - > - {isExpanded ? : } - {t('transaction')} {index + 1} - - {index !== 0 || transactions.length !== 1 ? ( - } - aria-label={t('removetransactionlabel')} - variant="unstyled" - onClick={() => removeTransaction(index)} - minWidth="auto" - _hover={{ color: 'gold.500' }} - _disabled={{ opacity: 0.4, cursor: 'default' }} - sx={{ '&:disabled:hover': { color: 'inherit', opacity: 0.4 } }} - isDisabled={pendingTransaction} - /> - ) : ( - - )} - - - - - - )} - - ); - })} - - ); -} - -export default Transactions; diff --git a/src/components/ProposalCreate/TransactionsForm.tsx b/src/components/ProposalCreate/TransactionsForm.tsx deleted file mode 100644 index 43d83bd404..0000000000 --- a/src/components/ProposalCreate/TransactionsForm.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Button, Box, Flex, Text, VStack, Divider, Alert, AlertTitle } from '@chakra-ui/react'; -import { Info } from '@decent-org/fractal-ui'; -import { FormikProps } from 'formik'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { CreateProposalForm, CreateProposalState } from '../../types'; -import { scrollToBottom } from '../../utils/ui'; -import Transactions from './Transactions'; -import { DEFAULT_TRANSACTION } from './constants'; - -interface TransactionsFormProps extends FormikProps { - isVisible: boolean; - pendingTransaction: boolean; - setFormState: (state: CreateProposalState) => void; - canUserCreateProposal?: boolean; -} - -function TransactionsForm(props: TransactionsFormProps) { - const { - isVisible, - pendingTransaction, - setFormState, - setFieldValue, - values: { transactions }, - errors: { transactions: transactionsError }, - canUserCreateProposal, - } = props; - const { t } = useTranslation(['proposal', 'common']); - const [expandedIndecies, setExpandedIndecies] = useState([0]); - - if (!isVisible) return null; - - return ( - - - - - - - - - {t('transactionExecutionAlertMessage')} - - - - - - - - - - - ); -} - -export default TransactionsForm; diff --git a/src/components/ProposalCreate/constants.ts b/src/components/ProposalCreate/constants.ts deleted file mode 100644 index 025a6a8060..0000000000 --- a/src/components/ProposalCreate/constants.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const DEFAULT_TRANSACTION = { - targetAddress: '', - ethValue: { value: '0', bigintValue: 0n }, - functionName: '', - functionSignature: '', - parameters: '', - encodedFunctionData: undefined, -}; - -export const DEFAULT_PROPOSAL = { - proposalMetadata: { - title: '', - description: '', - documentationUrl: '', - }, - transactions: [DEFAULT_TRANSACTION], - nonce: 0, -}; diff --git a/src/components/ProposalTemplates/ProposalTemplateCard.tsx b/src/components/ProposalTemplates/ProposalTemplateCard.tsx index cc8336146a..fcecd49fdf 100644 --- a/src/components/ProposalTemplates/ProposalTemplateCard.tsx +++ b/src/components/ProposalTemplates/ProposalTemplateCard.tsx @@ -9,7 +9,7 @@ import useSubmitProposal from '../../hooks/DAO/proposal/useSubmitProposal'; import { useCanUserCreateProposal } from '../../hooks/utils/useCanUserSubmitProposal'; import { useFractal } from '../../providers/App/AppProvider'; import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; -import { ProposalTemplate } from '../../types/createProposalTemplate'; +import { ProposalTemplate } from '../../types/proposalBuilder'; import ContentBox from '../ui/containers/ContentBox'; import { OptionMenu } from '../ui/menus/OptionMenu'; import { ModalType } from '../ui/modals/ModalProvider'; diff --git a/src/components/ui/forms/BigIntInput.tsx b/src/components/ui/forms/BigIntInput.tsx index e1a43bf0e5..58ef224efd 100644 --- a/src/components/ui/forms/BigIntInput.tsx +++ b/src/components/ui/forms/BigIntInput.tsx @@ -13,7 +13,7 @@ import { BigIntValuePair } from '../../../types'; export interface BigIntInputProps extends Omit, FormControlOptions { - value: bigint | undefined; + value?: bigint; onChange: (value: BigIntValuePair) => void; decimalPlaces?: number; min?: string; diff --git a/src/components/ui/modals/ForkProposalTemplateModal.tsx b/src/components/ui/modals/ForkProposalTemplateModal.tsx index ddf9b177ac..71b86644d5 100644 --- a/src/components/ui/modals/ForkProposalTemplateModal.tsx +++ b/src/components/ui/modals/ForkProposalTemplateModal.tsx @@ -9,7 +9,7 @@ import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitP import useSignerOrProvider from '../../../hooks/utils/useSignerOrProvider'; import { useFractal } from '../../../providers/App/AppProvider'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; -import { ProposalTemplate } from '../../../types/createProposalTemplate'; +import { ProposalTemplate } from '../../../types/proposalBuilder'; import { InputComponent } from '../forms/InputComponent'; interface IForkProposalTemplateModalProps { diff --git a/src/components/ui/modals/ProposalTemplateModal.tsx b/src/components/ui/modals/ProposalTemplateModal.tsx index e3565a7d70..6cc11a8431 100644 --- a/src/components/ui/modals/ProposalTemplateModal.tsx +++ b/src/components/ui/modals/ProposalTemplateModal.tsx @@ -20,8 +20,7 @@ import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitP import { useFractal } from '../../../providers/App/AppProvider'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; import { BigIntValuePair } from '../../../types'; -import { ProposalTemplate } from '../../../types/createProposalTemplate'; -import { isValidUrl } from '../../../utils/url'; +import { ProposalTemplate } from '../../../types/proposalBuilder'; import { CustomNonceInput } from '../forms/CustomNonceInput'; import { BigIntComponent, InputComponent } from '../forms/InputComponent'; import Markdown from '../proposal/Markdown'; @@ -115,27 +114,10 @@ export default function ProposalTemplateModal({ documentationUrl: '', }; - const proposalTransactions = filledProposalTransactions.map( - ({ targetAddress, ethValue, functionName, parameters }) => { - return { - targetAddress: utils.getAddress(targetAddress), // Safe proposal creation/execution might fail if targetAddress is not checksummed - ethValue, - functionName, - functionSignature: parameters.map(parameter => parameter.signature.trim()).join(', '), - parameters: parameters - .map(parameter => - isValidUrl(parameter.value!.trim()) - ? encodeURIComponent(parameter.value!.trim()) // If parameter.value is valid URL with special symbols like ":" or "?" - decoding might fail, thus we need to encode URL - : parameter.value!.trim(), - ) - .join(', '), - }; - }, - ); try { const proposalData = await prepareProposal({ proposalMetadata, - transactions: proposalTransactions, + transactions: filledProposalTransactions, }); submitProposal({ diff --git a/src/hooks/DAO/loaders/governance/useAzoriusListeners.ts b/src/hooks/DAO/loaders/governance/useAzoriusListeners.ts index 1c431262fa..6fdd1bd518 100644 --- a/src/hooks/DAO/loaders/governance/useAzoriusListeners.ts +++ b/src/hooks/DAO/loaders/governance/useAzoriusListeners.ts @@ -12,7 +12,7 @@ import { useFractal } from '../../../../providers/App/AppProvider'; import { FractalGovernanceAction } from '../../../../providers/App/governance/action'; import { useEthersProvider } from '../../../../providers/Ethers/hooks/useEthersProvider'; import { - ProposalMetadata, + CreateProposalMetadata, VotingStrategyType, DecodedTransaction, FractalActions, @@ -50,7 +50,7 @@ const proposalCreatedEventListener = ( const typedTransactions = transactions.map(t => ({ ...t, value: t.value.toBigInt() })); - const metaDataEvent: ProposalMetadata = JSON.parse(metadata); + const metaDataEvent: CreateProposalMetadata = JSON.parse(metadata); const proposalData = { metaData: { title: metaDataEvent.title, diff --git a/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts b/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts index 1dd8cef267..60ba70925f 100644 --- a/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts +++ b/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts @@ -8,7 +8,7 @@ import { VotedEvent as ERC721VotedEvent } from '@fractal-framework/fractal-contr import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useFractal } from '../../../../providers/App/AppProvider'; import { useEthersProvider } from '../../../../providers/Ethers/hooks/useEthersProvider'; -import { ProposalMetadata, VotingStrategyType, DecodedTransaction } from '../../../../types'; +import { CreateProposalMetadata, VotingStrategyType, DecodedTransaction } from '../../../../types'; import { AzoriusProposal } from '../../../../types/daoProposal'; import { Providers } from '../../../../types/network'; import { mapProposalCreatedEventToProposal, decodeTransactions } from '../../../../utils'; @@ -140,7 +140,9 @@ export const useAzoriusProposals = () => { for (const proposalCreatedEvent of proposalCreatedEvents) { let proposalData; if (proposalCreatedEvent.args.metadata) { - const metadataEvent: ProposalMetadata = JSON.parse(proposalCreatedEvent.args.metadata); + const metadataEvent: CreateProposalMetadata = JSON.parse( + proposalCreatedEvent.args.metadata, + ); const decodedTransactions = await decodeTransactions( _decode, proposalCreatedEvent.args.transactions.map(t => ({ ...t, value: t.value.toBigInt() })), diff --git a/src/hooks/DAO/proposal/useCreateProposalTemplate.ts b/src/hooks/DAO/proposal/useCreateProposalTemplate.ts index 3aaa6bc815..0fee5cdb6e 100644 --- a/src/hooks/DAO/proposal/useCreateProposalTemplate.ts +++ b/src/hooks/DAO/proposal/useCreateProposalTemplate.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { useFractal } from '../../../providers/App/AppProvider'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; import { ProposalExecuteData } from '../../../types'; -import { CreateProposalTemplateForm } from '../../../types/createProposalTemplate'; +import { CreateProposalForm } from '../../../types/proposalBuilder'; import { couldBeENS } from '../../../utils/url'; import useSafeContracts from '../../safe/useSafeContracts'; import useSignerOrProvider from '../../utils/useSignerOrProvider'; @@ -17,7 +17,7 @@ export default function useCreateProposalTemplate() { } = useFractal(); const prepareProposalTemplateProposal = useCallback( - async (values: CreateProposalTemplateForm) => { + async (values: CreateProposalForm) => { if (proposalTemplates && signerOrProvider && keyValuePairsContract) { const proposalMetadata = { title: 'Create Proposal Template', @@ -27,8 +27,8 @@ export default function useCreateProposalTemplate() { }; const proposalTemplateData = { - title: values.proposalTemplateMetadata.title.trim(), - description: values.proposalTemplateMetadata.description.trim(), + title: values.proposalMetadata.title.trim(), + description: values.proposalMetadata.description.trim(), transactions: await Promise.all( values.transactions.map(async tx => ({ ...tx, diff --git a/src/hooks/DAO/proposal/useGetMetadata.ts b/src/hooks/DAO/proposal/useGetMetadata.ts index edcc6975c9..cf7bf93d7a 100644 --- a/src/hooks/DAO/proposal/useGetMetadata.ts +++ b/src/hooks/DAO/proposal/useGetMetadata.ts @@ -2,7 +2,11 @@ import { utils } from 'ethers'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; -import { FractalProposal, ProposalMetadata, SafeMultisigTransactionResponse } from '../../../types'; +import { + FractalProposal, + CreateProposalMetadata, + SafeMultisigTransactionResponse, +} from '../../../types'; import { CacheKeys } from '../../utils/cache/cacheDefaults'; import { DBObjectKeys, useIndexedDB } from '../../utils/cache/useLocalDB'; @@ -20,15 +24,15 @@ interface Transaction { const useGetMultisigMetadata = (proposal: FractalProposal | null | undefined) => { const ipfsClient = useIPFSClient(); - const [multisigMetadata, setMultisigMetadata] = useState( - undefined, - ); + const [multisigMetadata, setMultisigMetadata] = useState< + CreateProposalMetadata | null | undefined + >(undefined); const [setValue, getValue] = useIndexedDB(DBObjectKeys.DECODED_TRANSACTIONS); const fetchMultisigMetadata = useCallback(async () => { if (!proposal) return; - const cached: ProposalMetadata = await getValue( + const cached: CreateProposalMetadata = await getValue( CacheKeys.MULTISIG_METADATA_PREFIX + proposal.proposalId, ); if (cached) { @@ -59,7 +63,7 @@ const useGetMultisigMetadata = (proposal: FractalProposal | null | undefined) => try { const decoded = new utils.AbiCoder().decode(['string'], encodedMetadata); const ipfsHash = (decoded as string[])[0]; - const meta: ProposalMetadata = await ipfsClient.cat(ipfsHash); + const meta: CreateProposalMetadata = await ipfsClient.cat(ipfsHash); // cache the metadata JSON await setValue(CacheKeys.MULTISIG_METADATA_PREFIX + proposal.proposalId, meta); @@ -78,7 +82,9 @@ const useGetMultisigMetadata = (proposal: FractalProposal | null | undefined) => return { multisigMetadata }; }; -export const useGetMetadata = (proposal: FractalProposal | null | undefined): ProposalMetadata => { +export const useGetMetadata = ( + proposal: FractalProposal | null | undefined, +): CreateProposalMetadata => { const { multisigMetadata } = useGetMultisigMetadata(proposal); const { t } = useTranslation('dashboard'); diff --git a/src/hooks/DAO/proposal/usePrepareProposal.ts b/src/hooks/DAO/proposal/usePrepareProposal.ts index 61680b757d..4e60043391 100644 --- a/src/hooks/DAO/proposal/usePrepareProposal.ts +++ b/src/hooks/DAO/proposal/usePrepareProposal.ts @@ -1,8 +1,8 @@ import { useCallback } from 'react'; import { useEthersSigner } from '../../../providers/Ethers/hooks/useEthersSigner'; -import { CreateProposalForm } from '../../../types'; +import { CreateProposalForm } from '../../../types/proposalBuilder'; import { encodeFunction } from '../../../utils/crypto'; -import { couldBeENS } from '../../../utils/url'; +import { couldBeENS, isValidUrl } from '../../../utils/url'; export function usePrepareProposal() { const signer = useEthersSigner(); @@ -10,10 +10,26 @@ export function usePrepareProposal() { async (values: CreateProposalForm) => { const { transactions, proposalMetadata } = values; const transactionsWithEncoding = transactions.map(tx => { - return { - ...tx, - encodedFunctionData: encodeFunction(tx.functionName, tx.functionSignature, tx.parameters), - }; + if (!tx.functionName) { + return { + ...tx, + calldata: '0x', + }; + } else { + const signature = tx.parameters.map(parameter => parameter.signature.trim()).join(', '); + const parameters = tx.parameters + .map(parameter => + isValidUrl(parameter.value!.trim()) + ? encodeURIComponent(parameter.value!.trim()) // If parameter.value is valid URL with special symbols like ":" or "?" - decoding might fail, thus we need to encode URL + : parameter.value!.trim(), + ) + .join(', '); + + return { + ...tx, + calldata: encodeFunction(tx.functionName, signature, parameters), + }; + } }); const targets = await Promise.all( transactionsWithEncoding.map(tx => { @@ -23,12 +39,11 @@ export function usePrepareProposal() { return tx.targetAddress; }), ); + return { targets, - values: transactionsWithEncoding.map(transaction => transaction.ethValue.bigintValue!), - calldatas: transactionsWithEncoding.map( - transaction => transaction.encodedFunctionData || '', - ), + values: transactionsWithEncoding.map(transaction => transaction.ethValue.bigintValue || 0n), + calldatas: transactionsWithEncoding.map(transaction => transaction.calldata || ''), metaData: { title: proposalMetadata.title, description: proposalMetadata.description, diff --git a/src/hooks/DAO/proposal/useSubmitProposal.ts b/src/hooks/DAO/proposal/useSubmitProposal.ts index b5a9590781..bfec37a83e 100644 --- a/src/hooks/DAO/proposal/useSubmitProposal.ts +++ b/src/hooks/DAO/proposal/useSubmitProposal.ts @@ -15,7 +15,7 @@ import { useSafeAPI } from '../../../providers/App/hooks/useSafeAPI'; import { useEthersProvider } from '../../../providers/Ethers/hooks/useEthersProvider'; import { useEthersSigner } from '../../../providers/Ethers/hooks/useEthersSigner'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; -import { MetaTransaction, ProposalExecuteData, ProposalMetadata } from '../../../types'; +import { MetaTransaction, ProposalExecuteData, CreateProposalMetadata } from '../../../types'; import { buildSafeApiUrl, getAzoriusModuleFromModules } from '../../../utils'; import useSafeContracts from '../../safe/useSafeContracts'; import useSignerOrProvider from '../../utils/useSignerOrProvider'; @@ -116,7 +116,7 @@ export default function useSubmitProposal() { proposalData.metaData.description || proposalData.metaData.documentationUrl ) { - const metaData: ProposalMetadata = { + const metaData: CreateProposalMetadata = { title: proposalData.metaData.title || '', description: proposalData.metaData.description || '', documentationUrl: proposalData.metaData.documentationUrl || '', diff --git a/src/hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema.ts b/src/hooks/schemas/proposalBuilder/useCreateProposalSchema.ts similarity index 83% rename from src/hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema.ts rename to src/hooks/schemas/proposalBuilder/useCreateProposalSchema.ts index 9da83d62af..cb7b9a92ae 100644 --- a/src/hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema.ts +++ b/src/hooks/schemas/proposalBuilder/useCreateProposalSchema.ts @@ -5,10 +5,10 @@ import { useFractal } from '../../../providers/App/AppProvider'; import { useValidationAddress } from '../common/useValidationAddress'; /** - * validation schema for Create Proposal Template workflow + * validation schema for Create Proposal workflow * @dev https://www.npmjs.com/package/yup */ -const useCreateProposalTemplateSchema = () => { +const useCreateProposalSchema = () => { const { t } = useTranslation('proposal'); const { addressValidationTest } = useValidationAddress(); const { @@ -30,7 +30,7 @@ const useCreateProposalTemplateSchema = () => { return false; }; - const createProposalTemplateValidation = useMemo( + const createProposalValidation = useMemo( () => Yup.object().shape({ transactions: Yup.array() @@ -59,9 +59,10 @@ const useCreateProposalTemplateSchema = () => { ), }), ), - proposalTemplateMetadata: Yup.object().shape({ + proposalMetadata: Yup.object().shape({ title: Yup.string().trim().required().max(50), - description: Yup.string().trim().notRequired().max(300), + description: Yup.string().trim().notRequired(), + documentationUrl: Yup.string().trim().notRequired(), }), nonce: Yup.number() .required() @@ -69,7 +70,7 @@ const useCreateProposalTemplateSchema = () => { }), [addressValidationTest, t, safe], ); - return { createProposalTemplateValidation }; + return { createProposalValidation }; }; -export default useCreateProposalTemplateSchema; +export default useCreateProposalSchema; diff --git a/src/hooks/schemas/proposalCreate/useCreateProposalSchema.ts b/src/hooks/schemas/proposalCreate/useCreateProposalSchema.ts deleted file mode 100644 index 7389278cd8..0000000000 --- a/src/hooks/schemas/proposalCreate/useCreateProposalSchema.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import * as Yup from 'yup'; -import { encodeFunction } from '../../../utils/crypto'; -import { useValidationAddress } from '../common/useValidationAddress'; - -/** - * validation schema for Create Proposal workflow - * @dev https://www.npmjs.com/package/yup - */ -export const useCreateProposalSchema = () => { - const { addressValidationTest } = useValidationAddress(); - - const { t } = useTranslation('proposal'); - - const createProposalValidation = useMemo( - () => - Yup.object().shape({ - transactions: Yup.array() - .min(1) - .of( - Yup.object().shape({ - targetAddress: Yup.string().required().test(addressValidationTest), - ethValue: Yup.object().shape({ - value: Yup.string(), - }), - functionName: Yup.string().matches(/^[a-z0-9]+$/i, { - message: t('functionNameError'), - }), - functionSignature: Yup.string(), - parameters: Yup.string(), - encodedFunctionData: Yup.string().test({ - message: t('errorInvalidFragments'), - test: (_, context) => { - const functionName = context.parent.functionName; - const functionSignature = context.parent.functionSignature; - const parameters = context.parent.parameters; - if (!functionName) return false; - const encodedFunction = encodeFunction( - functionName, - functionSignature, - parameters, - ); - - return !!encodedFunction; - }, - }), - }), - ), - proposalMetadata: Yup.object().shape({ - title: Yup.string().notRequired(), - description: Yup.string().notRequired(), - documentationUrl: Yup.string() - .notRequired() - .when({ - is: (value?: string) => !!value, - then: schema => - schema.test({ - message: t('Invalid URL'), - test: value => { - try { - return Boolean(new URL(value || '')); - } catch (e) { - return false; - } - }, - }), - }), - }), - nonce: Yup.number(), - }), - [addressValidationTest, t], - ); - return { createProposalValidation }; -}; diff --git a/src/i18n/locales/en/proposal.json b/src/i18n/locales/en/proposal.json index 2f8da0fd37..fe1656b144 100644 --- a/src/i18n/locales/en/proposal.json +++ b/src/i18n/locales/en/proposal.json @@ -2,7 +2,7 @@ "proposalOverview": "Proposal Overview", "breakdownTitle": "Breakdown", "labelTargetAddress": "Target Address", - "helperTargetAddress": "The smart contract address this proposal will modify", + "helperTargetAddress": "The smart contract address this proposal will modify. Paste an address and we'll try to fetch the ABI for this contract.", "labelFunctionName": "Function Name", "helperFunctionName": "The name of the function to be called if this proposal passes", "labelFunctionSignature": "Function Signature", diff --git a/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx b/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx index 5e1f59bd1d..c705da3d42 100644 --- a/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx @@ -1,41 +1,16 @@ -import { Box, Flex, Grid, GridItem, Text } from '@chakra-ui/react'; -import { Trash } from '@decent-org/fractal-ui'; -import { Formik, FormikProps } from 'formik'; import { useEffect, useState, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import ProposalTemplateDetails from '../../../../../components/CreateProposalTemplate/ProposalTemplateDetails'; -import ProposalTemplateMetadata from '../../../../../components/CreateProposalTemplate/ProposalTemplateMetadata'; -import ProposalTemplateTransactionsForm from '../../../../../components/CreateProposalTemplate/ProposalTemplateTransactionsForm'; -import { DEFAULT_PROPOSAL_TEMPLATE } from '../../../../../components/CreateProposalTemplate/constants'; -import { CustomNonceInput } from '../../../../../components/ui/forms/CustomNonceInput'; -import PageHeader from '../../../../../components/ui/page/Header/PageHeader'; -import { BACKGROUND_SEMI_TRANSPARENT } from '../../../../../constants/common'; -import { BASE_ROUTES, DAO_ROUTES } from '../../../../../constants/routes'; +import { useSearchParams } from 'react-router-dom'; +import ProposalBuilder from '../../../../../components/ProposalBuilder'; +import { DEFAULT_PROPOSAL } from '../../../../../components/ProposalBuilder/constants'; import { logError } from '../../../../../helpers/errorLogging'; import useCreateProposalTemplate from '../../../../../hooks/DAO/proposal/useCreateProposalTemplate'; -import useSubmitProposal from '../../../../../hooks/DAO/proposal/useSubmitProposal'; -import useCreateProposalTemplateSchema from '../../../../../hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema'; -import { useCanUserCreateProposal } from '../../../../../hooks/utils/useCanUserSubmitProposal'; -import { useFractal } from '../../../../../providers/App/AppProvider'; import useIPFSClient from '../../../../../providers/App/hooks/useIPFSClient'; -import { useNetworkConfig } from '../../../../../providers/NetworkConfig/NetworkConfigProvider'; -import { - CreateProposalTemplateForm, - CreateProposalTemplateFormState, - ProposalTemplate, -} from '../../../../../types/createProposalTemplate'; - -const templateAreaTwoCol = '"content details"'; -const templateAreaSingleCol = `"content" - "details"`; +import { ProposalBuilderMode, ProposalTemplate } from '../../../../../types/proposalBuilder'; export default function CreateProposalTemplatePage() { - const [formState, setFormState] = useState(CreateProposalTemplateFormState.METADATA_FORM); - const [initialProposalTemplate, setInitialProposalTemplate] = useState(DEFAULT_PROPOSAL_TEMPLATE); - const { t } = useTranslation(['proposalTemplate', 'proposal']); - - const navigate = useNavigate(); + const ipfsClient = useIPFSClient(); + const [initialProposalTemplate, setInitialProposalTemplate] = useState(DEFAULT_PROPOSAL); + const { prepareProposalTemplateProposal } = useCreateProposalTemplate(); const [searchParams] = useSearchParams(); const defaultProposalTemplatesHash = useMemo( () => searchParams?.get('templatesHash'), @@ -46,24 +21,6 @@ export default function CreateProposalTemplatePage() { [searchParams], ); - const { - node: { daoAddress, safe }, - } = useFractal(); - const { addressPrefix } = useNetworkConfig(); - - const { prepareProposalTemplateProposal } = useCreateProposalTemplate(); - const { submitProposal, pendingCreateTx } = useSubmitProposal(); - const { canUserCreateProposal } = useCanUserCreateProposal(); - const { createProposalTemplateValidation } = useCreateProposalTemplateSchema(); - const ipfsClient = useIPFSClient(); - - const successCallback = () => { - if (daoAddress) { - // Redirecting to proposals page so that user will see Proposal for Proposal Template creation - navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)); - } - }; - useEffect(() => { const loadInitialTemplate = async () => { if (defaultProposalTemplatesHash && defaultProposalTemplateIndex) { @@ -73,7 +30,7 @@ export default function CreateProposalTemplatePage() { if (initialTemplate) { const newInitialValue = { nonce: undefined, - proposalTemplateMetadata: { + proposalMetadata: { title: initialTemplate.title, description: initialTemplate.description || '', }, @@ -96,125 +53,10 @@ export default function CreateProposalTemplatePage() { }, [defaultProposalTemplatesHash, defaultProposalTemplateIndex, ipfsClient]); return ( - - validationSchema={createProposalTemplateValidation} + { - if (canUserCreateProposal) { - const proposalData = await prepareProposalTemplateProposal(values); - if (proposalData) { - submitProposal({ - proposalData, - nonce: values?.nonce, - pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), - successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), - failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), - successCallback, - }); - } - } - }} - > - {(formikProps: FormikProps) => { - const { handleSubmit } = formikProps; - - if (!daoAddress) { - return; - } - - return ( -
- - - navigate( - daoAddress - ? DAO_ROUTES.proposalTemplates.relative(addressPrefix, daoAddress) - : BASE_ROUTES.landing, - ) - } - isButtonDisabled={pendingCreateTx} - /> - - - - - {formState === CreateProposalTemplateFormState.METADATA_FORM ? ( - - ) : ( - <> - - - {formikProps.values.proposalTemplateMetadata.title} - - formikProps.setFieldValue('nonce', newNonce)} - align="end" - /> - - - - )} - - - - - - - - -
- ); - }} - + prepareProposalData={prepareProposalTemplateProposal} + /> ); } diff --git a/src/pages/daos/[daoAddress]/proposals/new/index.tsx b/src/pages/daos/[daoAddress]/proposals/new/index.tsx index bd0954259a..fada2ade2c 100644 --- a/src/pages/daos/[daoAddress]/proposals/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposals/new/index.tsx @@ -1,55 +1,18 @@ -import { Grid, GridItem, Box, Flex, Center } from '@chakra-ui/react'; -import { Trash } from '@decent-org/fractal-ui'; -import { Formik, FormikProps } from 'formik'; -import { useState, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { ProposalDetails } from '../../../../../components/ProposalCreate/ProposalDetails'; -import { ProposalHeader } from '../../../../../components/ProposalCreate/ProposalHeader'; -import ProposalMetadata from '../../../../../components/ProposalCreate/ProposalMetadata'; -import TransactionsForm from '../../../../../components/ProposalCreate/TransactionsForm'; -import { DEFAULT_PROPOSAL } from '../../../../../components/ProposalCreate/constants'; +import { Center } from '@chakra-ui/react'; +import ProposalBuilder from '../../../../../components/ProposalBuilder'; +import { DEFAULT_PROPOSAL } from '../../../../../components/ProposalBuilder/constants'; import { BarLoader } from '../../../../../components/ui/loaders/BarLoader'; -import PageHeader from '../../../../../components/ui/page/Header/PageHeader'; -import { BACKGROUND_SEMI_TRANSPARENT, HEADER_HEIGHT } from '../../../../../constants/common'; -import { DAO_ROUTES } from '../../../../../constants/routes'; +import { HEADER_HEIGHT } from '../../../../../constants/common'; import { usePrepareProposal } from '../../../../../hooks/DAO/proposal/usePrepareProposal'; -import useSubmitProposal from '../../../../../hooks/DAO/proposal/useSubmitProposal'; -import { useCreateProposalSchema } from '../../../../../hooks/schemas/proposalCreate/useCreateProposalSchema'; -import { useCanUserCreateProposal } from '../../../../../hooks/utils/useCanUserSubmitProposal'; import { useFractal } from '../../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../../providers/NetworkConfig/NetworkConfigProvider'; -import { CreateProposalForm, CreateProposalState, GovernanceType } from '../../../../../types'; +import { ProposalBuilderMode } from '../../../../../types'; -const templateAreaTwoCol = '"content details"'; -const templateAreaSingleCol = `"content" - "details"`; - -export default function ProposalCreatePage() { +export default function CreateProposalPage() { const { node: { daoAddress, safe }, governance: { type }, } = useFractal(); - const { addressPrefix } = useNetworkConfig(); - const { createProposalValidation } = useCreateProposalSchema(); const { prepareProposal } = usePrepareProposal(); - const { submitProposal, pendingCreateTx } = useSubmitProposal(); - const { canUserCreateProposal } = useCanUserCreateProposal(); - - const navigate = useNavigate(); - const { t } = useTranslation(['proposal', 'common', 'breadcrumbs']); - - const [formState, setFormState] = useState(CreateProposalState.METADATA_FORM); - const isAzorius = useMemo( - () => type === GovernanceType.AZORIUS_ERC20 || type === GovernanceType.AZORIUS_ERC721, - [type], - ); - - const successCallback = () => { - if (daoAddress) { - navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)); - } - }; if (!type || !daoAddress || !safe) { return ( @@ -60,108 +23,10 @@ export default function ProposalCreatePage() { } return ( - - validationSchema={createProposalValidation} + { - const { nonce } = values; - const proposalData = await prepareProposal(values); - submitProposal({ - proposalData, - nonce, - pendingToastMessage: t('proposalCreatePendingToastMessage'), - successToastMessage: t('proposalCreateSuccessToastMessage'), - failedToastMessage: t('proposalCreateFailureToastMessage'), - successCallback, - }); - }} - validateOnMount - isInitialValid={false} - > - {(formikProps: FormikProps) => { - const { handleSubmit, setFieldValue, values } = formikProps; - return ( -
- - - navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)) - } - isButtonDisabled={pendingCreateTx} - /> - - - - - { - setFieldValue('nonce', nonce); - }} - /> - - - - - - - - - - - -
- ); - }} - + mode={ProposalBuilderMode.PROPOSAL} + prepareProposalData={prepareProposal} + /> ); } diff --git a/src/providers/App/governance/action.ts b/src/providers/App/governance/action.ts index 601f08b628..7e1baab1d3 100644 --- a/src/providers/App/governance/action.ts +++ b/src/providers/App/governance/action.ts @@ -10,7 +10,7 @@ import { GovernanceType, ERC721TokenData, } from '../../../types'; -import { ProposalTemplate } from '../../../types/createProposalTemplate'; +import { ProposalTemplate } from '../../../types/proposalBuilder'; export enum FractalGovernanceAction { SET_GOVERNANCE_TYPE = 'SET_GOVERNANCE_TYPE', diff --git a/src/types/createProposalTemplate.ts b/src/types/createProposalTemplate.ts deleted file mode 100644 index 2046a4655f..0000000000 --- a/src/types/createProposalTemplate.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BigIntValuePair } from './common'; - -export enum CreateProposalTemplateFormState { - METADATA_FORM, - TRANSACTIONS_FORM, -} - -export interface CreateProposalTemplateTransaction { - targetAddress: string; - ethValue: T; - functionName: string; - parameters: { - signature: string; - label?: string; - value?: string; - }[]; -} - -export type CreateProposalTemplateMetadata = { - title: string; - description: string; -}; - -export type CreateProposalTemplateForm = { - transactions: CreateProposalTemplateTransaction[]; - proposalTemplateMetadata: CreateProposalTemplateMetadata; - nonce?: number; -}; - -export type ProposalTemplate = { - transactions: CreateProposalTemplateTransaction[]; -} & CreateProposalTemplateMetadata; diff --git a/src/types/daoProposal.ts b/src/types/daoProposal.ts index 80be7418d4..cc6bf6712b 100644 --- a/src/types/daoProposal.ts +++ b/src/types/daoProposal.ts @@ -1,10 +1,10 @@ -import { ProposalMetadata } from './createProposal'; import { GovernanceActivity } from './fractal'; +import { CreateProposalMetadata } from './proposalBuilder'; import { SafeMultisigConfirmationResponse } from './safeGlobal'; import { MetaTransaction, DecodedTransaction } from './transaction'; export interface ProposalExecuteData extends ExecuteData { - metaData: ProposalMetadata; + metaData: CreateProposalMetadata; } export interface ExecuteData { @@ -19,7 +19,7 @@ export type CreateProposalFunc = (proposal: { }) => void; export type ProposalData = { - metaData?: ProposalMetadata; + metaData?: CreateProposalMetadata; transactions?: MetaTransaction[]; decodedTransactions: DecodedTransaction[]; }; diff --git a/src/types/fractal.ts b/src/types/fractal.ts index 78378d41c1..8c0bc7b3b6 100644 --- a/src/types/fractal.ts +++ b/src/types/fractal.ts @@ -34,10 +34,10 @@ import { TreasuryActions } from '../providers/App/treasury/action'; import { NodeActions } from './../providers/App/node/action'; import { ERC721TokenData, VotesTokenData } from './account'; import { ContractConnection } from './contract'; -import { ProposalTemplate } from './createProposalTemplate'; import { FreezeGuardType, FreezeVotingType } from './daoGovernance'; import { ProposalData, MultisigProposal, AzoriusProposal, SnapshotProposal } from './daoProposal'; import { TreasuryActivity } from './daoTreasury'; +import { ProposalTemplate } from './proposalBuilder'; import { AllTransfersListResponse, SafeInfoResponseWithGuard } from './safeGlobal'; import { BIFormattedPair } from './votingFungibleToken'; /** @@ -177,10 +177,10 @@ export enum SafeTransferType { } export interface ITokenAccount { - userBalance: bigint | undefined; + userBalance?: bigint; userBalanceString: string | undefined; delegatee: string | undefined; - votingWeight: bigint | undefined; + votingWeight?: bigint; votingWeightString: string | undefined; isDelegatesSet: boolean | undefined; } diff --git a/src/types/index.ts b/src/types/index.ts index e4968af8de..39819b0953 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,7 +2,7 @@ export * from './account'; export * from './common'; export * from './contract'; export * from './createDAO'; -export * from './createProposal'; +export * from './proposalBuilder'; export * from './daoGeneral'; export * from './daoGovernance'; export * from './daoGuard'; diff --git a/src/types/createProposal.ts b/src/types/proposalBuilder.ts similarity index 50% rename from src/types/createProposal.ts rename to src/types/proposalBuilder.ts index 5a62fbc7ef..6caef5659d 100644 --- a/src/types/createProposal.ts +++ b/src/types/proposalBuilder.ts @@ -9,19 +9,29 @@ export interface CreateProposalTransaction { targetAddress: string; ethValue: T; functionName: string; - functionSignature: string; - parameters: string; - encodedFunctionData?: string; + parameters: { + signature: string; + label?: string; + value?: string; + }[]; } -export type ProposalMetadata = { +export type CreateProposalMetadata = { title: string; description: string; - documentationUrl: string; + documentationUrl?: string; }; +export enum ProposalBuilderMode { + PROPOSAL = 'PROPOSAL', + TEMPLATE = 'TEMPLATE', +} export type CreateProposalForm = { transactions: CreateProposalTransaction[]; - proposalMetadata: ProposalMetadata; + proposalMetadata: CreateProposalMetadata; nonce?: number; }; + +export type ProposalTemplate = { + transactions: CreateProposalTransaction[]; +} & CreateProposalMetadata;