Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactoring on top of #2646 #2683

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions src/components/ProposalBuilder/ProposalBuilder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import { Box, Flex, Grid, GridItem } from '@chakra-ui/react';
import { ArrowLeft } from '@phosphor-icons/react';
import { Formik, FormikProps } from 'formik';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { DAO_ROUTES } from '../../constants/routes';
import { logError } from '../../helpers/errorLogging';
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 { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore';
import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore';
import { BigIntValuePair, CreateProposalSteps, ProposalExecuteData } from '../../types';
import {
CreateProposalForm,
CreateProposalTransaction,
CreateSablierProposalForm,
Stream,
} from '../../types/proposalBuilder';
import { CustomNonceInput } from '../ui/forms/CustomNonceInput';
import { Crumb } from '../ui/page/Header/Breadcrumbs';
import PageHeader from '../ui/page/Header/PageHeader';
import ProposalDetails from './ProposalDetails';
import ProposalMetadata, { ProposalMetadataTypeProps } from './ProposalMetadata';

export function ShowNonceInputOnMultisig({
nonce,
nonceOnChange,
}: {
nonce: number | undefined;
nonceOnChange: (nonce?: string) => void;
}) {
const {
governance: { isAzorius },
} = useFractal();

if (isAzorius) {
return null;
}

return (
<Flex
alignItems="center"
justifyContent="space-between"
marginBottom="2rem"
rounded="lg"
p="1.5rem"
bg="neutral-2"
>
<CustomNonceInput
nonce={nonce}
onChange={nonceOnChange}
align="end"
renderTrimmed={false}
/>
</Flex>
);
}

interface ProposalBuilderProps {
pageHeaderTitle: string;
pageHeaderBreadcrumbs: Crumb[];
pageHeaderButtonClickHandler: () => void;
proposalMetadataTypeProps: ProposalMetadataTypeProps;
actionsExperience: React.ReactNode | null;
stepButtons: ({
formErrors,
createProposalBlocked,
}: {
formErrors: boolean;
createProposalBlocked: boolean;
}) => React.ReactNode;
transactionsDetails:
| ((transactions: CreateProposalTransaction<BigIntValuePair>[]) => React.ReactNode)
| null;
templateDetails: ((title: string) => React.ReactNode) | null;
streamsDetails: ((streams: Stream[]) => React.ReactNode) | null;
prepareProposalData: (values: CreateProposalForm) => Promise<ProposalExecuteData | undefined>;
initialValues: CreateProposalForm;
contentRoute: (
formikProps: FormikProps<CreateProposalForm>,
pendingCreateTx: boolean,
nonce: number | undefined,
) => React.ReactNode;
}

export function ProposalBuilder({
pageHeaderTitle,
pageHeaderBreadcrumbs,
pageHeaderButtonClickHandler,
proposalMetadataTypeProps,
actionsExperience,
stepButtons,
transactionsDetails,
templateDetails,
streamsDetails,
initialValues,
prepareProposalData,
contentRoute,
}: ProposalBuilderProps) {
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation(['proposalTemplate', 'proposal']);

const paths = location.pathname.split('/');
const step = (paths[paths.length - 1] || paths[paths.length - 2]) as
| CreateProposalSteps
| undefined;
const { safe } = useDaoInfoStore();
const safeAddress = safe?.address;

const { addressPrefix } = useNetworkConfigStore();
const { submitProposal, pendingCreateTx } = useSubmitProposal();
const { canUserCreateProposal } = useCanUserCreateProposal();
const { createProposalValidation } = useCreateProposalSchema();

const successCallback = () => {
if (safeAddress) {
// Redirecting to home page so that user will see newly created Proposal
navigate(DAO_ROUTES.dao.relative(addressPrefix, safeAddress));
}
};

useEffect(() => {
if (safeAddress && (!step || !Object.values(CreateProposalSteps).includes(step))) {
navigate(DAO_ROUTES.proposalNew.relative(addressPrefix, safeAddress), { replace: true });
}
}, [safeAddress, step, navigate, addressPrefix]);

return (
<Formik<CreateProposalForm>
validationSchema={createProposalValidation}
initialValues={initialValues}
enableReinitialize
onSubmit={async values => {
if (!canUserCreateProposal) {
toast.error(t('errorNotProposer', { ns: 'common' }));
}

try {
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,
});
}
} catch (e) {
logError(e);
toast.error(t('encodingFailedMessage', { ns: 'proposal' }));
}
}}
>
{(formikProps: FormikProps<CreateProposalForm>) => {
const {
handleSubmit,
values: {
proposalMetadata: { title, description },
transactions,
nonce,
},
} = formikProps;

if (!safeAddress) {
return;
}

const trimmedTitle = title.trim();

const createProposalButtonDisabled =
!canUserCreateProposal ||
!!formikProps.errors.transactions ||
!!formikProps.errors.nonce ||
pendingCreateTx;

return (
<form onSubmit={handleSubmit}>
<Box>
<PageHeader
title={pageHeaderTitle}
breadcrumbs={pageHeaderBreadcrumbs}
ButtonIcon={ArrowLeft}
buttonProps={{
isDisabled: pendingCreateTx,
variant: 'secondary',
onClick: pageHeaderButtonClickHandler,
}}
/>
<Grid
gap={4}
marginTop="3rem"
templateColumns={{ base: '1fr', lg: '2fr 1fr' }}
templateAreas={{
base: '"content" "details"',
lg: '"content details"',
}}
>
<GridItem area="content">
<Flex
flexDirection="column"
align="left"
>
<Box
marginBottom="2rem"
rounded="lg"
bg="neutral-2"
>
<Routes>
<Route
path={CreateProposalSteps.METADATA}
element={
<ProposalMetadata
typeProps={proposalMetadataTypeProps}
{...formikProps}
/>
}
/>
{contentRoute(formikProps, pendingCreateTx, nonce)}
<Route
path="*"
element={
<Navigate
to={`${CreateProposalSteps.METADATA}${location.search}`}
replace
/>
}
/>
</Routes>
</Box>
{actionsExperience}
{stepButtons({
formErrors: !!formikProps.errors.proposalMetadata,
createProposalBlocked: createProposalButtonDisabled,
})}
</Flex>
</GridItem>
<GridItem
area="details"
w="100%"
>
<ProposalDetails
title={trimmedTitle}
description={description}
transactionsDetails={
transactionsDetails ? transactionsDetails(transactions) : null
}
templateDetails={templateDetails ? templateDetails(trimmedTitle) : null}
streamsDetails={
streamsDetails
? streamsDetails((formikProps.values as CreateSablierProposalForm).streams)
: null
}
/>
</GridItem>
</Grid>
</Box>
</form>
);
}}
</Formik>
);
}
Loading
Loading