Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
elboletaire committed Jun 13, 2024
1 parent a7c1033 commit eca37d6
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/netlify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
run: yarn build
env:
VOCDONI_ENVIRONMENT: stg
PROCESS_IDS: '["4ae20a8eb4caa52f5588f7bb9f3c6d6b7cf003a5b03f4589edea100000000290","4ae20a8eb4caa52f5588f7bb9f3c6d6b7cf003a5b03f4589edea10000000027d"]'
PROCESS_IDS: '["4ae20a8eb4ca8bd340fc16a71ae591b88418c42e799705b9807302000000000f"]'

- name: Deploy to Netlify
uses: nwtgck/[email protected]
Expand Down
17 changes: 9 additions & 8 deletions src/components/Process/Aside.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Button, Card, Flex, Link, Text } from '@chakra-ui/react'
import { Box, Button, Card, Flex, FlexProps, Link, Text } from '@chakra-ui/react'
import { ConnectButton } from '@rainbow-me/rainbowkit'
import { VoteButton as CVoteButton, environment, SpreadsheetAccess, VoteWeight } from '@vocdoni/chakra-components'
import { useClient, useElection } from '@vocdoni/react-providers'
Expand Down Expand Up @@ -183,20 +183,23 @@ const ProcessAside = () => {
)
}

export const VoteButton = ({ setQuestionsTab, ...props }: { setQuestionsTab: () => void }) => {
export const VoteButton = ({ ...props }: FlexProps) => {
const { t } = useTranslation()
const { election, connected, isAbleToVote, isInCensus } = useElection()
const { isConnected } = useAccount()

if (!(election instanceof PublishedElection)) return null
if (!(election instanceof PublishedElection)) {
return null
}

const census: CensusMeta = dotobject(election?.meta || {}, 'census')
const census: CensusMeta | null = election.get('census')

if (
election?.status === ElectionStatus.CANCELED ||
(isConnected && !isInCensus && !['spreadsheet', 'csp'].includes(census?.type))
)
(isConnected && !isInCensus && !['spreadsheet', 'csp'].includes(census!.type))
) {
return null
}

const isWeighted = election?.census.weight !== election?.census.size

Expand Down Expand Up @@ -243,14 +246,12 @@ export const VoteButton = ({ setQuestionsTab, ...props }: { setQuestionsTab: ()
}}
</ConnectButton.Custom>
)}
{census?.type === 'spreadsheet' && !connected && <SpreadsheetAccess />}
{isAbleToVote && (
<>
<CVoteButton
w='60%'
fontSize='lg'
height='50px'
onClick={setQuestionsTab}
mb={4}
sx={{
'&::disabled': {
Expand Down
256 changes: 256 additions & 0 deletions src/components/Process/Chained.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { Box, Progress } from '@chakra-ui/react'
import { ConnectButton } from '@rainbow-me/rainbowkit'
import { ElectionQuestions, ElectionResults, SpreadsheetAccess } from '@vocdoni/chakra-components'
import { ElectionProvider, useElection } from '@vocdoni/react-providers'
import { ArchivedElection, InvalidElection, IVotePackage, PublishedElection, VocdoniSDKClient } from '@vocdoni/sdk'
import { useEffect, useState } from 'react'
import { Trans } from 'react-i18next'
import { VoteButton } from './Aside'
import { ChainedProvider, useChainedProcesses } from './ChainedContext'
import { ConfirmVoteModal } from './ConfirmVoteModal'

type ChainedProcessesProps = {
root?: PublishedElection | ArchivedElection | InvalidElection
}

const ChainedProcessesInner = () => {
const { election, voted, setClient } = useElection()
const { processes, client, current, setProcess, setCurrent, root } = useChainedProcesses()

// ensure the client is set to the root one
useEffect(() => {
setClient(client)
}, [client, election])

// fetch current process and process flow logic
useEffect(() => {
if (!current || processes[current] instanceof InvalidElection || !voted) return

const currentElection = processes[current]
const meta = currentElection.get('multiprocess')
if (!meta || (!meta.root && !meta.conditions && !meta.default)) return
;(async () => {
// fetch votes info
const next = await getNextProcessInFlow(client, voted, meta)

if (typeof processes[next] === 'undefined') {
const election = await client.fetchElection(next)
setProcess(next, election)
setCurrent(next)
}
})()
}, [processes, current, voted, client])

if (!current || !processes[current]) {
return <Progress w='full' size='xs' isIndeterminate />
}

if (processes[current] instanceof InvalidElection) {
return <Trans i18nKey='error.election_is_invalid'>Invalid election</Trans>
}

return (
<Box className='md-sizes' mb='100px' pt='25px'>
<ElectionQuestions
confirmContents={(election, answers) => <ConfirmVoteModal election={election} answers={answers} />}
/>
<Box position='sticky' bottom={0} left={0} pb={1} pt={1} display={{ base: 'none', lg2: 'block' }}>
<VoteButton />
</Box>
</Box>
)
}

export const ChainedProcesses = ({ root }: ChainedProcessesProps) => {
const { client } = useElection()
if (!root) {
return <Progress w='full' size='xs' isIndeterminate />
}

return (
<ChainedProvider root={root as PublishedElection} client={client}>
<ChainedProcessesWrapper />
</ChainedProvider>
)
}

export const ChainedResults = ({ root }: ChainedProcessesProps) => {
const { client } = useElection()
if (!root) {
return <Progress w='full' size='xs' isIndeterminate />
}

return (
<ChainedProvider root={root as PublishedElection} client={client}>
<ChainedResultsWrapper />
</ChainedProvider>
)
}

const ChainedProcessesWrapper = () => {
// note election context refers to the root election here, ALWAYS
const { connected, election } = useElection()
const { processes, current, root, setCurrent } = useChainedProcesses()

// set current to root if login out
useEffect(() => {
if (connected) return

setCurrent(root.id)
}, [connected])

if (!current || !processes[current] || !election || election instanceof InvalidElection) {
return <Progress w='full' size='xs' isIndeterminate />
}

return (
<>
<ElectionProvider key={current} election={processes[current]} ConnectButton={ConnectButton} fetchCensus>
<ChainedProcessesInner />
</ElectionProvider>
{!connected && election.get('census.type') === 'spreadsheet' && <SpreadsheetAccess />}
</>
)
}

const ChainedResultsWrapper = () => {
// note election context refers to the root election here, ALWAYS
const { election, client } = useElection()
const { processes, setProcess } = useChainedProcesses()
const [loaded, setLoaded] = useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(false)
const [sorted, setSorted] = useState<string[]>([])

useEffect(() => {
if (!election || election instanceof InvalidElection || loading || loaded) return
setLoading(true)
;(async () => {
const { processes: fetchedProcesses, ids } = await getAllProcessesInFlow(client, election)
for (const process of fetchedProcesses) {
setProcess(process.id, process)
}
setSorted(ids)
setLoaded(true)
setLoading(false)
})()
}, [election])

if (!loaded) {
return <Progress w='full' size='xs' isIndeterminate />
}

return (
<>
{sorted.map((id) => (
<ElectionProvider key={id} election={processes[id]}>
<ElectionResults />
</ElectionProvider>
))}
</>
)
}

// Note this logic has been thought for single-choice single-question processes.
// The brainfuck required to make it work for other implementations requires a
// thoroughful assessment of the entire feature.
// Also, processes cannot be secret... otherwise we cannot get the vote packages
// and know what the users voted
const getNextProcessInFlow = async (client: VocdoniSDKClient, voted: string, meta: any) => {
if (!meta.conditions) {
return meta.default
}

const ivote = await client.voteService.info(voted)

if (!(ivote.package as IVotePackage).votes) {
throw new Error('vote package is secret, cannot continue with the flow')
}

const [choice] = (ivote.package as IVotePackage).votes

// loop over conditions finding the selected choice
for (const condition of meta.conditions) {
if (choice === condition.choice && condition.question === 0) {
return condition.goto
}
}

return meta.default
}

const getProcessIdsInFlowStep = (meta: FlowNode) => {
const ids: string[] = []

ids.push(meta.default)

if (!meta.conditions) {
return ids
}

for (const condition of meta.conditions) {
ids.push(condition.goto)
}

return ids
}

export const getAllProcessesInFlow = async (
client: VocdoniSDKClient,
election: PublishedElection
): Promise<{ processes: PublishedElection[]; ids: string[] }> => {
const processes: { [key: string]: PublishedElection } = {}
const ids: string[] = []
const visited = new Set<string>()

const loadProcess = async (id: string) => {
if (!processes[id]) {
const election = await client.fetchElection(id)
processes[id] = election

const meta = election.get('multiprocess')
if (meta && meta.default && !visited.has(meta.default)) {
const idsToFetch = getProcessIdsInFlowStep(meta)
for (const nextId of idsToFetch) {
await loadProcess(nextId)
}

// Add conditions first
if (meta.conditions) {
for (const condition of meta.conditions) {
if (!visited.has(condition.goto)) {
visited.add(condition.goto)
ids.push(condition.goto)
}
}
}

// Add defaults after conditions
if (!visited.has(meta.default)) {
visited.add(meta.default)
ids.push(meta.default)
}
}
}
}

const meta = election.get('multiprocess')
const initialIds = [election.id, ...getProcessIdsInFlowStep(meta)]
for (const id of initialIds) {
await loadProcess(id)
}

ids.push(election.id)

return { processes: Object.values(processes), ids: ids.reverse() }
}

type FlowCondition = {
question: number
choice: number
goto: string
}

type FlowNode = {
default: string
conditions?: FlowCondition[]
}
48 changes: 48 additions & 0 deletions src/components/Process/ChainedContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { PublishedElection, VocdoniSDKClient } from '@vocdoni/sdk'
import { createContext, FC, PropsWithChildren, useContext, useState } from 'react'

type Processes = {
[key: string]: PublishedElection
}

type ChainedContextState = {
processes: Processes
current: string | null
client: VocdoniSDKClient
root: PublishedElection
setProcess: (id: string, process: PublishedElection) => void
setCurrent: (id: string | null) => void
}

type ChainedProviderProps = {
client: VocdoniSDKClient
root: PublishedElection
}

const ChainedContext = createContext<ChainedContextState | undefined>(undefined)

export const ChainedProvider: FC<PropsWithChildren<ChainedProviderProps>> = ({ children, root, client }) => {
const [processes, setProcesses] = useState<Processes>(root ? { [root.id]: root } : {})
const [current, setCurrent] = useState<string | null>(root ? root.id : null)

const setProcess = (id: string, process: PublishedElection) => {
setProcesses((prev) => ({
...prev,
[id]: process,
}))
}

return (
<ChainedContext.Provider value={{ processes, client, current, root, setProcess, setCurrent }}>
{children}
</ChainedContext.Provider>
)
}

export const useChainedProcesses = () => {
const context = useContext(ChainedContext)
if (!context) {
throw new Error('useProcesses must be used within a ProcessesProvider')
}
return context
}
Loading

0 comments on commit eca37d6

Please sign in to comment.