diff --git a/src/App.tsx b/src/App.tsx index 7581952..2bd47e2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { VocdoniEnvironment } from '~constants' import { translations } from '~i18n/components' import { clientToSigner } from '~util/wagmi-adapters' import { RoutesProvider } from './router' +import { ExternalProvider, JsonRpcFetchFunc } from '@ethersproject/providers' export const App = () => { const { data } = useWalletClient() @@ -15,7 +16,11 @@ export const App = () => { let signer: Signer = {} as Signer if (data) { - signer = clientToSigner(data) + signer = clientToSigner({ + transport: data.transport as ExternalProvider | JsonRpcFetchFunc, + chain: data.chain, + account: data.account, + }) } return ( diff --git a/src/components/Process/Aside.tsx b/src/components/Process/Aside.tsx index 12633c4..c2b831d 100644 --- a/src/components/Process/Aside.tsx +++ b/src/components/Process/Aside.tsx @@ -25,14 +25,16 @@ const ProcessAside = () => { } = useElection() const { isConnected } = useAccount() const { env, clear } = useClient() - if (!election || !(election instanceof PublishedElection)) return null const census: CensusMeta = dotobject(election?.meta || {}, 'census') + + const isMultiProcess = !!election.get('multiprocess') const renderVoteMenu = - voted || - (voting && election?.electionType.anonymous) || - (hasOverwriteEnabled(election) && isInCensus && votesLeft > 0 && voted) + !isMultiProcess && + (voted || + (voting && election?.electionType.anonymous) || + (hasOverwriteEnabled(election) && isInCensus && votesLeft > 0 && voted)) const showVoters = election?.status !== ElectionStatus.CANCELED && @@ -197,7 +199,8 @@ const ProcessAside = () => { ) } -export const VoteButton = ({ ...props }: FlexProps) => { +type VoteButtonProps = FlexProps +export const VoteButton = (props: VoteButtonProps) => { const { t } = useTranslation() const { election, connected, isAbleToVote, isInCensus } = useElection() const { isConnected } = useAccount() @@ -215,8 +218,7 @@ export const VoteButton = ({ ...props }: FlexProps) => { return null } - const isWeighted = election?.census.weight !== election?.census.size - + const isWeighted = Number(election?.census.weight) !== election?.census.size const isBlindCsp = census?.type === 'csp' && election?.meta.csp?.service === 'vocdoni-blind-csp' return ( @@ -271,7 +273,8 @@ export const VoteButton = ({ ...props }: FlexProps) => { mb={4} sx={{ '&::disabled': { - opacity: '0.8', + // opacity: '0.8', + display: 'none', }, }} /> diff --git a/src/components/Process/Chained.tsx b/src/components/Process/Chained.tsx index 76bcb88..03dc122 100644 --- a/src/components/Process/Chained.tsx +++ b/src/components/Process/Chained.tsx @@ -1,22 +1,54 @@ -import { Box, Progress } from '@chakra-ui/react' +import { Box, Flex, Progress, useBreakpointValue } from '@chakra-ui/react' import { ConnectButton } from '@rainbow-me/rainbowkit' -import { ElectionQuestions, ElectionResults, SpreadsheetAccess } from '@vocdoni/chakra-components' +import { + ElectionQuestions, + ElectionQuestionsForm, + ElectionResults, + QuestionsFormProvider, + RenderWith, + SpreadsheetAccess, +} from '@vocdoni/chakra-components' import { ElectionProvider, useElection } from '@vocdoni/react-providers' import { InvalidElection, IVotePackage, PublishedElection, VocdoniSDKClient } from '@vocdoni/sdk' -import { useEffect, useState } from 'react' +import { PropsWithChildren, useEffect, useState } from 'react' import { Trans } from 'react-i18next' -import { VoteButton } from './Aside' -import { ChainedProvider, useChainedProcesses } from './ChainedContext' -import { ConfirmVoteModal } from './ConfirmVoteModal' +import { VoteButton } from '~components/Process/Aside' import BlindCSPConnect from '~components/Process/BlindCSPConnect' +import { ConfirmVoteModal } from '~components/Process/ConfirmVoteModal' +import { MultiElectionSuccessVoteModal, SuccessVoteModal } from '~components/Process/SuccessVoteModal' +import VotingVoteModal, { MultiElectionVotingVoteModal } from '~components/Process/VotingVoteModal' +import { ChainedProvider, useChainedProcesses } from './ChainedContext' type ChainedProcessesInnerProps = { connected: boolean } +const VoteButtonContainer = ({ children }: PropsWithChildren) => { + const isBreakPoint = useBreakpointValue({ base: true, lg2: false }) + if (isBreakPoint) { + return ( + + {children} + + ) + } + return ( + + {children} + + ) +} + const ChainedProcessesInner = ({ connected }: ChainedProcessesInnerProps) => { const { election, voted, setClient, clearClient } = useElection() - const { processes, client, current, setProcess, setCurrent } = useChainedProcesses() + const { processes, client, current, setProcess, setCurrent, root } = useChainedProcesses() // clear session of local context when login out useEffect(() => { @@ -26,7 +58,9 @@ const ChainedProcessesInner = ({ connected }: ChainedProcessesInnerProps) => { // ensure the client is set to the root one useEffect(() => { - setClient(client) + if (election.id !== root.id) { + setClient(client) + } }, [client, election]) // fetch current process and process flow logic @@ -40,6 +74,7 @@ const ChainedProcessesInner = ({ connected }: ChainedProcessesInnerProps) => { // fetch votes info const next = await getNextProcessInFlow(client, voted, meta) + if (typeof next === 'undefined') return // If cannot found next process, return if (typeof processes[next] === 'undefined') { const election = await client.fetchElection(next) setProcess(next, election) @@ -48,6 +83,18 @@ const ChainedProcessesInner = ({ connected }: ChainedProcessesInnerProps) => { })() }, [processes, current, voted, client]) + const [renderWith, setRenderWith] = useState([]) + // Effect to set renderWith component state. + useEffect(() => { + if (!current || processes[current] instanceof InvalidElection) return + const currentElection = processes[current] + const meta = currentElection.get('multiprocess') + if (meta && meta.renderWith) { + setRenderWith(meta.renderWith) + } + }, [current, processes]) + const isRenderWith = renderWith.length > 0 + if (!current || !processes[current]) { return } @@ -56,15 +103,33 @@ const ChainedProcessesInner = ({ connected }: ChainedProcessesInnerProps) => { return Invalid election } + if (isRenderWith) { + return ( + } + > + + + + + + + + ) + } + return ( - + <> } + confirmContents={(elections, answers) => } /> - + - - + + + + ) } @@ -114,29 +179,43 @@ const ChainedProcessesWrapper = () => { return } + if (processes[current] instanceof InvalidElection) { + return Invalid election + } + const isBlindCsp = election.get('census.type') === 'csp' && election?.meta.csp?.service === 'vocdoni-blind-csp' return ( - <> - + + {current === election.id ? ( - - {!connected && election.get('census.type') === 'spreadsheet' && } - {isBlindCsp && !connected && } - + ) : ( + + + + )} + + + {!connected && election.get('census.type') === 'spreadsheet' && } + {isBlindCsp && !connected && } + + + ) } const ChainedResultsWrapper = () => { // note election context refers to the root election here, ALWAYS - const { election, client } = useElection() + const { election, client, voted } = useElection() const { processes, setProcess } = useChainedProcesses() const [loaded, setLoaded] = useState(false) const [loading, setLoading] = useState(false) const [sorted, setSorted] = useState([]) + // Get elections information useEffect(() => { if (!election || election instanceof InvalidElection || loading || loaded) return + setLoading(true) ;(async () => { try { @@ -154,6 +233,16 @@ const ChainedResultsWrapper = () => { })() }, [election]) + // Reset loaded state when root election voted changes. + // On renderWith elections this will update all the render elections. + // However, this fix won't work on chained processes since we should check voted state for all processes to update + // results on real time. + useEffect(() => { + if (voted) { + setLoaded(false) + } + }, [voted]) + if (!loaded) { return } @@ -204,12 +293,16 @@ const getProcessIdsInFlowStep = (meta: FlowNode) => { ids.push(meta.default) } - if (!meta.conditions) { - return ids + if (meta.renderWith) { + for (const renderWith of meta.renderWith) { + ids.push(renderWith.id) + } } - for (const condition of meta.conditions) { - ids.push(condition.goto) + if (meta.conditions) { + for (const condition of meta.conditions) { + ids.push(condition.goto) + } } return ids @@ -229,7 +322,7 @@ export const getAllProcessesInFlow = async ( processes[id] = election const meta = election.get('multiprocess') - if (meta && meta.default && !visited.has(meta.default)) { + if (meta && (meta.default || meta.renderWith) && !visited.has(meta.default)) { const idsToFetch = getProcessIdsInFlowStep(meta) for (const nextId of idsToFetch) { await loadProcess(nextId) @@ -245,6 +338,16 @@ export const getAllProcessesInFlow = async ( } } + // Add renderWith processes + if (meta.renderWith) { + for (const renderWithElection of meta.renderWith as RenderWith[]) { + if (!visited.has(renderWithElection.id)) { + visited.add(renderWithElection.id) + ids.push(renderWithElection.id) + } + } + } + // Add defaults after conditions if (!visited.has(meta.default)) { visited.add(meta.default) @@ -259,6 +362,7 @@ export const getAllProcessesInFlow = async ( if (meta) { initialIds.push(...getProcessIdsInFlowStep(meta)) } + for (const id of initialIds) { await loadProcess(id) } @@ -274,7 +378,15 @@ type FlowCondition = { goto: string } -type FlowNode = { - default: string - conditions?: FlowCondition[] -} +// FlowNode can have or conditions or renderWith, but not both +export type FlowNode = + | { + conditions?: FlowCondition[] + renderWith?: never + default: string + } + | { + conditions?: never + renderWith: RenderWith[] + default?: string // Default is optional for renderWith elections + } diff --git a/src/components/Process/ConfirmVoteModal.tsx b/src/components/Process/ConfirmVoteModal.tsx index 5ef87c8..b72fbd5 100644 --- a/src/components/Process/ConfirmVoteModal.tsx +++ b/src/components/Process/ConfirmVoteModal.tsx @@ -1,19 +1,15 @@ import { Box, Button, Flex, ModalBody, ModalFooter, ModalHeader, Text, useMultiStyleConfig } from '@chakra-ui/react' -import { useConfirm } from '@vocdoni/chakra-components' -import { ElectionResultsTypeNames, PublishedElection } from '@vocdoni/sdk' -import { FieldValues } from 'react-hook-form' +import { useConfirm, QuestionsConfirmationProps } from '@vocdoni/chakra-components' +import { ElectionResultsTypeNames } from '@vocdoni/sdk' import { Trans, useTranslation } from 'react-i18next' import { IoWarningOutline } from 'react-icons/io5' import confirmImg from '/assets/spreadsheet-confirm-modal.jpg' -export const ConfirmVoteModal = ({ election, answers }: { election: PublishedElection; answers: FieldValues }) => { +export const ConfirmVoteModal = ({ elections, answers }: QuestionsConfirmationProps) => { const { t } = useTranslation() const styles = useMultiStyleConfig('ConfirmModal') const { cancel, proceed } = useConfirm() - const canAbstain = - election.resultsType.name === ElectionResultsTypeNames.MULTIPLE_CHOICE && election.resultsType.properties.canAbstain - return ( <> @@ -21,76 +17,107 @@ export const ConfirmVoteModal = ({ election, answers }: { election: PublishedEle {t('process.spreadsheet.confirm.description')} - - {election.questions.map((q, i) => ( - - - - , - }} - values={{ - answer: q.title.default, - number: i + 1, - }} - /> - - {election.resultsType.name === ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION ? ( - - , - }} - values={{ - answer: q.choices[Number(answers[i])].title.default, - number: i + 1, - }} - /> + {Object.values(elections).map(({ election, isAbleToVote }) => { + const canAbstain = + election.resultsType.name === ElectionResultsTypeNames.MULTIPLE_CHOICE && + election.resultsType.properties.canAbstain + const eAnswers = answers[election.id] + if (!isAbleToVote) { + return ( + + + + {election.title.default} + + + {t('vote.not_able_to_vote')} - ) : ( + + + ) + } + return ( + <> + + {election.questions.map((q, i) => ( + + + + , + }} + values={{ + answer: q.title.default, + number: i + 1, + }} + /> + + {election.resultsType.name === ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION ? ( + + , + }} + values={{ + answer: q.choices[Number(eAnswers[i])].title.default, + number: i + 1, + }} + /> + + ) : ( + + , + }} + values={{ + answers: + eAnswers[0].length === 0 + ? t('process.spreadsheet.confirm.blank_vote') + : eAnswers[0] + .map((a: string) => q.choices[Number(a)].title.default) + .map((a: string) => `- ${a}`) + .join('
'), + }} + /> +
+ )} +
+ {i + 1 !== election.questions.length && } + + ))} +
+ {canAbstain && eAnswers[0].length < election.voteType.maxCount! && ( + + - , - }} - values={{ - answers: - answers[0].length === 0 - ? t('process.spreadsheet.confirm.blank_vote') - : answers[0] - .map((a: string) => q.choices[Number(a)].title.default) - .map((a: string) => `- ${a}`) - .join('
'), - }} - /> + {t('process.spreadsheet.confirm.abstain_count', { + count: election.voteType.maxCount! - eAnswers[0].length, + })}
- )} -
- {i + 1 !== election.questions.length && } - - ))} -
- {canAbstain && answers[0].length < election.voteType.maxCount! && ( - - - - {t('process.spreadsheet.confirm.abstain_count', { - count: election.voteType.maxCount! - answers[0].length, - })} - - - )} + + )} + + ) + })}