import { Close, GavelRounded, Speed, Visibility, VisibilityOff, } from '@mui/icons-material' import clsx from 'clsx' import Fuse from 'fuse.js' import { ComponentType, useState } from 'react' import { FieldValues, SubmitErrorHandler, SubmitHandler, UnpackNestedValue, useFormContext, } from 'react-hook-form' import { useTranslation } from 'react-i18next' import TimeAgo from 'react-timeago' import { ActiveThreshold, BaseNewProposalProps, LoadingData, } from '@dao-dao/types' import { formatDateTime, formatPercentOf100, formatTime, processError, } from '@dao-dao/utils' import { useHoldingKey } from '../../hooks' import { Button } from '../buttons' import { IconButton } from '../icon_buttons' import { FilterableItem, FilterableItemPopup } from '../popup' import { Tooltip } from '../tooltip' enum ProposeSubmitValue { Preview = 'Preview', Submit = 'Submit', } type BaseProps = Omit< BaseNewProposalProps, 'onCreateSuccess' > export type NewProposalProps< FormData extends FieldValues = any, ProposalData extends unknown = any, > = BaseProps & { content: { Header: ComponentType Main: ComponentType, 'actionsReadOnlyMode'>> Preview: ComponentType } getProposalDataFromFormData: ( formData: UnpackNestedValue ) => Promise createProposal: (newProposalData: ProposalData) => Promise simulateProposal: (newProposalData: ProposalData) => Promise proposalTitle: string isWalletConnecting: boolean additionalSubmitError?: string isPaused: boolean isActive: boolean activeThreshold: ActiveThreshold | null isMember: LoadingData /** * If defined, this is the reason why the user cannot propose. */ cannotProposeReason?: string depositUnsatisfied: boolean connected: boolean simulationBypassExpiration?: Date } export const NewProposal = < FormData extends FieldValues = any, ProposalData extends unknown = any, >({ content: { Header, Main, Preview }, getProposalDataFromFormData, createProposal, simulateProposal, proposalTitle, isWalletConnecting, additionalSubmitError, isPaused, isActive, activeThreshold, isMember, cannotProposeReason, depositUnsatisfied, connected, draft, saveDraft, drafts, loadDraft, unloadDraft, draftSaving, deleteDraft, simulationBypassExpiration, proposalModuleSelector, actionsReadOnlyMode, ProposalDaoInfoCards, }: NewProposalProps) => { const { t } = useTranslation() const { getValues, handleSubmit } = useFormContext() const [showPreview, setShowPreview] = useState(false) const [submitError, setSubmitError] = useState('') const holdingAltForSimulation = useHoldingKey({ key: 'alt' }) const holdingShiftForForce = useHoldingKey({ key: 'shift' }) const [loading, setLoading] = useState(false) const onSubmitForm: SubmitHandler = async (formData, event) => { setSubmitError('') const nativeEvent = event?.nativeEvent as SubmitEvent const submitterValue = (nativeEvent?.submitter as HTMLInputElement)?.value if (submitterValue === ProposeSubmitValue.Preview) { return } setLoading(true) try { const data = await getProposalDataFromFormData(formData) if (holdingAltForSimulation) { await simulateProposal(data) } else { await createProposal(data) } } catch (err) { console.error(err) setSubmitError( processError(err, { forceCapture: false, }) ) } finally { setLoading(false) } } const onSubmitError: SubmitErrorHandler = async (errors) => { console.error('Form errors', errors) // Even on error, try to simulate proposal. if (holdingAltForSimulation) { setLoading(true) try { const data = await getProposalDataFromFormData(getValues()) await simulateProposal(data) } catch (err) { console.error(err) setSubmitError( processError(err, { forceCapture: false, }) ) } finally { setLoading(false) } // Even on error, force publish if holding shift. } else if (holdingShiftForForce) { setLoading(true) try { const data = await getProposalDataFromFormData(getValues()) await createProposal(data) } catch (err) { console.error(err) setSubmitError( processError(err, { forceCapture: false, }) ) } finally { setLoading(false) } // If not simulating or forcing, show error to check for errors. } else { setSubmitError(t('error.correctErrorsAbove')) } } return (
{!actionsReadOnlyMode && proposalModuleSelector}

{t('info.reviewYourProposal')}

{!!cannotProposeReason && !isMember.loading && !isWalletConnecting && (

{cannotProposeReason}

)} {simulationBypassExpiration && (

{t('info.bypassSimulationExplanation')}

)} {!!submitError && (

{submitError}

)} {showPreview && (
)}
{draft ? ( <>

{draftSaving ? t('info.draftSaving') : t('info.draftSavedAtTime', { time: formatTime(new Date(draft.lastUpdatedAt)), })}

) : ( <> {drafts.length > 0 && !!loadDraft && ( ({ key: createdAt, label: name, description: ( <> {t('title.created')}:{' '} {formatDateTime(new Date(createdAt))}
{t('title.lastUpdated')}:{' '} {formatDateTime(new Date(lastUpdatedAt))} ), rightNode: ( { // Don't click on item button. event.stopPropagation() deleteDraft(index) }} variant="ghost" /> ), }) )} onSelect={(_, index) => loadDraft(index)} searchPlaceholder={t('info.searchDraftPlaceholder')} trigger={{ type: 'button', props: { variant: 'secondary', children: t('button.loadDraft'), }, }} /> )} )}
) } const FILTERABLE_KEYS: Fuse.FuseOptionKey[] = ['label']