From 5816b470609a2a270c69f7e40a22938e63991368 Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 8 Oct 2024 15:45:41 +0200 Subject: [PATCH 01/36] Refactor questions to accept multiple elections --- .../components/Election/Questions/Fields.tsx | 8 ++-- .../components/Election/Questions/Form.tsx | 14 +++--- .../Election/Questions/Questions.tsx | 45 ++++++++++++------- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Fields.tsx b/packages/chakra-components/src/components/Election/Questions/Fields.tsx index 946f436f..881cafbd 100644 --- a/packages/chakra-components/src/components/Election/Questions/Fields.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Fields.tsx @@ -22,9 +22,11 @@ export const QuestionField = ({ question, index }: QuestionFieldProps) => { formState: { errors }, } = useFormContext() + const [key, i] = index.split('.') + return ( - + {question.title.default} @@ -227,7 +229,7 @@ export const SingleChoice = ({ index, question }: QuestionProps) => { required: localize('validation.required'), }} name={index} - render={({ field }) => ( + render={({ field, fieldState: { error: fieldError } }) => ( {question.choices.map((choice, ck) => ( @@ -236,7 +238,7 @@ export const SingleChoice = ({ index, question }: QuestionProps) => { ))} - {errors[index]?.message as string} + {fieldError?.message as string} )} /> diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 4306fdff..36c8a31d 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -33,19 +33,21 @@ export const QuestionsFormProvider: React.FC { + const vote = async (values: Record) => { if (!election || !(election instanceof PublishedElection)) { console.warn('vote attempt with no valid election defined') return false } + const electionValues = values[election.id] + if ( client.wallet instanceof Wallet && !(await confirm( typeof confirmContents === 'function' ? ( - confirmContents(election, values) + confirmContents(election, electionValues) ) : ( - + ) )) ) { @@ -55,10 +57,10 @@ export const QuestionsFormProvider: React.FC parseInt(values[k.toString()], 10)) + results = election.questions.map((q, k) => parseInt(electionValues[k.toString()], 10)) break case ElectionResultsTypeNames.MULTIPLE_CHOICE: - results = Object.values(values) + results = Object.values(electionValues) .pop() .map((v: string) => parseInt(v, 10)) // map proper abstain ids @@ -71,7 +73,7 @@ export const QuestionsFormProvider: React.FC { - if (values[0].includes(k.toString())) { + if (electionValues[0].includes(k.toString())) { return 1 } else { return 0 diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index 6538f6b8..f76a00b2 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -4,7 +4,7 @@ import { useElection } from '@vocdoni/react-providers' import { IQuestion, PublishedElection } from '@vocdoni/sdk' import { FieldValues, SubmitErrorHandler } from 'react-hook-form' import { QuestionField } from './Fields' -import { QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' +import { QuestionsFormContextState, QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' import { QuestionsTypeBadge } from './TypeBadge' import { Voted } from './Voted' @@ -21,6 +21,22 @@ export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestio ) export const ElectionQuestionsForm = (props: ElectionQuestionsFormProps) => { + const methods = useQuestionsForm() + const { fmethods, vote } = methods + const { election } = useElection() + + return ( +
+ + + ) +} + +export const ElectionQuestion = ({ + fmethods, + vote, + ...props +}: QuestionsFormContextState & ElectionQuestionsFormProps) => { const { election, voted, @@ -28,7 +44,6 @@ export const ElectionQuestionsForm = (props: ElectionQuestionsFormProps) => { localize, isAbleToVote, } = useElection() - const { fmethods, vote } = useQuestionsForm() const styles = useMultiStyleConfig('ElectionQuestions') const questions: IQuestion[] | undefined = (election as PublishedElection)?.questions const { onInvalid, ...rest } = props @@ -51,20 +66,18 @@ export const ElectionQuestionsForm = (props: ElectionQuestionsFormProps) => { return ( -
- - - - {questions.map((question, qk) => ( - - ))} - {error && ( - - - {error} - - )} - + + + + {questions.map((question, qk) => ( + + ))} + {error && ( + + + {error} + + )}
) } From 20264f44945fa26e13fb96949587477d79c40e99 Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 8 Oct 2024 15:53:41 +0200 Subject: [PATCH 02/36] Permit change election form id Also fix a type --- .../components/Election/Questions/Form.tsx | 2 ++ .../Election/Questions/Questions.tsx | 26 +++++++++---------- .../src/components/Election/VoteButton.tsx | 8 +++--- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 36c8a31d..4a1562bb 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -6,6 +6,8 @@ import { FieldValues, FormProvider, useForm, UseFormReturn } from 'react-hook-fo import { useConfirm } from '../../layout' import { QuestionsConfirmation } from './Confirmation' +export const DefaultElectionFormId = 'election-questions' + export type QuestionsFormContextState = { fmethods: UseFormReturn vote: (values: FieldValues) => Promise diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index f76a00b2..d52b3428 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -4,12 +4,19 @@ import { useElection } from '@vocdoni/react-providers' import { IQuestion, PublishedElection } from '@vocdoni/sdk' import { FieldValues, SubmitErrorHandler } from 'react-hook-form' import { QuestionField } from './Fields' -import { QuestionsFormContextState, QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' +import { + DefaultElectionFormId, + QuestionsFormContextState, + QuestionsFormProvider, + QuestionsFormProviderProps, + useQuestionsForm, +} from './Form' import { QuestionsTypeBadge } from './TypeBadge' import { Voted } from './Voted' export type ElectionQuestionsFormProps = ChakraProps & { onInvalid?: SubmitErrorHandler + formId?: string } export type ElectionQuestionsProps = ElectionQuestionsFormProps & QuestionsFormProviderProps @@ -20,23 +27,17 @@ export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestio ) -export const ElectionQuestionsForm = (props: ElectionQuestionsFormProps) => { +export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQuestionsFormProps) => { const methods = useQuestionsForm() const { fmethods, vote } = methods - const { election } = useElection() - return ( -
- + + ) } -export const ElectionQuestion = ({ - fmethods, - vote, - ...props -}: QuestionsFormContextState & ElectionQuestionsFormProps) => { +export const ElectionQuestion = (props: ChakraProps) => { const { election, voted, @@ -46,7 +47,6 @@ export const ElectionQuestion = ({ } = useElection() const styles = useMultiStyleConfig('ElectionQuestions') const questions: IQuestion[] | undefined = (election as PublishedElection)?.questions - const { onInvalid, ...rest } = props if (!(election instanceof PublishedElection)) return null @@ -64,7 +64,7 @@ export const ElectionQuestion = ({ } return ( - + diff --git a/packages/chakra-components/src/components/Election/VoteButton.tsx b/packages/chakra-components/src/components/Election/VoteButton.tsx index 26127514..9b31b108 100644 --- a/packages/chakra-components/src/components/Election/VoteButton.tsx +++ b/packages/chakra-components/src/components/Election/VoteButton.tsx @@ -4,9 +4,11 @@ import { chakra, useMultiStyleConfig } from '@chakra-ui/system' import { Signer } from '@ethersproject/abstract-signer' import { useClient, useElection } from '@vocdoni/react-providers' import { ElectionStatus, InvalidElection, PublishedElection } from '@vocdoni/sdk' -import { useEffect, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { Button } from '../layout/Button' import { results } from './Results' +import { MultiElectionsContext } from './Questions/MultiElectionContext' +import { DefaultElectionFormId } from './Questions' export const VoteButton = (props: ButtonProps) => { const { connected } = useClient() @@ -36,8 +38,8 @@ export const VoteButton = (props: ButtonProps) => { const button: ButtonProps = { type: 'submit', + form: DefaultElectionFormId, ...props, - form: `election-questions-${election.id}`, isDisabled, isLoading: voting, children: voted && isAbleToVote ? localize('vote.button_update') : localize('vote.button'), @@ -98,7 +100,7 @@ export const VoteWeight = () => { })() }, [client, election]) - if (!weight || !election || !(election instanceof PublishedElection)) return + if (!weight || !election || !(election instanceof PublishedElection)) return <> return ( From 26958af095b3d70ca4f10ba7b1477dee62d7d9cb Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 9 Oct 2024 14:23:05 +0200 Subject: [PATCH 03/36] Extract getVotePackage function --- .../components/Election/Questions/Form.tsx | 71 ++++++++++--------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 4a1562bb..8fb9fd4c 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -41,52 +41,24 @@ export const QuestionsFormProvider: React.FC + ) )) ) { return false } - let results: number[] = [] - switch (election.resultsType.name) { - case ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION: - results = election.questions.map((q, k) => parseInt(electionValues[k.toString()], 10)) - break - case ElectionResultsTypeNames.MULTIPLE_CHOICE: - results = Object.values(electionValues) - .pop() - .map((v: string) => parseInt(v, 10)) - // map proper abstain ids - if (election.resultsType.properties.canAbstain && results.length < election.voteType.maxCount!) { - let abs = 0 - while (results.length < (election.voteType.maxCount || 1)) { - results.push(parseInt(election.resultsType.properties.abstainValues[abs++], 10)) - } - } - break - case ElectionResultsTypeNames.APPROVAL: - results = election.questions[0].choices.map((c, k) => { - if (electionValues[0].includes(k.toString())) { - return 1 - } else { - return 0 - } - }) - break - default: - throw new Error('Unknown or invalid election type') - } + const votePackage = getVotePackage(election, electionChoices) - return bvote(results) + return bvote(votePackage) } // reset form if account gets disconnected @@ -110,3 +82,36 @@ export const QuestionsFormProvider: React.FC ) } + +export const getVotePackage = (election: PublishedElection, choices: FieldValues) => { + let results: number[] = [] + switch (election.resultsType.name) { + case ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION: + results = election.questions.map((q, k) => parseInt(choices[k.toString()], 10)) + break + case ElectionResultsTypeNames.MULTIPLE_CHOICE: + results = Object.values(choices) + .pop() + .map((v: string) => parseInt(v, 10)) + // map proper abstain ids + if (election.resultsType.properties.canAbstain && results.length < election.voteType.maxCount!) { + let abs = 0 + while (results.length < (election.voteType.maxCount || 1)) { + results.push(parseInt(election.resultsType.properties.abstainValues[abs++], 10)) + } + } + break + case ElectionResultsTypeNames.APPROVAL: + results = election.questions[0].choices.map((c, k) => { + if (choices[0].includes(k.toString())) { + return 1 + } else { + return 0 + } + }) + break + default: + throw new Error('Unknown or invalid election type') + } + return results +} From 4920506da13499a47ee84150f2c0a22a1746ea00 Mon Sep 17 00:00:00 2001 From: selankon Date: Thu, 10 Oct 2024 12:58:38 +0200 Subject: [PATCH 04/36] Split VoteButton logic --- .../components/Election/Questions/Fields.tsx | 5 ++--- .../src/components/Election/VoteButton.tsx | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Fields.tsx b/packages/chakra-components/src/components/Election/Questions/Fields.tsx index 881cafbd..fb1a2ff9 100644 --- a/packages/chakra-components/src/components/Election/Questions/Fields.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Fields.tsx @@ -22,11 +22,10 @@ export const QuestionField = ({ question, index }: QuestionFieldProps) => { formState: { errors }, } = useFormContext() - const [key, i] = index.split('.') - + const [election, questionI] = index.split('.') return ( - + {question.title.default} diff --git a/packages/chakra-components/src/components/Election/VoteButton.tsx b/packages/chakra-components/src/components/Election/VoteButton.tsx index 9b31b108..1e3e54f2 100644 --- a/packages/chakra-components/src/components/Election/VoteButton.tsx +++ b/packages/chakra-components/src/components/Election/VoteButton.tsx @@ -2,15 +2,24 @@ import { ButtonProps } from '@chakra-ui/button' import { Text } from '@chakra-ui/layout' import { chakra, useMultiStyleConfig } from '@chakra-ui/system' import { Signer } from '@ethersproject/abstract-signer' -import { useClient, useElection } from '@vocdoni/react-providers' +import { ElectionState, useClient, useElection } from '@vocdoni/react-providers' import { ElectionStatus, InvalidElection, PublishedElection } from '@vocdoni/sdk' -import { useContext, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { Button } from '../layout/Button' import { results } from './Results' -import { MultiElectionsContext } from './Questions/MultiElectionContext' import { DefaultElectionFormId } from './Questions' export const VoteButton = (props: ButtonProps) => { + const election = useElection() + return +} + +export const VoteButtonLogic = ({ + electionState, + ...props +}: { + electionState: ElectionState +} & ButtonProps) => { const { connected } = useClient() const { client, @@ -23,7 +32,7 @@ export const VoteButton = (props: ButtonProps) => { localize, sik: { signature }, sikSignature, - } = useElection() + } = electionState const [loading, setLoading] = useState(false) if (!election || election instanceof InvalidElection) { From 47e25bf21590e8af715b88fd3dfd91d6d1c5217f Mon Sep 17 00:00:00 2001 From: selankon Date: Thu, 10 Oct 2024 13:01:43 +0200 Subject: [PATCH 05/36] Refactor name --- .../src/components/Election/Questions/Form.tsx | 4 ++-- .../src/components/Election/Questions/Questions.tsx | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 8fb9fd4c..761891f8 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -56,7 +56,7 @@ export const QuestionsFormProvider: React.FC { +export const getVoteBallot = (election: PublishedElection, choices: FieldValues) => { let results: number[] = [] switch (election.resultsType.name) { case ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION: diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index d52b3428..eec7489b 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -4,13 +4,7 @@ import { useElection } from '@vocdoni/react-providers' import { IQuestion, PublishedElection } from '@vocdoni/sdk' import { FieldValues, SubmitErrorHandler } from 'react-hook-form' import { QuestionField } from './Fields' -import { - DefaultElectionFormId, - QuestionsFormContextState, - QuestionsFormProvider, - QuestionsFormProviderProps, - useQuestionsForm, -} from './Form' +import { DefaultElectionFormId, QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' import { QuestionsTypeBadge } from './TypeBadge' import { Voted } from './Voted' From ba1772058bc6eb81ee8e6780e2d85f33412cb918 Mon Sep 17 00:00:00 2001 From: selankon Date: Thu, 10 Oct 2024 13:14:13 +0200 Subject: [PATCH 06/36] Fix isInvalid lint error --- .../src/components/Election/Questions/Fields.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Fields.tsx b/packages/chakra-components/src/components/Election/Questions/Fields.tsx index fb1a2ff9..cc49b4f0 100644 --- a/packages/chakra-components/src/components/Election/Questions/Fields.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Fields.tsx @@ -22,10 +22,15 @@ export const QuestionField = ({ question, index }: QuestionFieldProps) => { formState: { errors }, } = useFormContext() - const [election, questionI] = index.split('.') + const [election, qi] = index.split('.') + const questionIndex = Number(qi) + let isInvalid = false + if (errors[election] && Array.isArray(errors[election]) && errors[election][questionIndex]) { + isInvalid = !!errors[election][questionIndex] + } return ( - + {question.title.default} From 9c1ac44e41688bea7284e068607d92c0c26a6178 Mon Sep 17 00:00:00 2001 From: selankon Date: Mon, 14 Oct 2024 09:19:11 +0200 Subject: [PATCH 07/36] Add MultiElection files --- .gitignore | 1 + .../Questions/MultiElectionConfirmation.tsx | 87 ++++++++++++ .../Questions/MultiElectionContext.tsx | 126 +++++++++++++++++ .../Questions/MultiElectionQuestions.tsx | 133 ++++++++++++++++++ .../components/Election/Questions/index.tsx | 3 + 5 files changed, 350 insertions(+) create mode 100644 packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx create mode 100644 packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx create mode 100644 packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx diff --git a/.gitignore b/.gitignore index ad5e84eb..38a99496 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ vite.config.ts.timestamp-*.mjs .vscode .yarn .yarnrc* +.idea package.json.backup diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx new file mode 100644 index 00000000..aa374f92 --- /dev/null +++ b/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx @@ -0,0 +1,87 @@ +import { Button } from '@chakra-ui/button' +import { Box, Text } from '@chakra-ui/layout' +import { ModalBody, ModalCloseButton, ModalFooter, ModalHeader } from '@chakra-ui/modal' +import { chakra, omitThemingProps, useMultiStyleConfig } from '@chakra-ui/system' +import { useClient } from '@vocdoni/react-providers' +import { ElectionResultsTypeNames } from '@vocdoni/sdk' +import { FieldValues } from 'react-hook-form' +import { useConfirm } from '@vocdoni/chakra-components' +import { ElectionStateStorage } from './MultiElectionContext' + +export type MultiElectionConfirmationProps = { + answers: Record + elections: ElectionStateStorage +} + +export const MultiElectionConfirmation = ({ answers, elections, ...rest }: MultiElectionConfirmationProps) => { + const mstyles = useMultiStyleConfig('ConfirmModal') + const styles = useMultiStyleConfig('QuestionsConfirmation', rest) + const { cancel, proceed } = useConfirm() + const props = omitThemingProps(rest) + const { localize } = useClient() + return ( + <> + {localize('confirm.title')} + + + {localize('vote.confirm')} + {Object.values(elections).map(({ election, voted, isAbleToVote }) => { + if (voted) + return ( + + {election.title.default} + {localize('vote.already_voted')} + + ) + if (!isAbleToVote) + return ( + + {election.title.default} + {localize('vote.not_able_to_vote')} + + ) + return ( + + {election.questions.map((q, k) => { + if (election.resultsType.name === ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION) { + const choice = q.choices.find((v) => v.value === parseInt(answers[election.id][k.toString()], 10)) + return ( + + {q.title.default} + {choice?.title.default} + + ) + } + const choices = answers[election.id][0] + .map((a: string) => + q.choices[Number(a)] ? q.choices[Number(a)].title.default : localize('vote.abstain') + ) + .map((a: string) => ( + + - {a} +
+
+ )) + + return ( + + {q.title.default} + {choices} + + ) + })} +
+ ) + })} +
+ + + + + + ) +} diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx new file mode 100644 index 00000000..11f57b7f --- /dev/null +++ b/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx @@ -0,0 +1,126 @@ +import React, { createContext, FC, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from 'react' +import { FieldValues, FormProvider, useForm, UseFormReturn } from 'react-hook-form' +import { PublishedElection, VocdoniSDKClient } from '@vocdoni/sdk' +import { Wallet } from '@ethersproject/wallet' +import { useElection, ElectionState } from '@vocdoni/react-providers' +import { MultiElectionConfirmation } from './MultiElectionConfirmation' +import { useConfirm, getVoteBallot } from '@vocdoni/chakra-components' + +export type MultiElectionFormContextState = { + fmethods: UseFormReturn +} & ReturnType + +export const MultiElectionsContext = createContext(undefined) + +export type MultiElectionsProviderProps = { + renderWith: RenderWith[] + rootClient: VocdoniSDKClient + confirmContents?: (elections: ElectionStateStorage, answers: Record) => ReactNode +} + +export type RenderWith = { + id: string +} + +export const MultiElectionsProvider: FC> = ({ children, ...props }) => { + const fmethods = useForm() + + const multiElections = useMultiElectionsProvider({ fmethods, ...props }) + return ( + + + {children} + + + ) +} + +export type SubElectionState = { election: PublishedElection } & Pick +export type ElectionStateStorage = Record + +const useMultiElectionsProvider = ({ + fmethods, + renderWith, + rootClient, + confirmContents, +}: { fmethods: UseFormReturn } & MultiElectionsProviderProps) => { + const { confirm } = useConfirm() + const { client } = useElection() + // State to store on memory the loaded elections to pass it into confirm modal to show the info + const [electionsStates, setElectionsStates] = useState({}) + const [voting, setVoting] = useState(false) + + const addElection = (electionState: SubElectionState) => { + setElectionsStates((prev) => ({ + ...prev, + [(electionState.election as PublishedElection).id]: electionState, + })) + } + + const voteAll = async (values: Record) => { + if (!electionsStates || Object.keys(electionsStates).length === 0) { + console.warn('vote attempt with no valid elections not defined') + return false + } + + if ( + client.wallet instanceof Wallet && + !(await confirm( + typeof confirmContents === 'function' ? ( + confirmContents(electionsStates, values) + ) : ( + + ) + )) + ) { + return false + } + + setVoting(true) + + const votingList = Object.entries(electionsStates).map(([key, { election, vote }]) => { + if (!(election instanceof PublishedElection) || !values[election.id]) return Promise.resolve() + const votePackage = getVoteBallot(election, values[election.id]) + return vote(votePackage) + }) + return Promise.all(votingList).finally(() => setVoting(false)) + } + + // reset form if account gets disconnected + useEffect(() => { + if (typeof client.wallet !== 'undefined') return + + setElectionsStates({}) + fmethods.reset({ + ...Object.values(electionsStates).reduce((acc, { election }) => ({ ...acc, [election.id]: '' }), {}), + }) + }, [client, electionsStates, fmethods]) + + const voted = useMemo( + () => (electionsStates && Object.values(electionsStates).every(({ voted }) => voted) ? 'true' : null), + [electionsStates] + ) + const isAbleToVote = useMemo( + () => electionsStates && Object.values(electionsStates).some(({ isAbleToVote }) => isAbleToVote), + [electionsStates] + ) + + return { + voting, + voteAll, + renderWith, + rootClient, + elections: electionsStates, + addElection, + isAbleToVote, + voted, + } +} + +export const useMultiElections = () => { + const context = useContext(MultiElectionsContext) + if (!context) { + throw new Error('useMultiElections must be used within an MultiElectionsProvider') + } + return context +} diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx new file mode 100644 index 00000000..9d6c3dc4 --- /dev/null +++ b/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx @@ -0,0 +1,133 @@ +import { SubElectionState, useMultiElections } from './MultiElectionContext' +import { ElectionProvider, useElection } from '@vocdoni/react-providers' +import { ComponentType, useEffect, useMemo, useState } from 'react' +import { PublishedElection } from '@vocdoni/sdk' +import { ButtonProps } from '@chakra-ui/button' +import { + DefaultElectionFormId, + ElectionQuestion, + ElectionQuestionsFormProps, + VoteButtonLogic, +} from '@vocdoni/chakra-components' +import { Flex, FormControl, FormErrorMessage, useMultiStyleConfig } from '@chakra-ui/react' +import { FieldValues, useFormContext, ValidateResult } from 'react-hook-form' + +export type SubmitFormValidation = (values: Record) => ValidateResult | Promise + +export type MultiElectionQuestionsFormProps = { + ConnectButton?: ComponentType + validate?: SubmitFormValidation +} & ElectionQuestionsFormProps + +export const MultiElectionVoteButton = (props: ButtonProps) => { + const { isAbleToVote, voting, voted } = useMultiElections() + const election = useElection() // use Root election information + + return ( + + ) +} + +export const MultiElectionQuestionsForm = ({ + formId, + onInvalid, + ConnectButton, + validate, + ...props +}: MultiElectionQuestionsFormProps) => { + const styles = useMultiStyleConfig('ElectionQuestions') + const { voteAll, fmethods, renderWith } = useMultiElections() + const [globalError, setGlobalError] = useState('') + + const { handleSubmit, watch } = fmethods + const formData = watch() + + const _validate = () => { + if (validate) { + const error = validate(formData) + if (typeof error === 'string' || (typeof error === 'boolean' && !error)) { + setGlobalError(error.toString()) + return false + } + } + setGlobalError('') + return true + } + + const onSubmit = (values: Record) => { + if (validate && !_validate()) { + return + } + voteAll(values) + } + + return ( +
+ {renderWith.length > 0 && ( + + {renderWith.map(({ id }) => ( + + + + ))} + + )} + + {globalError} + +
+ ) +} + +const SubElectionQuestions = (props: Omit) => { + const { rootClient, addElection, elections } = useMultiElections() + const { election, setClient, vote, connected, clearClient, isAbleToVote, voted } = useElection() + + const subElectionState: SubElectionState | null = useMemo(() => { + if (!election || !(election instanceof PublishedElection)) return null + return { + vote, + election, + isAbleToVote, + voted, + } + }, [vote, election, isAbleToVote, voted]) + + // clear session of local context when login out + useEffect(() => { + if (connected) return + clearClient() + }, [connected]) + + // ensure the client is set to the root one + useEffect(() => { + setClient(rootClient) + }, [rootClient, election]) + + // Update election state cache + useEffect(() => { + if (!subElectionState || !subElectionState.election) return + const actualState = elections[subElectionState.election.id] + if (subElectionState.vote === actualState?.vote || subElectionState.isAbleToVote === actualState?.isAbleToVote) { + return + } + addElection(subElectionState) + }, [subElectionState, elections, election]) + + return +} + +/** + * Check all values responses have the same length + * Won't work for multiquestions elections. + */ +export const sameLengthValidator: SubmitFormValidation = (answers) => { + const [first, ...rest] = Object.values(answers) + if (!first) { + throw new Error('No fields found') + } + return rest.every((ballot) => ballot[0].length === first[0].length) +} diff --git a/packages/chakra-components/src/components/Election/Questions/index.tsx b/packages/chakra-components/src/components/Election/Questions/index.tsx index 20e0dabf..bc326b3e 100644 --- a/packages/chakra-components/src/components/Election/Questions/index.tsx +++ b/packages/chakra-components/src/components/Election/Questions/index.tsx @@ -5,3 +5,6 @@ export * from './Questions' export * from './Tip' export * from './TypeBadge' export * from './Voted' +export * from './MultiElectionQuestions' +export * from './MultiElectionContext' +export * from './MultiElectionConfirmation' From 473d2f3f7ddd556a232347f0dffd349e9cf0520a Mon Sep 17 00:00:00 2001 From: selankon Date: Mon, 14 Oct 2024 12:42:38 +0200 Subject: [PATCH 08/36] Simplify validation function --- .../Election/Questions/MultiElectionQuestions.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx index 9d6c3dc4..53f985d4 100644 --- a/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx @@ -45,21 +45,14 @@ export const MultiElectionQuestionsForm = ({ const { handleSubmit, watch } = fmethods const formData = watch() - const _validate = () => { + const onSubmit = (values: Record) => { if (validate) { const error = validate(formData) if (typeof error === 'string' || (typeof error === 'boolean' && !error)) { setGlobalError(error.toString()) - return false + return } - } - setGlobalError('') - return true - } - - const onSubmit = (values: Record) => { - if (validate && !_validate()) { - return + setGlobalError('') } voteAll(values) } From bd672278d9503232573455e64c79b007f95840af Mon Sep 17 00:00:00 2001 From: selankon Date: Mon, 14 Oct 2024 12:53:16 +0200 Subject: [PATCH 09/36] Use relative imports --- .../Questions/MultiElectionConfirmation.tsx | 2 +- .../Election/Questions/MultiElectionContext.tsx | 3 ++- .../Election/Questions/MultiElectionQuestions.tsx | 15 +++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx index aa374f92..51b5a80e 100644 --- a/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx +++ b/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx @@ -5,8 +5,8 @@ import { chakra, omitThemingProps, useMultiStyleConfig } from '@chakra-ui/system import { useClient } from '@vocdoni/react-providers' import { ElectionResultsTypeNames } from '@vocdoni/sdk' import { FieldValues } from 'react-hook-form' -import { useConfirm } from '@vocdoni/chakra-components' import { ElectionStateStorage } from './MultiElectionContext' +import { useConfirm } from '../../layout' export type MultiElectionConfirmationProps = { answers: Record diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx index 11f57b7f..97d9ec84 100644 --- a/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx +++ b/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx @@ -4,7 +4,8 @@ import { PublishedElection, VocdoniSDKClient } from '@vocdoni/sdk' import { Wallet } from '@ethersproject/wallet' import { useElection, ElectionState } from '@vocdoni/react-providers' import { MultiElectionConfirmation } from './MultiElectionConfirmation' -import { useConfirm, getVoteBallot } from '@vocdoni/chakra-components' +import { useConfirm } from '../../layout' +import { getVoteBallot } from './Form' export type MultiElectionFormContextState = { fmethods: UseFormReturn diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx index 53f985d4..934fddce 100644 --- a/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx @@ -3,14 +3,13 @@ import { ElectionProvider, useElection } from '@vocdoni/react-providers' import { ComponentType, useEffect, useMemo, useState } from 'react' import { PublishedElection } from '@vocdoni/sdk' import { ButtonProps } from '@chakra-ui/button' -import { - DefaultElectionFormId, - ElectionQuestion, - ElectionQuestionsFormProps, - VoteButtonLogic, -} from '@vocdoni/chakra-components' -import { Flex, FormControl, FormErrorMessage, useMultiStyleConfig } from '@chakra-ui/react' -import { FieldValues, useFormContext, ValidateResult } from 'react-hook-form' +import { useMultiStyleConfig } from '@chakra-ui/system' +import { Flex } from '@chakra-ui/layout' +import { FormControl, FormErrorMessage } from '@chakra-ui/form-control' +import { FieldValues, ValidateResult } from 'react-hook-form' +import { ElectionQuestion, ElectionQuestionsFormProps } from './Questions' +import { VoteButtonLogic } from '../VoteButton' +import { DefaultElectionFormId } from './Form' export type SubmitFormValidation = (values: Record) => ValidateResult | Promise From 321b048d4b542a15ce783e5ffaa031c2fe36d5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=92scar=20Casajuana?= Date: Mon, 14 Oct 2024 14:52:25 +0200 Subject: [PATCH 10/36] Added ffjavascript resolution to fix version clash issues in tests --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 25cde20c..78613ad7 100644 --- a/package.json +++ b/package.json @@ -44,5 +44,8 @@ "engines": { "npm": "please use yarn", "yarn": ">= 1.19.1 && < 2" + }, + "resolutions": { + "ffjavascript": "^0.3.1" } } From 8f4a8301eeb045ad7528397394db9543676550b7 Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 15 Oct 2024 09:12:20 +0200 Subject: [PATCH 11/36] Use null instead of empty fragment --- .../chakra-components/src/components/Election/VoteButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chakra-components/src/components/Election/VoteButton.tsx b/packages/chakra-components/src/components/Election/VoteButton.tsx index 1e3e54f2..ba7ed96f 100644 --- a/packages/chakra-components/src/components/Election/VoteButton.tsx +++ b/packages/chakra-components/src/components/Election/VoteButton.tsx @@ -109,7 +109,7 @@ export const VoteWeight = () => { })() }, [client, election]) - if (!weight || !election || !(election instanceof PublishedElection)) return <> + if (!weight || !election || !(election instanceof PublishedElection)) return null return ( From c745e91633a200997402f866fb33d7f3c998a199 Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 15 Oct 2024 14:24:30 +0200 Subject: [PATCH 12/36] Delete sameLengthValidator --- .../Election/Questions/MultiElectionQuestions.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx index 934fddce..51773a51 100644 --- a/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx @@ -111,15 +111,3 @@ const SubElectionQuestions = (props: Omit } - -/** - * Check all values responses have the same length - * Won't work for multiquestions elections. - */ -export const sameLengthValidator: SubmitFormValidation = (answers) => { - const [first, ...rest] = Object.values(answers) - if (!first) { - throw new Error('No fields found') - } - return rest.every((ballot) => ballot[0].length === first[0].length) -} From ef03ef060ab8fba576ad1acac1a83fe845932896 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 16 Oct 2024 12:56:12 +0200 Subject: [PATCH 13/36] Merge Multielection with the normal form --- .../components/Election/Questions/Form.tsx | 177 ++++++++++++------ .../Questions/MultiElectionConfirmation.tsx | 18 +- .../Questions/MultiElectionContext.tsx | 127 ------------- .../Questions/MultiElectionQuestions.tsx | 113 ----------- .../Election/Questions/Questions.tsx | 104 ++++++++-- .../components/Election/Questions/index.tsx | 2 - .../src/components/Election/VoteButton.tsx | 21 ++- 7 files changed, 245 insertions(+), 317 deletions(-) delete mode 100644 packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx delete mode 100644 packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 761891f8..79d81767 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -1,17 +1,18 @@ import { Wallet } from '@ethersproject/wallet' import { useElection } from '@vocdoni/react-providers' import { ElectionResultsTypeNames, PublishedElection } from '@vocdoni/sdk' -import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect } from 'react' +import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from 'react' import { FieldValues, FormProvider, useForm, UseFormReturn } from 'react-hook-form' import { useConfirm } from '../../layout' -import { QuestionsConfirmation } from './Confirmation' +import { MultiElectionConfirmation } from './MultiElectionConfirmation' +import { ElectionStateStorage, RenderWith, SubElectionState, SubmitFormValidation } from './Questions' export const DefaultElectionFormId = 'election-questions' export type QuestionsFormContextState = { fmethods: UseFormReturn - vote: (values: FieldValues) => Promise -} +} & SpecificFormProviderProps & + ReturnType const QuestionsFormContext = createContext(undefined) @@ -24,66 +25,33 @@ export const useQuestionsForm = () => { } export type QuestionsFormProviderProps = { - confirmContents?: (election: PublishedElection, answers: FieldValues) => ReactNode + confirmContents?: (elections: ElectionStateStorage, answers: Record) => ReactNode } -export const QuestionsFormProvider: React.FC> = ({ - confirmContents, - children, -}) => { - const fmethods = useForm() - const { confirm } = useConfirm() - const { election, client, vote: bvote } = useElection() - - const vote = async (values: Record) => { - if (!election || !(election instanceof PublishedElection)) { - console.warn('vote attempt with no valid election defined') - return false - } - - const electionChoices = values[election.id] - - if ( - client.wallet instanceof Wallet && - !(await confirm( - typeof confirmContents === 'function' ? ( - confirmContents(election, electionChoices) - ) : ( - - ) - )) - ) { - return false - } - - const votePackage = getVoteBallot(election, electionChoices) - - return bvote(votePackage) - } - - // reset form if account gets disconnected - useEffect(() => { - if ( - typeof client.wallet !== 'undefined' || - !election || - !(election instanceof PublishedElection) || - !election?.questions - ) - return +// Props that must not be shared with ElectionQuestionsProps +export type SpecificFormProviderProps = { + renderWith?: RenderWith[] + validate?: SubmitFormValidation +} - fmethods.reset({ - ...election.questions.reduce((acc, question, index) => ({ ...acc, [index]: '' }), {}), - }) - }, [client, election, fmethods]) +export const QuestionsFormProvider: React.FC< + PropsWithChildren +> = ({ children, ...props }) => { + const fmethods = useForm() + const multiElections = useMultiElectionsProvider({ fmethods, ...props }) return ( - {children} + + {children} + ) } -export const getVoteBallot = (election: PublishedElection, choices: FieldValues) => { +export const constructVoteBallot = (election: PublishedElection, choices: FieldValues) => { let results: number[] = [] switch (election.resultsType.name) { case ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION: @@ -115,3 +83,104 @@ export const getVoteBallot = (election: PublishedElection, choices: FieldValues) } return results } + +const useMultiElectionsProvider = ({ + fmethods, + confirmContents, +}: { fmethods: UseFormReturn } & QuestionsFormProviderProps) => { + const { confirm } = useConfirm() + const { client, isAbleToVote: rootIsAbleToVote, voted: rootVoted, election, vote } = useElection() // Root Election + // State to store on memory the loaded elections to pass it into confirm modal to show the info + const [electionsStates, setElectionsStates] = useState({}) + const [voting, setVoting] = useState(false) + + const voted = useMemo( + () => (electionsStates && Object.values(electionsStates).every(({ voted }) => voted) ? 'true' : null), + [electionsStates] + ) + + const isAbleToVote = useMemo( + () => electionsStates && Object.values(electionsStates).some(({ isAbleToVote }) => isAbleToVote), + [electionsStates] + ) + + // Add an election to the storage + const addElection = (electionState: SubElectionState) => { + setElectionsStates((prev) => ({ + ...prev, + [(electionState.election as PublishedElection).id]: electionState, + })) + } + + // Root election state to be added to the state storage + const rootElectionState: SubElectionState | null = useMemo(() => { + if (!election || !(election instanceof PublishedElection)) return null + return { + vote, + election, + isAbleToVote: rootIsAbleToVote, + voted: rootVoted, + } + }, [vote, election, rootIsAbleToVote, rootVoted]) + + // reset form if account gets disconnected + useEffect(() => { + if (typeof client.wallet !== 'undefined') return + + setElectionsStates({}) + fmethods.reset({ + ...Object.values(electionsStates).reduce((acc, { election }) => ({ ...acc, [election.id]: '' }), {}), + }) + }, [client, electionsStates, fmethods]) + + // Add the root election to the state to elections cache + useEffect(() => { + if (!rootElectionState || !rootElectionState.election) return + const actualState = electionsStates[rootElectionState.election.id] + if (rootElectionState.vote === actualState?.vote || rootElectionState.isAbleToVote === actualState?.isAbleToVote) { + return + } + addElection(rootElectionState) + }, [rootElectionState, electionsStates, election]) + + const voteAll = async (values: Record) => { + if (!electionsStates || Object.keys(electionsStates).length === 0) { + console.warn('vote attempt with no valid elections not defined') + return false + } + + if ( + client.wallet instanceof Wallet && + !(await confirm( + typeof confirmContents === 'function' ? ( + confirmContents(electionsStates, values) + ) : ( + + ) + )) + ) { + return false + } + + setVoting(true) + + const votingList = Object.entries(electionsStates).map(([key, { election, vote, voted, isAbleToVote }]) => { + if (!(election instanceof PublishedElection) || !values[election.id] || !isAbleToVote) { + return Promise.resolve() + } + const votePackage = constructVoteBallot(election, values[election.id]) + return vote(votePackage) + }) + return Promise.all(votingList).finally(() => setVoting(false)) + } + + return { + voting, + voteAll, + rootClient: client, + elections: electionsStates, + addElection, + isAbleToVote, + voted, + } +} diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx index 51b5a80e..ae77da57 100644 --- a/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx +++ b/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx @@ -5,14 +5,15 @@ import { chakra, omitThemingProps, useMultiStyleConfig } from '@chakra-ui/system import { useClient } from '@vocdoni/react-providers' import { ElectionResultsTypeNames } from '@vocdoni/sdk' import { FieldValues } from 'react-hook-form' -import { ElectionStateStorage } from './MultiElectionContext' import { useConfirm } from '../../layout' +import { ElectionStateStorage } from './Questions' export type MultiElectionConfirmationProps = { answers: Record elections: ElectionStateStorage } +// todo(kon): refactor this to merge it with the current Confirmation modal export const MultiElectionConfirmation = ({ answers, elections, ...rest }: MultiElectionConfirmationProps) => { const mstyles = useMultiStyleConfig('ConfirmModal') const styles = useMultiStyleConfig('QuestionsConfirmation', rest) @@ -26,13 +27,13 @@ export const MultiElectionConfirmation = ({ answers, elections, ...rest }: Multi {localize('vote.confirm')} {Object.values(elections).map(({ election, voted, isAbleToVote }) => { - if (voted) - return ( - - {election.title.default} - {localize('vote.already_voted')} - - ) + // if (voted) + // return ( + // + // {election.title.default} + // {localize('vote.already_voted')} + // + // ) if (!isAbleToVote) return ( @@ -42,6 +43,7 @@ export const MultiElectionConfirmation = ({ answers, elections, ...rest }: Multi ) return ( + {/*todo(kon): refactor to add election title and if already voted but can overwrite*/} {election.questions.map((q, k) => { if (election.resultsType.name === ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION) { const choice = q.choices.find((v) => v.value === parseInt(answers[election.id][k.toString()], 10)) diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx deleted file mode 100644 index 97d9ec84..00000000 --- a/packages/chakra-components/src/components/Election/Questions/MultiElectionContext.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { createContext, FC, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from 'react' -import { FieldValues, FormProvider, useForm, UseFormReturn } from 'react-hook-form' -import { PublishedElection, VocdoniSDKClient } from '@vocdoni/sdk' -import { Wallet } from '@ethersproject/wallet' -import { useElection, ElectionState } from '@vocdoni/react-providers' -import { MultiElectionConfirmation } from './MultiElectionConfirmation' -import { useConfirm } from '../../layout' -import { getVoteBallot } from './Form' - -export type MultiElectionFormContextState = { - fmethods: UseFormReturn -} & ReturnType - -export const MultiElectionsContext = createContext(undefined) - -export type MultiElectionsProviderProps = { - renderWith: RenderWith[] - rootClient: VocdoniSDKClient - confirmContents?: (elections: ElectionStateStorage, answers: Record) => ReactNode -} - -export type RenderWith = { - id: string -} - -export const MultiElectionsProvider: FC> = ({ children, ...props }) => { - const fmethods = useForm() - - const multiElections = useMultiElectionsProvider({ fmethods, ...props }) - return ( - - - {children} - - - ) -} - -export type SubElectionState = { election: PublishedElection } & Pick -export type ElectionStateStorage = Record - -const useMultiElectionsProvider = ({ - fmethods, - renderWith, - rootClient, - confirmContents, -}: { fmethods: UseFormReturn } & MultiElectionsProviderProps) => { - const { confirm } = useConfirm() - const { client } = useElection() - // State to store on memory the loaded elections to pass it into confirm modal to show the info - const [electionsStates, setElectionsStates] = useState({}) - const [voting, setVoting] = useState(false) - - const addElection = (electionState: SubElectionState) => { - setElectionsStates((prev) => ({ - ...prev, - [(electionState.election as PublishedElection).id]: electionState, - })) - } - - const voteAll = async (values: Record) => { - if (!electionsStates || Object.keys(electionsStates).length === 0) { - console.warn('vote attempt with no valid elections not defined') - return false - } - - if ( - client.wallet instanceof Wallet && - !(await confirm( - typeof confirmContents === 'function' ? ( - confirmContents(electionsStates, values) - ) : ( - - ) - )) - ) { - return false - } - - setVoting(true) - - const votingList = Object.entries(electionsStates).map(([key, { election, vote }]) => { - if (!(election instanceof PublishedElection) || !values[election.id]) return Promise.resolve() - const votePackage = getVoteBallot(election, values[election.id]) - return vote(votePackage) - }) - return Promise.all(votingList).finally(() => setVoting(false)) - } - - // reset form if account gets disconnected - useEffect(() => { - if (typeof client.wallet !== 'undefined') return - - setElectionsStates({}) - fmethods.reset({ - ...Object.values(electionsStates).reduce((acc, { election }) => ({ ...acc, [election.id]: '' }), {}), - }) - }, [client, electionsStates, fmethods]) - - const voted = useMemo( - () => (electionsStates && Object.values(electionsStates).every(({ voted }) => voted) ? 'true' : null), - [electionsStates] - ) - const isAbleToVote = useMemo( - () => electionsStates && Object.values(electionsStates).some(({ isAbleToVote }) => isAbleToVote), - [electionsStates] - ) - - return { - voting, - voteAll, - renderWith, - rootClient, - elections: electionsStates, - addElection, - isAbleToVote, - voted, - } -} - -export const useMultiElections = () => { - const context = useContext(MultiElectionsContext) - if (!context) { - throw new Error('useMultiElections must be used within an MultiElectionsProvider') - } - return context -} diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx deleted file mode 100644 index 51773a51..00000000 --- a/packages/chakra-components/src/components/Election/Questions/MultiElectionQuestions.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { SubElectionState, useMultiElections } from './MultiElectionContext' -import { ElectionProvider, useElection } from '@vocdoni/react-providers' -import { ComponentType, useEffect, useMemo, useState } from 'react' -import { PublishedElection } from '@vocdoni/sdk' -import { ButtonProps } from '@chakra-ui/button' -import { useMultiStyleConfig } from '@chakra-ui/system' -import { Flex } from '@chakra-ui/layout' -import { FormControl, FormErrorMessage } from '@chakra-ui/form-control' -import { FieldValues, ValidateResult } from 'react-hook-form' -import { ElectionQuestion, ElectionQuestionsFormProps } from './Questions' -import { VoteButtonLogic } from '../VoteButton' -import { DefaultElectionFormId } from './Form' - -export type SubmitFormValidation = (values: Record) => ValidateResult | Promise - -export type MultiElectionQuestionsFormProps = { - ConnectButton?: ComponentType - validate?: SubmitFormValidation -} & ElectionQuestionsFormProps - -export const MultiElectionVoteButton = (props: ButtonProps) => { - const { isAbleToVote, voting, voted } = useMultiElections() - const election = useElection() // use Root election information - - return ( - - ) -} - -export const MultiElectionQuestionsForm = ({ - formId, - onInvalid, - ConnectButton, - validate, - ...props -}: MultiElectionQuestionsFormProps) => { - const styles = useMultiStyleConfig('ElectionQuestions') - const { voteAll, fmethods, renderWith } = useMultiElections() - const [globalError, setGlobalError] = useState('') - - const { handleSubmit, watch } = fmethods - const formData = watch() - - const onSubmit = (values: Record) => { - if (validate) { - const error = validate(formData) - if (typeof error === 'string' || (typeof error === 'boolean' && !error)) { - setGlobalError(error.toString()) - return - } - setGlobalError('') - } - voteAll(values) - } - - return ( -
- {renderWith.length > 0 && ( - - {renderWith.map(({ id }) => ( - - - - ))} - - )} - - {globalError} - -
- ) -} - -const SubElectionQuestions = (props: Omit) => { - const { rootClient, addElection, elections } = useMultiElections() - const { election, setClient, vote, connected, clearClient, isAbleToVote, voted } = useElection() - - const subElectionState: SubElectionState | null = useMemo(() => { - if (!election || !(election instanceof PublishedElection)) return null - return { - vote, - election, - isAbleToVote, - voted, - } - }, [vote, election, isAbleToVote, voted]) - - // clear session of local context when login out - useEffect(() => { - if (connected) return - clearClient() - }, [connected]) - - // ensure the client is set to the root one - useEffect(() => { - setClient(rootClient) - }, [rootClient, election]) - - // Update election state cache - useEffect(() => { - if (!subElectionState || !subElectionState.election) return - const actualState = elections[subElectionState.election.id] - if (subElectionState.vote === actualState?.vote || subElectionState.isAbleToVote === actualState?.isAbleToVote) { - return - } - addElection(subElectionState) - }, [subElectionState, elections, election]) - - return -} diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index eec7489b..d01f87ee 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -1,12 +1,21 @@ import { Alert, AlertIcon } from '@chakra-ui/alert' import { chakra, ChakraProps, useMultiStyleConfig } from '@chakra-ui/system' -import { useElection } from '@vocdoni/react-providers' +import { ElectionProvider, ElectionState, useElection } from '@vocdoni/react-providers' import { IQuestion, PublishedElection } from '@vocdoni/sdk' -import { FieldValues, SubmitErrorHandler } from 'react-hook-form' +import { FieldValues, SubmitErrorHandler, ValidateResult } from 'react-hook-form' import { QuestionField } from './Fields' import { DefaultElectionFormId, QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' import { QuestionsTypeBadge } from './TypeBadge' import { Voted } from './Voted' +import { FormControl, FormErrorMessage } from '@chakra-ui/form-control' +import { useEffect, useMemo, useState } from 'react' +import { Flex } from '@chakra-ui/layout' + +export type RenderWith = { + id: string +} + +export type SubmitFormValidation = (values: Record) => ValidateResult | Promise export type ElectionQuestionsFormProps = ChakraProps & { onInvalid?: SubmitErrorHandler @@ -15,18 +24,50 @@ export type ElectionQuestionsFormProps = ChakraProps & { export type ElectionQuestionsProps = ElectionQuestionsFormProps & QuestionsFormProviderProps -export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestionsProps) => ( - - - -) +export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestionsProps) => { + return ( + + + + ) +} export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQuestionsFormProps) => { - const methods = useQuestionsForm() - const { fmethods, vote } = methods + const styles = useMultiStyleConfig('ElectionQuestions') + const { fmethods, voteAll, validate, renderWith } = useQuestionsForm() + const { ConnectButton } = useElection() // use Root election information + const [globalError, setGlobalError] = useState('') + + const { handleSubmit, watch } = fmethods + const formData = watch() + + const onSubmit = (values: Record) => { + if (validate) { + const error = validate(formData) + if (typeof error === 'string' || (typeof error === 'boolean' && !error)) { + setGlobalError(error.toString()) + return + } + setGlobalError('') + } + voteAll(values) + } + return ( -
- + + + {renderWith?.length > 0 && ( + + {renderWith.map(({ id }) => ( + + + + ))} + + )} + + {globalError} + ) } @@ -75,3 +116,44 @@ export const ElectionQuestion = (props: ChakraProps) => {
) } + +export type SubElectionState = { election: PublishedElection } & Pick +export type ElectionStateStorage = Record + +const SubElectionQuestions = (props: ChakraProps) => { + const { rootClient, addElection, elections } = useQuestionsForm() + const { election, setClient, vote, connected, clearClient, isAbleToVote, voted } = useElection() + + const subElectionState: SubElectionState | null = useMemo(() => { + if (!election || !(election instanceof PublishedElection)) return null + return { + vote, + election, + isAbleToVote, + voted, + } + }, [vote, election, isAbleToVote, voted]) + + // clear session of local context when login out + useEffect(() => { + if (connected) return + clearClient() + }, [connected]) + + // ensure the client is set to the root one + useEffect(() => { + setClient(rootClient) + }, [rootClient, election]) + + // Add the election to the state cache + useEffect(() => { + if (!subElectionState || !subElectionState.election) return + const actualState = elections[subElectionState.election.id] + if (subElectionState.vote === actualState?.vote || subElectionState.isAbleToVote === actualState?.isAbleToVote) { + return + } + addElection(subElectionState) + }, [subElectionState, elections, election]) + + return +} diff --git a/packages/chakra-components/src/components/Election/Questions/index.tsx b/packages/chakra-components/src/components/Election/Questions/index.tsx index bc326b3e..2c1cccbc 100644 --- a/packages/chakra-components/src/components/Election/Questions/index.tsx +++ b/packages/chakra-components/src/components/Election/Questions/index.tsx @@ -5,6 +5,4 @@ export * from './Questions' export * from './Tip' export * from './TypeBadge' export * from './Voted' -export * from './MultiElectionQuestions' -export * from './MultiElectionContext' export * from './MultiElectionConfirmation' diff --git a/packages/chakra-components/src/components/Election/VoteButton.tsx b/packages/chakra-components/src/components/Election/VoteButton.tsx index ba7ed96f..04d37d7a 100644 --- a/packages/chakra-components/src/components/Election/VoteButton.tsx +++ b/packages/chakra-components/src/components/Election/VoteButton.tsx @@ -7,11 +7,28 @@ import { ElectionStatus, InvalidElection, PublishedElection } from '@vocdoni/sdk import { useEffect, useState } from 'react' import { Button } from '../layout/Button' import { results } from './Results' -import { DefaultElectionFormId } from './Questions' +import { DefaultElectionFormId, useQuestionsForm } from './Questions' export const VoteButton = (props: ButtonProps) => { const election = useElection() - return + try { + const questionForm = useQuestionsForm() + return + } catch (e) { + return + } +} + +export const MultiElectionVoteButton = (props: ButtonProps) => { + const { isAbleToVote, voting, voted } = useQuestionsForm() + const election = useElection() // use Root election information + + return ( + + ) } export const VoteButtonLogic = ({ From 084cbb932d34954e12d1401c00faf171ef76cee6 Mon Sep 17 00:00:00 2001 From: selankon Date: Fri, 18 Oct 2024 12:01:25 +0200 Subject: [PATCH 14/36] Deprecate multielection confirmation --- .../Election/Questions/Confirmation.tsx | 69 ++++++++------ .../components/Election/Questions/Form.tsx | 4 +- .../Questions/MultiElectionConfirmation.tsx | 89 ------------------- .../components/Election/Questions/index.tsx | 1 - 4 files changed, 43 insertions(+), 120 deletions(-) delete mode 100644 packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx diff --git a/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx b/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx index dc79b417..8c0b0ec3 100644 --- a/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx @@ -3,55 +3,68 @@ import { Box, Text } from '@chakra-ui/layout' import { ModalBody, ModalCloseButton, ModalFooter, ModalHeader } from '@chakra-ui/modal' import { chakra, omitThemingProps, useMultiStyleConfig } from '@chakra-ui/system' import { useClient } from '@vocdoni/react-providers' -import { ElectionResultsTypeNames, PublishedElection } from '@vocdoni/sdk' +import { ElectionResultsTypeNames } from '@vocdoni/sdk' import { FieldValues } from 'react-hook-form' import { useConfirm } from '../../layout' +import { ElectionStateStorage } from './Questions' export type QuestionsConfirmationProps = { - answers: FieldValues - election: PublishedElection + answers: Record + elections: ElectionStateStorage } -export const QuestionsConfirmation = ({ answers, election, ...rest }: QuestionsConfirmationProps) => { +export const QuestionsConfirmation = ({ answers, elections, ...rest }: QuestionsConfirmationProps) => { const mstyles = useMultiStyleConfig('ConfirmModal') const styles = useMultiStyleConfig('QuestionsConfirmation', rest) const { cancel, proceed } = useConfirm() const props = omitThemingProps(rest) const { localize } = useClient() - return ( <> {localize('confirm.title')} + {localize('vote.confirm')} - {localize('vote.confirm')} - {election.questions.map((q, k) => { - if (election.resultsType.name === ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION) { - const choice = q.choices.find((v) => v.value === parseInt(answers[k.toString()], 10)) + {Object.values(elections).map(({ election, voted, isAbleToVote }) => { + if (!isAbleToVote) return ( - - {q.title.default} - {choice?.title.default} + + {election.title.default} + {localize('vote.not_able_to_vote')} ) - } - const choices = answers[0] - .map((a: string) => - q.choices[Number(a)] ? q.choices[Number(a)].title.default : localize('vote.abstain') - ) - .map((a: string) => ( - - - {a} -
-
- )) - return ( - - {q.title.default} - {choices} - + <> + {election.questions.map((q, k) => { + if (election.resultsType.name === ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION) { + const choice = q.choices.find((v) => v.value === parseInt(answers[election.id][k.toString()], 10)) + return ( + + {q.title.default} + {choice?.title.default} + + ) + } + const choices = answers[election.id][0] + .map((a: string) => + q.choices[Number(a)] ? q.choices[Number(a)].title.default : localize('vote.abstain') + ) + .map((a: string) => ( + + - {a} +
+
+ )) + + return ( + + {q.title.default} + {choices} + + ) + })} + ) })}
diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 79d81767..ae161607 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -4,7 +4,7 @@ import { ElectionResultsTypeNames, PublishedElection } from '@vocdoni/sdk' import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from 'react' import { FieldValues, FormProvider, useForm, UseFormReturn } from 'react-hook-form' import { useConfirm } from '../../layout' -import { MultiElectionConfirmation } from './MultiElectionConfirmation' +import { QuestionsConfirmation } from './Confirmation' import { ElectionStateStorage, RenderWith, SubElectionState, SubmitFormValidation } from './Questions' export const DefaultElectionFormId = 'election-questions' @@ -155,7 +155,7 @@ const useMultiElectionsProvider = ({ typeof confirmContents === 'function' ? ( confirmContents(electionsStates, values) ) : ( - + ) )) ) { diff --git a/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx b/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx deleted file mode 100644 index ae77da57..00000000 --- a/packages/chakra-components/src/components/Election/Questions/MultiElectionConfirmation.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Button } from '@chakra-ui/button' -import { Box, Text } from '@chakra-ui/layout' -import { ModalBody, ModalCloseButton, ModalFooter, ModalHeader } from '@chakra-ui/modal' -import { chakra, omitThemingProps, useMultiStyleConfig } from '@chakra-ui/system' -import { useClient } from '@vocdoni/react-providers' -import { ElectionResultsTypeNames } from '@vocdoni/sdk' -import { FieldValues } from 'react-hook-form' -import { useConfirm } from '../../layout' -import { ElectionStateStorage } from './Questions' - -export type MultiElectionConfirmationProps = { - answers: Record - elections: ElectionStateStorage -} - -// todo(kon): refactor this to merge it with the current Confirmation modal -export const MultiElectionConfirmation = ({ answers, elections, ...rest }: MultiElectionConfirmationProps) => { - const mstyles = useMultiStyleConfig('ConfirmModal') - const styles = useMultiStyleConfig('QuestionsConfirmation', rest) - const { cancel, proceed } = useConfirm() - const props = omitThemingProps(rest) - const { localize } = useClient() - return ( - <> - {localize('confirm.title')} - - - {localize('vote.confirm')} - {Object.values(elections).map(({ election, voted, isAbleToVote }) => { - // if (voted) - // return ( - // - // {election.title.default} - // {localize('vote.already_voted')} - // - // ) - if (!isAbleToVote) - return ( - - {election.title.default} - {localize('vote.not_able_to_vote')} - - ) - return ( - - {/*todo(kon): refactor to add election title and if already voted but can overwrite*/} - {election.questions.map((q, k) => { - if (election.resultsType.name === ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION) { - const choice = q.choices.find((v) => v.value === parseInt(answers[election.id][k.toString()], 10)) - return ( - - {q.title.default} - {choice?.title.default} - - ) - } - const choices = answers[election.id][0] - .map((a: string) => - q.choices[Number(a)] ? q.choices[Number(a)].title.default : localize('vote.abstain') - ) - .map((a: string) => ( - - - {a} -
-
- )) - - return ( - - {q.title.default} - {choices} - - ) - })} -
- ) - })} -
- - - - - - ) -} diff --git a/packages/chakra-components/src/components/Election/Questions/index.tsx b/packages/chakra-components/src/components/Election/Questions/index.tsx index 2c1cccbc..20e0dabf 100644 --- a/packages/chakra-components/src/components/Election/Questions/index.tsx +++ b/packages/chakra-components/src/components/Election/Questions/index.tsx @@ -5,4 +5,3 @@ export * from './Questions' export * from './Tip' export * from './TypeBadge' export * from './Voted' -export * from './MultiElectionConfirmation' From 4fd42c6117bd86cfb07b308b479c599dbc1569d5 Mon Sep 17 00:00:00 2001 From: selankon Date: Fri, 18 Oct 2024 12:33:56 +0200 Subject: [PATCH 15/36] Use election id as form id --- .../src/components/Election/Questions/Form.tsx | 2 -- .../src/components/Election/Questions/Questions.tsx | 8 +++++--- .../src/components/Election/VoteButton.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index ae161607..8833ee2d 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -7,8 +7,6 @@ import { useConfirm } from '../../layout' import { QuestionsConfirmation } from './Confirmation' import { ElectionStateStorage, RenderWith, SubElectionState, SubmitFormValidation } from './Questions' -export const DefaultElectionFormId = 'election-questions' - export type QuestionsFormContextState = { fmethods: UseFormReturn } & SpecificFormProviderProps & diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index d01f87ee..b6246dfc 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -4,7 +4,7 @@ import { ElectionProvider, ElectionState, useElection } from '@vocdoni/react-pro import { IQuestion, PublishedElection } from '@vocdoni/sdk' import { FieldValues, SubmitErrorHandler, ValidateResult } from 'react-hook-form' import { QuestionField } from './Fields' -import { DefaultElectionFormId, QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' +import { QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' import { QuestionsTypeBadge } from './TypeBadge' import { Voted } from './Voted' import { FormControl, FormErrorMessage } from '@chakra-ui/form-control' @@ -35,7 +35,7 @@ export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestio export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQuestionsFormProps) => { const styles = useMultiStyleConfig('ElectionQuestions') const { fmethods, voteAll, validate, renderWith } = useQuestionsForm() - const { ConnectButton } = useElection() // use Root election information + const { ConnectButton, election } = useElection() // use Root election information const [globalError, setGlobalError] = useState('') const { handleSubmit, watch } = fmethods @@ -53,8 +53,10 @@ export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQu voteAll(values) } + if (!(election instanceof PublishedElection)) return null + return ( -
+ {renderWith?.length > 0 && ( diff --git a/packages/chakra-components/src/components/Election/VoteButton.tsx b/packages/chakra-components/src/components/Election/VoteButton.tsx index 04d37d7a..85e1a665 100644 --- a/packages/chakra-components/src/components/Election/VoteButton.tsx +++ b/packages/chakra-components/src/components/Election/VoteButton.tsx @@ -7,7 +7,7 @@ import { ElectionStatus, InvalidElection, PublishedElection } from '@vocdoni/sdk import { useEffect, useState } from 'react' import { Button } from '../layout/Button' import { results } from './Results' -import { DefaultElectionFormId, useQuestionsForm } from './Questions' +import { useQuestionsForm } from './Questions' export const VoteButton = (props: ButtonProps) => { const election = useElection() @@ -64,7 +64,7 @@ export const VoteButtonLogic = ({ const button: ButtonProps = { type: 'submit', - form: DefaultElectionFormId, + form: `election-questions-${election.id}`, ...props, isDisabled, isLoading: voting, From d6e3168682e4149eaa7e535855fd97d10f64b8fc Mon Sep 17 00:00:00 2001 From: selankon Date: Fri, 18 Oct 2024 12:35:04 +0200 Subject: [PATCH 16/36] Fix warn message --- .../src/components/Election/Questions/Form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 8833ee2d..ad46bff0 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -143,7 +143,7 @@ const useMultiElectionsProvider = ({ const voteAll = async (values: Record) => { if (!electionsStates || Object.keys(electionsStates).length === 0) { - console.warn('vote attempt with no valid elections not defined') + console.warn('vote attempt with no valid elections defined') return false } From 41fb9b5e758b64133dce003f9d25d377bf8c8b0a Mon Sep 17 00:00:00 2001 From: selankon Date: Fri, 18 Oct 2024 12:56:09 +0200 Subject: [PATCH 17/36] Fix unused variable --- .../src/components/Election/Questions/Confirmation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx b/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx index 8c0b0ec3..2a59cb51 100644 --- a/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx @@ -26,7 +26,7 @@ export const QuestionsConfirmation = ({ answers, elections, ...rest }: Questions {localize('vote.confirm')} - {Object.values(elections).map(({ election, voted, isAbleToVote }) => { + {Object.values(elections).map(({ election, isAbleToVote }) => { if (!isAbleToVote) return ( From a3a9997ee4762e14ae83f85ef976ff19c547c315 Mon Sep 17 00:00:00 2001 From: selankon Date: Mon, 21 Oct 2024 12:04:30 +0200 Subject: [PATCH 18/36] Export component --- .../src/components/Election/Questions/Questions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index b6246dfc..0df10f91 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -122,7 +122,7 @@ export const ElectionQuestion = (props: ChakraProps) => { export type SubElectionState = { election: PublishedElection } & Pick export type ElectionStateStorage = Record -const SubElectionQuestions = (props: ChakraProps) => { +export const SubElectionQuestions = (props: ChakraProps) => { const { rootClient, addElection, elections } = useQuestionsForm() const { election, setClient, vote, connected, clearClient, isAbleToVote, voted } = useElection() From 8691569f3fb0a2e9752ee481f79d11b2fa4b222b Mon Sep 17 00:00:00 2001 From: selankon Date: Mon, 21 Oct 2024 12:40:04 +0200 Subject: [PATCH 19/36] Fix multielection layout --- packages/chakra-components/package.json | 2 +- .../src/components/Election/Questions/Questions.tsx | 11 +++++------ packages/chakra-components/src/theme/questions.ts | 2 ++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/chakra-components/package.json b/packages/chakra-components/package.json index cb49bb8a..32404e75 100644 --- a/packages/chakra-components/package.json +++ b/packages/chakra-components/package.json @@ -1,6 +1,6 @@ { "name": "@vocdoni/chakra-components", - "version": "0.9.6", + "version": "0.9.6-2", "license": "GPL-3.0-or-later", "homepage": "https://github.com/vocdoni/ui-components/tree/main/packages/chakra-components#readme", "bugs": "https://github.com/vocdoni/ui-components/issues", diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index 0df10f91..1d589667 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -57,16 +57,15 @@ export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQu return ( - - {renderWith?.length > 0 && ( - - {renderWith.map(({ id }) => ( + + + {renderWith?.length > 0 && + renderWith.map(({ id }) => ( ))} - - )} + {globalError} diff --git a/packages/chakra-components/src/theme/questions.ts b/packages/chakra-components/src/theme/questions.ts index 3bbc6104..7c3d09f5 100644 --- a/packages/chakra-components/src/theme/questions.ts +++ b/packages/chakra-components/src/theme/questions.ts @@ -9,6 +9,8 @@ export const questionsAnatomy = [ 'alertTitle', 'alertDescription', 'alertLink', + // elections wrapper for multielections + 'elections', // question wrapper 'question', // question header From ec21eae117b03f5fb04a232c5aed28c7f8649f91 Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 22 Oct 2024 10:07:55 +0200 Subject: [PATCH 20/36] Implement MultipleElectionVoted --- .../components/Election/Questions/Form.tsx | 13 ++-- .../Election/Questions/Questions.tsx | 8 +-- .../components/Election/Questions/Voted.tsx | 72 ++++++++++++++++--- .../chakra-components/src/i18n/locales.ts | 1 + .../chakra-components/src/theme/questions.ts | 1 + 5 files changed, 77 insertions(+), 18 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index ad46bff0..66a11562 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -92,14 +92,17 @@ const useMultiElectionsProvider = ({ const [electionsStates, setElectionsStates] = useState({}) const [voting, setVoting] = useState(false) + const electionsEmpty = Object.values(electionsStates).length === 0 + const voted = useMemo( - () => (electionsStates && Object.values(electionsStates).every(({ voted }) => voted) ? 'true' : null), - [electionsStates] + () => + electionsStates && !electionsEmpty && Object.values(electionsStates).every(({ voted }) => voted) ? 'true' : null, + [electionsStates, electionsEmpty] ) const isAbleToVote = useMemo( - () => electionsStates && Object.values(electionsStates).some(({ isAbleToVote }) => isAbleToVote), - [electionsStates] + () => electionsStates && !electionsEmpty && Object.values(electionsStates).some(({ isAbleToVote }) => isAbleToVote), + [electionsStates, electionsEmpty] ) // Add an election to the storage @@ -162,7 +165,7 @@ const useMultiElectionsProvider = ({ setVoting(true) - const votingList = Object.entries(electionsStates).map(([key, { election, vote, voted, isAbleToVote }]) => { + const votingList = Object.entries(electionsStates).map(([key, { election, vote, isAbleToVote }]) => { if (!(election instanceof PublishedElection) || !values[election.id] || !isAbleToVote) { return Promise.resolve() } diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index 1d589667..63830782 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -6,10 +6,9 @@ import { FieldValues, SubmitErrorHandler, ValidateResult } from 'react-hook-form import { QuestionField } from './Fields' import { QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' import { QuestionsTypeBadge } from './TypeBadge' -import { Voted } from './Voted' +import { MultiElectionVoted, Voted } from './Voted' import { FormControl, FormErrorMessage } from '@chakra-ui/form-control' import { useEffect, useMemo, useState } from 'react' -import { Flex } from '@chakra-ui/layout' export type RenderWith = { id: string @@ -34,7 +33,7 @@ export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestio export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQuestionsFormProps) => { const styles = useMultiStyleConfig('ElectionQuestions') - const { fmethods, voteAll, validate, renderWith } = useQuestionsForm() + const { fmethods, voteAll, validate, renderWith, voted, isAbleToVote } = useQuestionsForm() const { ConnectButton, election } = useElection() // use Root election information const [globalError, setGlobalError] = useState('') @@ -58,6 +57,7 @@ export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQu return ( + {renderWith?.length > 0 && renderWith.map(({ id }) => ( @@ -87,7 +87,7 @@ export const ElectionQuestion = (props: ChakraProps) => { if (!(election instanceof PublishedElection)) return null if (voted && !isAbleToVote) { - return + return null } if (!questions || (questions && !questions?.length)) { diff --git a/packages/chakra-components/src/components/Election/Questions/Voted.tsx b/packages/chakra-components/src/components/Election/Questions/Voted.tsx index 0a3b0552..0dbf42a1 100644 --- a/packages/chakra-components/src/components/Election/Questions/Voted.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Voted.tsx @@ -1,16 +1,37 @@ import { Alert, AlertDescription, AlertIcon, AlertTitle } from '@chakra-ui/alert' import { Link } from '@chakra-ui/layout' -import { useMultiStyleConfig } from '@chakra-ui/system' +import { chakra, useMultiStyleConfig } from '@chakra-ui/system' import { useClient, useElection } from '@vocdoni/react-providers' import reactStringReplace from 'react-string-replace' import { environment } from '../../../environment' +import { useQuestionsForm } from './Form' + +export const MultiElectionVoted = () => { + const { voted, elections } = useQuestionsForm() + if (!voted) { + return null + } + const votes = Object.values(elections).map((e) => e.voted) + return +} export const Voted = () => { - const { env } = useClient() - const { localize, voted } = useElection() + const { voted } = useElection() + if (!voted) { + return null + } + return +} + +interface IVotedLogicProps { + voteds: string[] +} + +const VotedLogic = ({ voteds }: IVotedLogicProps) => { + const { localize } = useElection() const styles = useMultiStyleConfig('ElectionQuestions') - if (!voted) { + if (!(voteds?.length > 0)) { return null } @@ -28,12 +49,45 @@ export const Voted = () => { {localize('vote.voted_title')} - {reactStringReplace(localize('vote.voted_description', { id: voted }), voted, (match, k) => ( - - {match} - - ))} + {voteds.length === 1 ? : } ) } + +const SingleElectionVoted = ({ voted }: { voted: string }) => { + const { localize } = useElection() + const { env } = useClient() + const styles = useMultiStyleConfig('ElectionQuestions') + return reactStringReplace(localize('vote.voted_description', { id: voted }), voted, (match, k) => ( + + {match} + + )) +} + +const MultipleElectionVoted = ({ voteds }: IVotedLogicProps) => { + const { localize } = useElection() + const { env } = useClient() + const styles = useMultiStyleConfig('ElectionQuestions') + const votedsString = voteds.join(',') + return ( + + {reactStringReplace(localize('vote.voted_description_multielection', { ids: votedsString }), votedsString, () => ( + <> + {voteds.map((voted) => ( + + {voted} + + ))} + + ))} + + ) +} diff --git a/packages/chakra-components/src/i18n/locales.ts b/packages/chakra-components/src/i18n/locales.ts index b7ffb091..7007dd14 100644 --- a/packages/chakra-components/src/i18n/locales.ts +++ b/packages/chakra-components/src/i18n/locales.ts @@ -102,6 +102,7 @@ export const locales = { confirm: 'Please confirm your choices:', sign: 'Sign first', voted_description: 'Your vote id is {{ id }}. You can use it to verify your vote.', + voted_description_multielection: 'Your vote ids are: {{ ids }} You can use its to verify your votes.', voted_title: 'Your vote was successfully cast!', weight: 'Your voting power is: ', }, diff --git a/packages/chakra-components/src/theme/questions.ts b/packages/chakra-components/src/theme/questions.ts index 7c3d09f5..3a9e51f7 100644 --- a/packages/chakra-components/src/theme/questions.ts +++ b/packages/chakra-components/src/theme/questions.ts @@ -8,6 +8,7 @@ export const questionsAnatomy = [ 'alert', 'alertTitle', 'alertDescription', + 'alertDescriptionWrapper', // Wrapper for multielection voted message 'alertLink', // elections wrapper for multielections 'elections', From 23d449077fec680e791dc7fc7cc19e22437add0a Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 22 Oct 2024 10:26:28 +0200 Subject: [PATCH 21/36] Fix lintern --- .../src/components/Election/Questions/Voted.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Voted.tsx b/packages/chakra-components/src/components/Election/Questions/Voted.tsx index 0dbf42a1..3a4fefca 100644 --- a/packages/chakra-components/src/components/Election/Questions/Voted.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Voted.tsx @@ -59,11 +59,15 @@ const SingleElectionVoted = ({ voted }: { voted: string }) => { const { localize } = useElection() const { env } = useClient() const styles = useMultiStyleConfig('ElectionQuestions') - return reactStringReplace(localize('vote.voted_description', { id: voted }), voted, (match, k) => ( - - {match} - - )) + return ( + <> + {reactStringReplace(localize('vote.voted_description', { id: voted }), voted, (match, k) => ( + + {match} + + ))} + + ) } const MultipleElectionVoted = ({ voteds }: IVotedLogicProps) => { From a3038b6b58fd30bd42b385b0a45939b83cd93509 Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 22 Oct 2024 10:34:42 +0200 Subject: [PATCH 22/36] Refactor variable name --- .../components/Election/Questions/Voted.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Voted.tsx b/packages/chakra-components/src/components/Election/Questions/Voted.tsx index 3a4fefca..37ee58d7 100644 --- a/packages/chakra-components/src/components/Election/Questions/Voted.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Voted.tsx @@ -12,7 +12,7 @@ export const MultiElectionVoted = () => { return null } const votes = Object.values(elections).map((e) => e.voted) - return + return } export const Voted = () => { @@ -20,18 +20,18 @@ export const Voted = () => { if (!voted) { return null } - return + return } interface IVotedLogicProps { - voteds: string[] + votes: string[] } -const VotedLogic = ({ voteds }: IVotedLogicProps) => { +const VotedLogic = ({ votes }: IVotedLogicProps) => { const { localize } = useElection() const styles = useMultiStyleConfig('ElectionQuestions') - if (!(voteds?.length > 0)) { + if (!(votes?.length > 0)) { return null } @@ -49,7 +49,7 @@ const VotedLogic = ({ voteds }: IVotedLogicProps) => { {localize('vote.voted_title')} - {voteds.length === 1 ? : } + {votes.length === 1 ? : } ) @@ -70,16 +70,16 @@ const SingleElectionVoted = ({ voted }: { voted: string }) => { ) } -const MultipleElectionVoted = ({ voteds }: IVotedLogicProps) => { +const MultipleElectionVoted = ({ votes }: IVotedLogicProps) => { const { localize } = useElection() const { env } = useClient() const styles = useMultiStyleConfig('ElectionQuestions') - const votedsString = voteds.join(',') + const votesString = votes.join(',') return ( - {reactStringReplace(localize('vote.voted_description_multielection', { ids: votedsString }), votedsString, () => ( + {reactStringReplace(localize('vote.voted_description_multielection', { ids: votesString }), votesString, () => ( <> - {voteds.map((voted) => ( + {votes.map((voted) => ( Date: Tue, 22 Oct 2024 12:19:19 +0200 Subject: [PATCH 23/36] Create FormFieldValues type --- .../components/Election/Questions/Confirmation.tsx | 2 +- .../src/components/Election/Questions/Form.tsx | 12 +++++++----- .../src/components/Election/Questions/Questions.tsx | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx b/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx index 2a59cb51..ee383165 100644 --- a/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx @@ -9,7 +9,7 @@ import { useConfirm } from '../../layout' import { ElectionStateStorage } from './Questions' export type QuestionsConfirmationProps = { - answers: Record + answers: FormFieldValues elections: ElectionStateStorage } diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 66a11562..f36d804a 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -7,8 +7,10 @@ import { useConfirm } from '../../layout' import { QuestionsConfirmation } from './Confirmation' import { ElectionStateStorage, RenderWith, SubElectionState, SubmitFormValidation } from './Questions' +export type FormFieldValues = Record + export type QuestionsFormContextState = { - fmethods: UseFormReturn + fmethods: UseFormReturn } & SpecificFormProviderProps & ReturnType @@ -23,7 +25,7 @@ export const useQuestionsForm = () => { } export type QuestionsFormProviderProps = { - confirmContents?: (elections: ElectionStateStorage, answers: Record) => ReactNode + confirmContents?: (elections: ElectionStateStorage, answers: FormFieldValues) => ReactNode } // Props that must not be shared with ElectionQuestionsProps @@ -35,7 +37,7 @@ export type SpecificFormProviderProps = { export const QuestionsFormProvider: React.FC< PropsWithChildren > = ({ children, ...props }) => { - const fmethods = useForm() + const fmethods = useForm() const multiElections = useMultiElectionsProvider({ fmethods, ...props }) return ( @@ -85,7 +87,7 @@ export const constructVoteBallot = (election: PublishedElection, choices: FieldV const useMultiElectionsProvider = ({ fmethods, confirmContents, -}: { fmethods: UseFormReturn } & QuestionsFormProviderProps) => { +}: { fmethods: UseFormReturn } & QuestionsFormProviderProps) => { const { confirm } = useConfirm() const { client, isAbleToVote: rootIsAbleToVote, voted: rootVoted, election, vote } = useElection() // Root Election // State to store on memory the loaded elections to pass it into confirm modal to show the info @@ -144,7 +146,7 @@ const useMultiElectionsProvider = ({ addElection(rootElectionState) }, [rootElectionState, electionsStates, election]) - const voteAll = async (values: Record) => { + const voteAll = async (values: FormFieldValues) => { if (!electionsStates || Object.keys(electionsStates).length === 0) { console.warn('vote attempt with no valid elections defined') return false diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index 63830782..8e6da771 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -4,7 +4,7 @@ import { ElectionProvider, ElectionState, useElection } from '@vocdoni/react-pro import { IQuestion, PublishedElection } from '@vocdoni/sdk' import { FieldValues, SubmitErrorHandler, ValidateResult } from 'react-hook-form' import { QuestionField } from './Fields' -import { QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' +import { FormFieldValues, QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' import { QuestionsTypeBadge } from './TypeBadge' import { MultiElectionVoted, Voted } from './Voted' import { FormControl, FormErrorMessage } from '@chakra-ui/form-control' @@ -14,7 +14,7 @@ export type RenderWith = { id: string } -export type SubmitFormValidation = (values: Record) => ValidateResult | Promise +export type SubmitFormValidation = (values: FormFieldValues) => ValidateResult | Promise export type ElectionQuestionsFormProps = ChakraProps & { onInvalid?: SubmitErrorHandler @@ -40,7 +40,7 @@ export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQu const { handleSubmit, watch } = fmethods const formData = watch() - const onSubmit = (values: Record) => { + const onSubmit = (values: FormFieldValues) => { if (validate) { const error = validate(formData) if (typeof error === 'string' || (typeof error === 'boolean' && !error)) { From c9f7176b6b430d2842f7b77e6a68e6e645841e06 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 23 Oct 2024 17:11:25 +0200 Subject: [PATCH 24/36] Expose isDisabled to manually disable the form --- .../components/Election/Questions/Fields.tsx | 19 ++++++++++--------- .../components/Election/Questions/Form.tsx | 4 ++++ .../Election/Questions/Questions.tsx | 14 +++++++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Fields.tsx b/packages/chakra-components/src/components/Election/Questions/Fields.tsx index cc49b4f0..2e00841b 100644 --- a/packages/chakra-components/src/components/Election/Questions/Fields.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Fields.tsx @@ -12,11 +12,12 @@ import { QuestionTip } from './Tip' export type QuestionProps = { index: string question: IQuestion + isDisabled?: boolean } export type QuestionFieldProps = ChakraProps & QuestionProps -export const QuestionField = ({ question, index }: QuestionFieldProps) => { +export const QuestionField = ({ question, index, isDisabled }: QuestionFieldProps) => { const styles = useMultiStyleConfig('ElectionQuestions') const { formState: { errors }, @@ -40,7 +41,7 @@ export const QuestionField = ({ question, index }: QuestionFieldProps) => { {question.description.default} )} - + @@ -64,7 +65,7 @@ export const FieldSwitcher = (props: QuestionProps) => { } } -export const MultiChoice = ({ index, question }: QuestionProps) => { +export const MultiChoice = ({ index, question, isDisabled }: QuestionProps) => { const styles = useMultiStyleConfig('ElectionQuestions') const { election, @@ -91,7 +92,7 @@ export const MultiChoice = ({ index, question }: QuestionProps) => { { // allow a single selection if is an abstain @@ -145,7 +146,7 @@ export const MultiChoice = ({ index, question }: QuestionProps) => { ) } -export const ApprovalChoice = ({ index, question }: QuestionProps) => { +export const ApprovalChoice = ({ index, question, isDisabled }: QuestionProps) => { const styles = useMultiStyleConfig('ElectionQuestions') const { election, @@ -170,7 +171,7 @@ export const ApprovalChoice = ({ index, question }: QuestionProps) => { { return (v && v.length > 0) || localize('validation.at_least_one') @@ -187,7 +188,7 @@ export const ApprovalChoice = ({ index, question }: QuestionProps) => { key={ck} sx={styles.checkbox} value={choice.value.toString()} - isDisabled={isNotAbleToVote} + isDisabled={isNotAbleToVote || isDisabled} onChange={(e) => { if (values.includes(e.target.value)) { onChange(values.filter((v: string) => v !== e.target.value)) @@ -209,7 +210,7 @@ export const ApprovalChoice = ({ index, question }: QuestionProps) => { ) } -export const SingleChoice = ({ index, question }: QuestionProps) => { +export const SingleChoice = ({ index, question, isDisabled }: QuestionProps) => { const styles = useMultiStyleConfig('ElectionQuestions') const { election, @@ -224,7 +225,7 @@ export const SingleChoice = ({ index, question }: QuestionProps) => { if (!(election instanceof PublishedElection)) return null - const disabled = election?.status !== ElectionStatus.ONGOING || !isAbleToVote || voting + const disabled = election?.status !== ElectionStatus.ONGOING || !isAbleToVote || voting || isDisabled return ( } & QuestionsFormProviderProps) => { const { confirm } = useConfirm() + // State to manually disable the form + const [isDisabled, setIsDisabled] = useState(false) const { client, isAbleToVote: rootIsAbleToVote, voted: rootVoted, election, vote } = useElection() // Root Election // State to store on memory the loaded elections to pass it into confirm modal to show the info const [electionsStates, setElectionsStates] = useState({}) @@ -185,5 +187,7 @@ const useMultiElectionsProvider = ({ addElection, isAbleToVote, voted, + isDisabled, + setIsDisabled, } } diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index 8e6da771..b9c1335d 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -3,7 +3,7 @@ import { chakra, ChakraProps, useMultiStyleConfig } from '@chakra-ui/system' import { ElectionProvider, ElectionState, useElection } from '@vocdoni/react-providers' import { IQuestion, PublishedElection } from '@vocdoni/sdk' import { FieldValues, SubmitErrorHandler, ValidateResult } from 'react-hook-form' -import { QuestionField } from './Fields' +import { QuestionField, QuestionProps } from './Fields' import { FormFieldValues, QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' import { QuestionsTypeBadge } from './TypeBadge' import { MultiElectionVoted, Voted } from './Voted' @@ -33,7 +33,7 @@ export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestio export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQuestionsFormProps) => { const styles = useMultiStyleConfig('ElectionQuestions') - const { fmethods, voteAll, validate, renderWith, voted, isAbleToVote } = useQuestionsForm() + const { fmethods, voteAll, validate, renderWith, isDisabled } = useQuestionsForm() const { ConnectButton, election } = useElection() // use Root election information const [globalError, setGlobalError] = useState('') @@ -58,7 +58,7 @@ export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQu - + {renderWith?.length > 0 && renderWith.map(({ id }) => ( @@ -73,7 +73,7 @@ export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQu ) } -export const ElectionQuestion = (props: ChakraProps) => { +export const ElectionQuestion = ({ isDisabled, ...props }: Pick & ChakraProps) => { const { election, voted, @@ -106,7 +106,7 @@ export const ElectionQuestion = (props: ChakraProps) => { {questions.map((question, qk) => ( - + ))} {error && ( @@ -122,7 +122,7 @@ export type SubElectionState = { election: PublishedElection } & Pick export const SubElectionQuestions = (props: ChakraProps) => { - const { rootClient, addElection, elections } = useQuestionsForm() + const { rootClient, addElection, elections, isDisabled } = useQuestionsForm() const { election, setClient, vote, connected, clearClient, isAbleToVote, voted } = useElection() const subElectionState: SubElectionState | null = useMemo(() => { @@ -156,5 +156,5 @@ export const SubElectionQuestions = (props: ChakraProps) => { addElection(subElectionState) }, [subElectionState, elections, election]) - return + return } From 32181c90e1cb46cd2cefa4da2ba315b3087701ee Mon Sep 17 00:00:00 2001 From: selankon Date: Thu, 24 Oct 2024 10:39:08 +0200 Subject: [PATCH 25/36] Expose onSubmit handler --- .../Election/Questions/Questions.tsx | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index b9c1335d..a0cbd2be 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -2,13 +2,13 @@ import { Alert, AlertIcon } from '@chakra-ui/alert' import { chakra, ChakraProps, useMultiStyleConfig } from '@chakra-ui/system' import { ElectionProvider, ElectionState, useElection } from '@vocdoni/react-providers' import { IQuestion, PublishedElection } from '@vocdoni/sdk' -import { FieldValues, SubmitErrorHandler, ValidateResult } from 'react-hook-form' +import { FieldValues, SubmitErrorHandler, SubmitHandler, ValidateResult } from 'react-hook-form' import { QuestionField, QuestionProps } from './Fields' import { FormFieldValues, QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' import { QuestionsTypeBadge } from './TypeBadge' import { MultiElectionVoted, Voted } from './Voted' import { FormControl, FormErrorMessage } from '@chakra-ui/form-control' -import { useEffect, useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' export type RenderWith = { id: string @@ -16,8 +16,14 @@ export type RenderWith = { export type SubmitFormValidation = (values: FormFieldValues) => ValidateResult | Promise +export type ExtendedSubmitHandler = ( + onSubmit: SubmitHandler, + ...args: [...Parameters>] +) => ReturnType> + export type ElectionQuestionsFormProps = ChakraProps & { onInvalid?: SubmitErrorHandler + onSubmit?: ExtendedSubmitHandler formId?: string } @@ -31,7 +37,7 @@ export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestio ) } -export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQuestionsFormProps) => { +export const ElectionQuestionsForm = ({ formId, onSubmit, onInvalid, ...rest }: ElectionQuestionsFormProps) => { const styles = useMultiStyleConfig('ElectionQuestions') const { fmethods, voteAll, validate, renderWith, isDisabled } = useQuestionsForm() const { ConnectButton, election } = useElection() // use Root election information @@ -40,7 +46,7 @@ export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQu const { handleSubmit, watch } = fmethods const formData = watch() - const onSubmit = (values: FormFieldValues) => { + const _onSubmit = (values: FormFieldValues) => { if (validate) { const error = validate(formData) if (typeof error === 'string' || (typeof error === 'boolean' && !error)) { @@ -55,7 +61,15 @@ export const ElectionQuestionsForm = ({ formId, onInvalid, ...rest }: ElectionQu if (!(election instanceof PublishedElection)) return null return ( - + { + if (onSubmit) { + return onSubmit(_onSubmit, ...params) + } + return _onSubmit(params[0]) + }, onInvalid)} + id={formId ?? `election-questions-${election.id}`} + > From 47c0eb16ea941671b62cca5e9c6bc743db1b3396 Mon Sep 17 00:00:00 2001 From: selankon Date: Thu, 24 Oct 2024 11:11:41 +0200 Subject: [PATCH 26/36] Fix lint --- .../src/components/Election/Questions/Confirmation.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx b/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx index ee383165..7fb5143c 100644 --- a/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Confirmation.tsx @@ -6,6 +6,7 @@ import { useClient } from '@vocdoni/react-providers' import { ElectionResultsTypeNames } from '@vocdoni/sdk' import { FieldValues } from 'react-hook-form' import { useConfirm } from '../../layout' +import { FormFieldValues } from './Form' import { ElectionStateStorage } from './Questions' export type QuestionsConfirmationProps = { From a9c1796765652d906f56ba3ceca5e3d2ab45de01 Mon Sep 17 00:00:00 2001 From: selankon Date: Fri, 25 Oct 2024 11:02:25 +0200 Subject: [PATCH 27/36] Fix version --- packages/chakra-components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chakra-components/package.json b/packages/chakra-components/package.json index 32404e75..cb49bb8a 100644 --- a/packages/chakra-components/package.json +++ b/packages/chakra-components/package.json @@ -1,6 +1,6 @@ { "name": "@vocdoni/chakra-components", - "version": "0.9.6-2", + "version": "0.9.6", "license": "GPL-3.0-or-later", "homepage": "https://github.com/vocdoni/ui-components/tree/main/packages/chakra-components#readme", "bugs": "https://github.com/vocdoni/ui-components/issues", From 8fc54f9ce431828aa566b4fb7735d054078345a9 Mon Sep 17 00:00:00 2001 From: selankon Date: Fri, 25 Oct 2024 12:39:06 +0200 Subject: [PATCH 28/36] Delete uneeded resolution --- package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/package.json b/package.json index 78613ad7..c502c4f9 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,5 @@ "engines": { "npm": "please use yarn", "yarn": ">= 1.19.1 && < 2" - }, - "resolutions": { - "ffjavascript": "^0.3.1" - } + } } From ee98753171074d5c8d432adaf69975ced5da1bbb Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 29 Oct 2024 11:46:27 +0100 Subject: [PATCH 29/36] Fix null --- .../src/components/Election/Questions/Form.tsx | 2 +- .../src/components/Election/Questions/Voted.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 2cb6a35b..866301c0 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -43,7 +43,7 @@ export const QuestionsFormProvider: React.FC< return ( {children} diff --git a/packages/chakra-components/src/components/Election/Questions/Voted.tsx b/packages/chakra-components/src/components/Election/Questions/Voted.tsx index 37ee58d7..d322dcde 100644 --- a/packages/chakra-components/src/components/Election/Questions/Voted.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Voted.tsx @@ -11,7 +11,9 @@ export const MultiElectionVoted = () => { if (!voted) { return null } - const votes = Object.values(elections).map((e) => e.voted) + const votes = Object.values(elections) + .map((e) => e.voted) + .filter((voted) => voted !== null) return } From 0c59e5ef6ac36a794a8a4bf3c4b16eb25ebd7978 Mon Sep 17 00:00:00 2001 From: selankon Date: Mon, 4 Nov 2024 10:56:15 +0100 Subject: [PATCH 30/36] Add chakra.form --- .../src/components/Election/Questions/Questions.tsx | 7 ++++--- packages/chakra-components/src/theme/questions.ts | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index a0cbd2be..b13dbcb4 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -61,7 +61,7 @@ export const ElectionQuestionsForm = ({ formId, onSubmit, onInvalid, ...rest }: if (!(election instanceof PublishedElection)) return null return ( - { if (onSubmit) { return onSubmit(_onSubmit, ...params) @@ -69,9 +69,10 @@ export const ElectionQuestionsForm = ({ formId, onSubmit, onInvalid, ...rest }: return _onSubmit(params[0]) }, onInvalid)} id={formId ?? `election-questions-${election.id}`} + __css={styles.form} > + - {renderWith?.length > 0 && renderWith.map(({ id }) => ( @@ -83,7 +84,7 @@ export const ElectionQuestionsForm = ({ formId, onSubmit, onInvalid, ...rest }: {globalError} - + ) } diff --git a/packages/chakra-components/src/theme/questions.ts b/packages/chakra-components/src/theme/questions.ts index 3a9e51f7..a53f48f3 100644 --- a/packages/chakra-components/src/theme/questions.ts +++ b/packages/chakra-components/src/theme/questions.ts @@ -2,7 +2,7 @@ import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system' import { theme } from '@chakra-ui/theme' export const questionsAnatomy = [ - // main content wrapper + // Question wrapper 'wrapper', // alert messages (voted or no questions available) 'alert', @@ -33,6 +33,8 @@ export const questionsAnatomy = [ 'checkbox', // form error message 'error', + // form wrapper + 'form', ] export const questionsConfirmationAnatomy = [ From aa4958cc0f42aa26adc3b279ce33d88c1f2d1296 Mon Sep 17 00:00:00 2001 From: selankon Date: Mon, 4 Nov 2024 13:22:16 +0100 Subject: [PATCH 31/36] Use isDisabled properly --- .../src/components/Election/Questions/Fields.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Fields.tsx b/packages/chakra-components/src/components/Election/Questions/Fields.tsx index 2e00841b..ea727101 100644 --- a/packages/chakra-components/src/components/Election/Questions/Fields.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Fields.tsx @@ -78,7 +78,7 @@ export const MultiChoice = ({ index, question, isDisabled }: QuestionProps) => { if (!(election instanceof PublishedElection)) return null - const isNotAbleToVote = election?.status !== ElectionStatus.ONGOING || !isAbleToVote || voting + const disabled = election?.status !== ElectionStatus.ONGOING || !isAbleToVote || voting || isDisabled if (!(election && election.resultsType.name === ElectionResultsTypeNames.MULTIPLE_CHOICE)) { return null @@ -92,7 +92,7 @@ export const MultiChoice = ({ index, question, isDisabled }: QuestionProps) => { { // allow a single selection if is an abstain @@ -121,7 +121,7 @@ export const MultiChoice = ({ index, question, isDisabled }: QuestionProps) => { key={ck} sx={styles.checkbox} value={choice.value.toString()} - isDisabled={isNotAbleToVote || maxSelected} + isDisabled={disabled || maxSelected} isChecked={currentValues.includes(choice.value.toString())} onChange={(e) => { if (values.includes(e.target.value)) { @@ -159,7 +159,7 @@ export const ApprovalChoice = ({ index, question, isDisabled }: QuestionProps) = if (!(election instanceof PublishedElection)) return null - const isNotAbleToVote = election?.status !== ElectionStatus.ONGOING || !isAbleToVote || voting + const disabled = election?.status !== ElectionStatus.ONGOING || !isAbleToVote || voting || isDisabled if (!(election && election.resultsType.name === ElectionResultsTypeNames.APPROVAL)) { return null @@ -171,7 +171,7 @@ export const ApprovalChoice = ({ index, question, isDisabled }: QuestionProps) = { return (v && v.length > 0) || localize('validation.at_least_one') @@ -188,7 +188,7 @@ export const ApprovalChoice = ({ index, question, isDisabled }: QuestionProps) = key={ck} sx={styles.checkbox} value={choice.value.toString()} - isDisabled={isNotAbleToVote || isDisabled} + isDisabled={disabled} onChange={(e) => { if (values.includes(e.target.value)) { onChange(values.filter((v: string) => v !== e.target.value)) From 27eb8cd9cf67a553c18c97871a508eb8ffd6cb2d Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 5 Nov 2024 11:18:13 +0100 Subject: [PATCH 32/36] Fix renderWith logout --- .../src/components/Election/Questions/Form.tsx | 15 +++++++++------ .../components/Election/Questions/Questions.tsx | 13 +++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 866301c0..f2563cb9 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -130,12 +130,15 @@ const useMultiElectionsProvider = ({ // reset form if account gets disconnected useEffect(() => { - if (typeof client.wallet !== 'undefined') return - - setElectionsStates({}) - fmethods.reset({ - ...Object.values(electionsStates).reduce((acc, { election }) => ({ ...acc, [election.id]: '' }), {}), - }) + if ( + (typeof client.wallet === 'undefined' || Object.values(client.wallet).length === 0) && + Object.keys(electionsStates).length > 0 + ) { + setElectionsStates({}) + fmethods.reset({ + ...Object.values(electionsStates).reduce((acc, { election }) => ({ ...acc, [election.id]: '' }), {}), + }) + } }, [client, electionsStates, fmethods]) // Add the root election to the state to elections cache diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index b13dbcb4..0e1f1168 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -138,7 +138,7 @@ export type ElectionStateStorage = Record export const SubElectionQuestions = (props: ChakraProps) => { const { rootClient, addElection, elections, isDisabled } = useQuestionsForm() - const { election, setClient, vote, connected, clearClient, isAbleToVote, voted } = useElection() + const { election, setClient, vote, clearClient, isAbleToVote, voted } = useElection() const subElectionState: SubElectionState | null = useMemo(() => { if (!election || !(election instanceof PublishedElection)) return null @@ -152,16 +152,17 @@ export const SubElectionQuestions = (props: ChakraProps) => { // clear session of local context when login out useEffect(() => { - if (connected) return - clearClient() - }, [connected]) + if (rootClient.wallet === undefined || Object.keys(rootClient.wallet).length === 0) { + clearClient() + } + }, [rootClient]) // ensure the client is set to the root one useEffect(() => { setClient(rootClient) - }, [rootClient, election]) + }, [rootClient]) - // Add the election to the state cache + // Add the sub election to the state cache useEffect(() => { if (!subElectionState || !subElectionState.election) return const actualState = elections[subElectionState.election.id] From b1493f3c660edceb7fa110f2be3d7ea3a38a1083 Mon Sep 17 00:00:00 2001 From: selankon Date: Tue, 5 Nov 2024 13:08:24 +0100 Subject: [PATCH 33/36] Implement loaded state --- .../components/Election/Questions/Form.tsx | 73 +++++++++++++------ .../Election/Questions/Questions.tsx | 28 ++++--- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index f2563cb9..3668fd27 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -1,7 +1,16 @@ import { Wallet } from '@ethersproject/wallet' import { useElection } from '@vocdoni/react-providers' import { ElectionResultsTypeNames, PublishedElection } from '@vocdoni/sdk' -import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from 'react' +import React, { + createContext, + PropsWithChildren, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react' import { FieldValues, FormProvider, useForm, UseFormReturn } from 'react-hook-form' import { useConfirm } from '../../layout' import { QuestionsConfirmation } from './Confirmation' @@ -39,11 +48,12 @@ export const QuestionsFormProvider: React.FC< > = ({ children, ...props }) => { const fmethods = useForm() const multiElections = useMultiElectionsProvider({ fmethods, ...props }) + const value = { fmethods, renderWith: props.renderWith, validate: props.validate, ...multiElections } return ( {children} @@ -87,35 +97,51 @@ export const constructVoteBallot = (election: PublishedElection, choices: FieldV const useMultiElectionsProvider = ({ fmethods, confirmContents, -}: { fmethods: UseFormReturn } & QuestionsFormProviderProps) => { + ...rest +}: { fmethods: UseFormReturn } & QuestionsFormProviderProps & SpecificFormProviderProps) => { const { confirm } = useConfirm() // State to manually disable the form const [isDisabled, setIsDisabled] = useState(false) - const { client, isAbleToVote: rootIsAbleToVote, voted: rootVoted, election, vote } = useElection() // Root Election + const { client, isAbleToVote: rootIsAbleToVote, voted: rootVoted, election, loaded: rootLoaded, vote } = useElection() // Root Election // State to store on memory the loaded elections to pass it into confirm modal to show the info const [electionsStates, setElectionsStates] = useState({}) const [voting, setVoting] = useState(false) - const electionsEmpty = Object.values(electionsStates).length === 0 - + // Util to check if the electionsStates object contains elections and is not empty + const _electionsCount = Object.values(electionsStates).length const voted = useMemo( () => - electionsStates && !electionsEmpty && Object.values(electionsStates).every(({ voted }) => voted) ? 'true' : null, - [electionsStates, electionsEmpty] + electionsStates && _electionsCount > 0 && Object.values(electionsStates).every(({ voted }) => voted) + ? 'true' + : null, + [electionsStates, _electionsCount] ) const isAbleToVote = useMemo( - () => electionsStates && !electionsEmpty && Object.values(electionsStates).some(({ isAbleToVote }) => isAbleToVote), - [electionsStates, electionsEmpty] + () => + electionsStates && _electionsCount > 0 && Object.values(electionsStates).some(({ isAbleToVote }) => isAbleToVote), + [electionsStates, _electionsCount] + ) + + const loaded = useMemo( + () => + electionsStates && + _electionsCount > 0 && + _electionsCount === rest.renderWith?.length + 1 && // If the amount of elections is the same as the amount of subelections + root election + Object.values(electionsStates).every(({ loaded }) => loaded.election), + [electionsStates, _electionsCount] ) // Add an election to the storage - const addElection = (electionState: SubElectionState) => { - setElectionsStates((prev) => ({ - ...prev, - [(electionState.election as PublishedElection).id]: electionState, - })) - } + const addElection = useCallback( + (electionState: SubElectionState) => { + setElectionsStates((prev) => ({ + ...prev, + [(electionState.election as PublishedElection).id]: electionState, + })) + }, + [setElectionsStates] + ) // Root election state to be added to the state storage const rootElectionState: SubElectionState | null = useMemo(() => { @@ -125,8 +151,9 @@ const useMultiElectionsProvider = ({ election, isAbleToVote: rootIsAbleToVote, voted: rootVoted, + loaded: rootLoaded, } - }, [vote, election, rootIsAbleToVote, rootVoted]) + }, [vote, election, rootIsAbleToVote, rootVoted, rootLoaded]) // reset form if account gets disconnected useEffect(() => { @@ -134,7 +161,6 @@ const useMultiElectionsProvider = ({ (typeof client.wallet === 'undefined' || Object.values(client.wallet).length === 0) && Object.keys(electionsStates).length > 0 ) { - setElectionsStates({}) fmethods.reset({ ...Object.values(electionsStates).reduce((acc, { election }) => ({ ...acc, [election.id]: '' }), {}), }) @@ -145,11 +171,13 @@ const useMultiElectionsProvider = ({ useEffect(() => { if (!rootElectionState || !rootElectionState.election) return const actualState = electionsStates[rootElectionState.election.id] - if (rootElectionState.vote === actualState?.vote || rootElectionState.isAbleToVote === actualState?.isAbleToVote) { - return + if ( + (!actualState && rootElectionState.loaded.election) || + (actualState && rootElectionState.isAbleToVote !== actualState?.isAbleToVote) + ) { + addElection(rootElectionState) } - addElection(rootElectionState) - }, [rootElectionState, electionsStates, election]) + }, [rootElectionState, electionsStates]) const voteAll = async (values: FormFieldValues) => { if (!electionsStates || Object.keys(electionsStates).length === 0) { @@ -192,5 +220,6 @@ const useMultiElectionsProvider = ({ voted, isDisabled, setIsDisabled, + loaded, } } diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index 0e1f1168..b8b770b6 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -39,7 +39,7 @@ export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestio export const ElectionQuestionsForm = ({ formId, onSubmit, onInvalid, ...rest }: ElectionQuestionsFormProps) => { const styles = useMultiStyleConfig('ElectionQuestions') - const { fmethods, voteAll, validate, renderWith, isDisabled } = useQuestionsForm() + const { loaded, fmethods, voteAll, validate, renderWith, isDisabled } = useQuestionsForm() const { ConnectButton, election } = useElection() // use Root election information const [globalError, setGlobalError] = useState('') @@ -73,7 +73,7 @@ export const ElectionQuestionsForm = ({ formId, onSubmit, onInvalid, ...rest }: > - + {loaded && } {renderWith?.length > 0 && renderWith.map(({ id }) => ( @@ -133,12 +133,15 @@ export const ElectionQuestion = ({ isDisabled, ...props }: Pick +export type SubElectionState = { election: PublishedElection } & Pick< + ElectionState, + 'vote' | 'isAbleToVote' | 'voted' | 'loaded' +> export type ElectionStateStorage = Record export const SubElectionQuestions = (props: ChakraProps) => { - const { rootClient, addElection, elections, isDisabled } = useQuestionsForm() - const { election, setClient, vote, clearClient, isAbleToVote, voted } = useElection() + const { rootClient, addElection, elections, isDisabled, loaded: renderWithLoaded } = useQuestionsForm() + const { election, setClient, vote, clearClient, isAbleToVote, voted, loaded } = useElection() const subElectionState: SubElectionState | null = useMemo(() => { if (!election || !(election instanceof PublishedElection)) return null @@ -147,8 +150,9 @@ export const SubElectionQuestions = (props: ChakraProps) => { election, isAbleToVote, voted, + loaded, } - }, [vote, election, isAbleToVote, voted]) + }, [vote, election, isAbleToVote, voted, loaded]) // clear session of local context when login out useEffect(() => { @@ -166,11 +170,15 @@ export const SubElectionQuestions = (props: ChakraProps) => { useEffect(() => { if (!subElectionState || !subElectionState.election) return const actualState = elections[subElectionState.election.id] - if (subElectionState.vote === actualState?.vote || subElectionState.isAbleToVote === actualState?.isAbleToVote) { - return + if ( + (!actualState && subElectionState.loaded.election) || + (actualState && subElectionState.isAbleToVote !== actualState?.isAbleToVote) + ) { + addElection(subElectionState) } - addElection(subElectionState) - }, [subElectionState, elections, election]) + }, [subElectionState, elections]) + + if (!renderWithLoaded) return null return } From dd00b0110ae8fd93b6e2623ff9bcde37a48db341 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 6 Nov 2024 14:55:40 +0100 Subject: [PATCH 34/36] Fix not renderWith elections loaded state --- .../src/components/Election/Questions/Form.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 3668fd27..607dd00f 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -123,14 +123,18 @@ const useMultiElectionsProvider = ({ [electionsStates, _electionsCount] ) - const loaded = useMemo( - () => + const loaded = useMemo(() => { + let renderWithCached = true + if (rest.renderWith?.length) { + renderWithCached = _electionsCount === rest.renderWith?.length + 1 + } + return ( electionsStates && _electionsCount > 0 && - _electionsCount === rest.renderWith?.length + 1 && // If the amount of elections is the same as the amount of subelections + root election - Object.values(electionsStates).every(({ loaded }) => loaded.election), - [electionsStates, _electionsCount] - ) + renderWithCached && // If the amount of elections is the same as the amount of subelections + root election + Object.values(electionsStates).every(({ loaded }) => loaded.election) + ) + }, [rest.renderWith?.length, electionsStates, _electionsCount]) // Add an election to the storage const addElection = useCallback( From 4d94f493c0d00d0c76a349fcd100d546f829f627 Mon Sep 17 00:00:00 2001 From: selankon Date: Mon, 11 Nov 2024 08:36:27 -0300 Subject: [PATCH 35/36] Delete global error logic --- .../components/Election/Questions/Form.tsx | 11 ++---- .../Election/Questions/Questions.tsx | 34 ++++--------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Form.tsx b/packages/chakra-components/src/components/Election/Questions/Form.tsx index 607dd00f..4860d898 100644 --- a/packages/chakra-components/src/components/Election/Questions/Form.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Form.tsx @@ -14,7 +14,7 @@ import React, { import { FieldValues, FormProvider, useForm, UseFormReturn } from 'react-hook-form' import { useConfirm } from '../../layout' import { QuestionsConfirmation } from './Confirmation' -import { ElectionStateStorage, RenderWith, SubElectionState, SubmitFormValidation } from './Questions' +import { ElectionStateStorage, RenderWith, SubElectionState } from './Questions' export type FormFieldValues = Record @@ -40,7 +40,6 @@ export type QuestionsFormProviderProps = { // Props that must not be shared with ElectionQuestionsProps export type SpecificFormProviderProps = { renderWith?: RenderWith[] - validate?: SubmitFormValidation } export const QuestionsFormProvider: React.FC< @@ -48,15 +47,11 @@ export const QuestionsFormProvider: React.FC< > = ({ children, ...props }) => { const fmethods = useForm() const multiElections = useMultiElectionsProvider({ fmethods, ...props }) - const value = { fmethods, renderWith: props.renderWith, validate: props.validate, ...multiElections } + const value = { fmethods, renderWith: props.renderWith, ...multiElections } return ( - - {children} - + {children} ) } diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index b8b770b6..e8a9d927 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -2,20 +2,17 @@ import { Alert, AlertIcon } from '@chakra-ui/alert' import { chakra, ChakraProps, useMultiStyleConfig } from '@chakra-ui/system' import { ElectionProvider, ElectionState, useElection } from '@vocdoni/react-providers' import { IQuestion, PublishedElection } from '@vocdoni/sdk' -import { FieldValues, SubmitErrorHandler, SubmitHandler, ValidateResult } from 'react-hook-form' +import { FieldValues, SubmitErrorHandler, SubmitHandler } from 'react-hook-form' import { QuestionField, QuestionProps } from './Fields' import { FormFieldValues, QuestionsFormProvider, QuestionsFormProviderProps, useQuestionsForm } from './Form' import { QuestionsTypeBadge } from './TypeBadge' import { MultiElectionVoted, Voted } from './Voted' -import { FormControl, FormErrorMessage } from '@chakra-ui/form-control' -import React, { useEffect, useMemo, useState } from 'react' +import React, { useEffect, useMemo } from 'react' export type RenderWith = { id: string } -export type SubmitFormValidation = (values: FormFieldValues) => ValidateResult | Promise - export type ExtendedSubmitHandler = ( onSubmit: SubmitHandler, ...args: [...Parameters>] @@ -39,24 +36,10 @@ export const ElectionQuestions = ({ confirmContents, ...props }: ElectionQuestio export const ElectionQuestionsForm = ({ formId, onSubmit, onInvalid, ...rest }: ElectionQuestionsFormProps) => { const styles = useMultiStyleConfig('ElectionQuestions') - const { loaded, fmethods, voteAll, validate, renderWith, isDisabled } = useQuestionsForm() + const { loaded, fmethods, voteAll, renderWith, isDisabled } = useQuestionsForm() const { ConnectButton, election } = useElection() // use Root election information - const [globalError, setGlobalError] = useState('') - - const { handleSubmit, watch } = fmethods - const formData = watch() - - const _onSubmit = (values: FormFieldValues) => { - if (validate) { - const error = validate(formData) - if (typeof error === 'string' || (typeof error === 'boolean' && !error)) { - setGlobalError(error.toString()) - return - } - setGlobalError('') - } - voteAll(values) - } + + const { handleSubmit } = fmethods if (!(election instanceof PublishedElection)) return null @@ -64,9 +47,9 @@ export const ElectionQuestionsForm = ({ formId, onSubmit, onInvalid, ...rest }: { if (onSubmit) { - return onSubmit(_onSubmit, ...params) + return onSubmit(voteAll, ...params) } - return _onSubmit(params[0]) + return voteAll(params[0]) }, onInvalid)} id={formId ?? `election-questions-${election.id}`} __css={styles.form} @@ -81,9 +64,6 @@ export const ElectionQuestionsForm = ({ formId, onSubmit, onInvalid, ...rest }: ))} - - {globalError} - ) } From 562beb108f31f936f414eed13a9ddabc5dfe3928 Mon Sep 17 00:00:00 2001 From: selankon Date: Fri, 15 Nov 2024 08:12:29 -0300 Subject: [PATCH 36/36] Add missing dependency --- .../src/components/Election/Questions/Questions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chakra-components/src/components/Election/Questions/Questions.tsx b/packages/chakra-components/src/components/Election/Questions/Questions.tsx index e8a9d927..53f23d94 100644 --- a/packages/chakra-components/src/components/Election/Questions/Questions.tsx +++ b/packages/chakra-components/src/components/Election/Questions/Questions.tsx @@ -144,7 +144,7 @@ export const SubElectionQuestions = (props: ChakraProps) => { // ensure the client is set to the root one useEffect(() => { setClient(rootClient) - }, [rootClient]) + }, [election, rootClient]) // Add the sub election to the state cache useEffect(() => {