diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 26d93056..71b8f5af 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,9 +6,7 @@ import Header from './components/Header' import Elections from './components/Elections' import Login from './components/Login' import CreateElectionTemplates from './components/ElectionForm/CreateElectionTemplates' -// import AddElection from './components/ElectionForm/AddElection' import Election from './components/Election/Election' -import DuplicateElection from './components/ElectionForm/DuplicateElection' import Sandbox from './components/Sandbox' import DebugPage from './components/DebugPage' import LandingPage from './components/LandingPage' @@ -58,7 +56,6 @@ const App = () => { } /> } /> } /> - } /> } /> diff --git a/frontend/src/components/Election/Admin/Admin.tsx b/frontend/src/components/Election/Admin/Admin.tsx index 69743c8d..ed475496 100644 --- a/frontend/src/components/Election/Admin/Admin.tsx +++ b/frontend/src/components/Election/Admin/Admin.tsx @@ -9,7 +9,7 @@ const Admin = ({ authSession, election, permissions, fetchElection }) => { return ( - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Election/Admin/AdminHome.tsx b/frontend/src/components/Election/Admin/AdminHome.tsx index 422c6246..c03a1e00 100644 --- a/frontend/src/components/Election/Admin/AdminHome.tsx +++ b/frontend/src/components/Election/Admin/AdminHome.tsx @@ -3,16 +3,18 @@ import Grid from "@mui/material/Grid"; import { Box, Divider, Paper } from "@mui/material"; import { Typography } from "@mui/material"; import { StyledButton } from "../../styles"; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { Election } from '../../../../../domain_model/Election'; import ShareButton from "../ShareButton"; -import { useArchiveEleciton, useFinalizeEleciton, useSetPublicResults } from "../../../hooks/useAPI"; +import { useArchiveEleciton, useFinalizeEleciton, usePostElection, useSetPublicResults } from "../../../hooks/useAPI"; import { formatDate } from '../../util'; import useConfirm from '../../ConfirmationDialogProvider'; import useElection from '../../ElectionContextProvider'; import ElectionDetailsInlineForm from '../../ElectionForm/Details/ElectionDetailsInlineForm'; import Races from '../../ElectionForm/Races/Races'; import ElectionSettings from '../../ElectionForm/ElectionSettings'; +import structuredClone from '@ungap/structured-clone'; +import { IAuthSession } from '../../../hooks/useAuthSession'; const hasPermission = (permissions: string[], requiredPermission: string) => { return (permissions && permissions.includes(requiredPermission)) } @@ -27,7 +29,7 @@ const Section = ({ Description, Button }: SectionProps) => { return ( <> - + {Description} @@ -39,7 +41,7 @@ const Section = ({ Description, Button }: SectionProps) => { } const EditRolesSection = ({ election, permissions }: { election: Election, permissions: string[] }) => { - if(process.env.REACT_APP_FF_METHOD_PLURALITY !== 'true') return <>; + if (process.env.REACT_APP_FF_METHOD_PLURALITY !== 'true') return <>; return
@@ -129,48 +131,15 @@ const EditElectionSection = ({ election, permissions }: { election: Election, pe /> } -const AddVotersSection = ({ election, permissions }: { election: Election, permissions: string[] }) => { +const VotersSection = ({ election, permissions }: { election: Election, permissions: string[] }) => { return
- Add voters to your election + {election.state === 'draft' ? 'Add voters to your election' : 'View voters'} - Add voters who are approved to vote in your election - - - Voter access must be set to closed - - {!hasPermission(permissions, 'canViewElectionRoll') && - - You do not have the correct permissions for this action - - } - )} - Button={(<> - - - Add voters - - - - )} - /> -} - -const ViewVotersSection = ({ election, permissions }: { election: Election, permissions: string[] }) => { - return
- - View Voters + {election.state === 'draft' ? 'Add voters who are approved to vote in your election' : 'View the status of your voters'} {!hasPermission(permissions, 'canViewElectionRoll') && @@ -187,7 +156,7 @@ const ViewVotersSection = ({ election, permissions }: { election: Election, perm component={Link} to={`/Election/${election.election_id}/admin/voters`} > - View voters + {election.state === 'draft' ? 'Add voters' : 'View voters'} @@ -224,7 +193,7 @@ const PreviewBallotSection = ({ election, permissions }: { election: Election, p /> } -const DuplicateElectionSection = ({ election, permissions }: { election: Election, permissions: string[] }) => { +const DuplicateElectionSection = ({ election, permissions, duplicateElection }: { election: Election, permissions: string[], duplicateElection: Function }) => { return
@@ -241,13 +210,12 @@ const DuplicateElectionSection = ({ election, permissions }: { election: Electio variant='contained' disabled={!hasPermission(permissions, 'canEditElectionState')} fullwidth - component={Link} to={`/DuplicateElection/${election.election_id}`} + onClick={() => duplicateElection()} > Duplicate - )} /> } @@ -356,7 +324,128 @@ const ShareSection = ({ election, permissions }: { election: Election, permissio /> } -const AdminHome = () => { +const HeaderSection = ({ election, permissions }: { election: Election, permissions: string[] }) => { + return ( + <> + {election.state === 'finalized' && + <> + + + Your election is finalized + + + {election.settings.invitation && + + + Invitations have been sent to your voters + + + } + {election.start_time && + + + {`Your election will open on ${formatDate(election.start_time, election.settings.time_zone)}`} + + } + + } + {election.state === 'open' && + <> + + + Your election is open + + + {election.settings.invitation && + + + Invitations have been sent to your voters + + + } + {election.end_time && + + + {`Your election will end on ${formatDate(election.end_time, election.settings.time_zone)}`} + + } + + } + {election.state === 'closed' && + <> + + + Your election is closed + + + {election.end_time && + + + {`Your election ended on ${formatDate(election.end_time, election.settings.time_zone)}`} + + } + + } + {election.state === 'archived' && + <> + + + This election has been archived + + + {election.end_time && + + + {`Your election ended on ${formatDate(election.end_time, election.settings.time_zone)}`} + + } + + } + + + ) +} + +const FinalizeSection = ({ election, permissions, finalizeElection }: { election: Election, permissions: string[], finalizeElection: Function }) => { + return ( + <> + + + {/* {`If you're finished setting up your election you can finalize it. This will prevent future edits ${election.settings.invitation ? ', send out invitations, ' : ''} and open the election for voters to submit ballots${election.start_time ? ' after your specified start time' : ''}.`} */} + {`When finished setting up your election, finalize it. Once final, it can't be edited. Voting begins ${election.start_time ? 'after your specified start time.' : 'immediately.'}`} + + {election.settings.invitation && + + Invitations will be sent to your voters + + } + {!hasPermission(permissions, 'canEditElectionState') && + + You do not have the correct permissions for this action + + } + + + finalizeElection()} + > + + Finalize Election + + + + ) +} + + +type Props = { + authSession: IAuthSession} + +const AdminHome = ({authSession}:Props) => { const { election, refreshElection: fetchElection, permissions } = useElection() const { makeRequest } = useSetPublicResults(election.election_id) const togglePublicResults = async () => { @@ -367,13 +456,16 @@ const AdminHome = () => { const { makeRequest: finalize } = useFinalizeEleciton(election.election_id) const { makeRequest: archive } = useArchiveEleciton(election.election_id) + const navigate = useNavigate() + const { error, isPending, makeRequest: postElection } = usePostElection() + const confirm = useConfirm() const finalizeElection = async () => { console.log("finalizing election") -const confirmed = await confirm( + const confirmed = await confirm( { - title: 'Confirm Finalize Election', + title: 'Confirm Finalize Election', message: "Are you sure you want to finalize your election? Once finalized you won't be able to edit it." }) if (!confirmed) return @@ -389,18 +481,44 @@ const confirmed = await confirm( console.log("archiving election") const confirmed = await confirm( { - title: 'Confirm Archive Election', + title: 'Confirm Archive Election', message: "Are you sure you wish to archive this election? This action cannot be undone." }) if (!confirmed) return - console.log('confirmed') - try { - await archive() - await fetchElection() - } catch (err) { - console.log(err) - } + console.log('confirmed') + try { + await archive() + await fetchElection() + } catch (err) { + console.log(err) + } } + const duplicateElection = async () => { + console.log("duplicating election") + const confirmed = await confirm( + { + title: 'Confirm Duplicate Election', + message: "Are you sure you wish to duplicate this election?" + }) + if (!confirmed) return + console.log('confirmed') + const copiedElection = structuredClone(election) + copiedElection.title = 'Copy of ' + copiedElection.title + copiedElection.frontend_url = '' + copiedElection.owner_id = authSession.getIdField('sub') + copiedElection.state = 'draft' + + const newElection = await postElection( + { + Election: copiedElection, + }) + + if ((!newElection)) { + throw Error("Error submitting election"); + } + navigate(`/Election/${newElection.election.election_id}/admin`) + } + return ( - - - - - - - - + + + + + + + + + + + + + + + {(election.settings.voter_access === 'closed' || election.state !== 'draft') && <> + + + } + {(election.state !== 'draft' && election.state !== 'finalized') && <> + + + + + + + + + } + + + + + + + {election.state === 'draft' && - + + - {election.state === 'draft' && - <> - - - - - - - {/* - */} - - - - - - - {/* {`If you're finished setting up your election you can finalize it. This will prevent future edits ${election.settings.invitation ? ', send out invitations, ' : ''} and open the election for voters to submit ballots${election.start_time ? ' after your specified start time' : ''}.`} */} - {`When finished setting up your election, finalize it. Once final, it can't be edited. Voting begins ${election.start_time ? 'after your specified start time.' : 'immediately.'}`} - - {election.settings.invitation && - - Invitations will be sent to your voters - - } - {!hasPermission(permissions, 'canEditElectionState') && - - You do not have the correct permissions for this action - - } - - - finalizeElection()} - > - - Finalize Election - - - - - - } - {election.state === 'finalized' && - <> - - - Your election is finalized - - - {election.settings.invitation && - - - Invitations have been sent to your voters - - - } - {election.start_time && - - - {`Your election will open on ${formatDate(election.start_time, election.settings.time_zone)}`} - - } - - - - - - - - - - - - - - - } - - {election.state === 'open' && - <> - - - Your election is open - - - {election.settings.invitation && - - - Invitations have been sent to your voters - - - } - {election.end_time && - - - {`Your election will end on ${formatDate(election.end_time, election.settings.time_zone)}`} - - } - - - - - - - - - - - - - - - - - - - } - {election.state === 'closed' && - <> - - - Your election is closed - - - {election.end_time && - - - {`Your election ended on ${formatDate(election.end_time, election.settings.time_zone)}`} - - } - - - - - - - - - - - - - - - - - } - {election.state === 'archived' && - <> - - - This election has been archived - - - {election.end_time && - - - {`Your election ended on ${formatDate(election.end_time, election.settings.time_zone)}`} - - } - - - - - - - - - - - } - - + } + ) } diff --git a/frontend/src/components/Election/Sidebar.tsx b/frontend/src/components/Election/Sidebar.tsx index 359fab28..c6adb0ec 100644 --- a/frontend/src/components/Election/Sidebar.tsx +++ b/frontend/src/components/Election/Sidebar.tsx @@ -35,15 +35,11 @@ export default function Sidebar({ electionData }) { {electionData.election.state === 'draft' && <> - {process.env.REACT_APP_FF_METHOD_ELECTION_ROLES === 'true' && } - - - } diff --git a/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx b/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx index b622f749..9cb436bf 100644 --- a/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx +++ b/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx @@ -56,6 +56,7 @@ export default function ElectionDetailsInlineForm() { diff --git a/frontend/src/components/ElectionForm/DuplicateElection.tsx b/frontend/src/components/ElectionForm/DuplicateElection.tsx deleted file mode 100644 index 4ec73aff..00000000 --- a/frontend/src/components/ElectionForm/DuplicateElection.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import { useParams } from "react-router"; -import Container from '@mui/material/Container'; -import ElectionForm from "./ElectionForm"; -import { useState, useEffect } from "react"; -import { useNavigate } from "react-router" -import { useGetElection, usePostElection } from '../../hooks/useAPI'; - -const DuplicateElection = ({ authSession }) => { - const navigate = useNavigate() - const { id } = useParams(); - - const [data, setData] = useState(null); - - let { data: prevData, isPending, error, makeRequest: fetchElection } = useGetElection(id); - const { isPending: postIsPending, error: postError, makeRequest: postElection } = usePostElection() - useEffect(() => { - fetchElection() - }, []) - - useEffect( - () => { - if (prevData && prevData.election) { - setData({ ...prevData, election: { ...prevData.election, title: `Copy of ${prevData.election.title}` } }); - } - }, [prevData] - ) - - const onCreateElection = async (election) => { - // calls post election api, throws error if response not ok - const newElection = await postElection( - { - Election: election, - }) - if ((!newElection)) { - throw Error("Error submitting election"); - } - - localStorage.removeItem('Election') - navigate(`/Election/${newElection.election.election_id}`) - } - - return ( - - {isPending &&
Loading Election...
} - {!authSession.isLoggedIn() &&
Must be logged in to create elections
} - {authSession.isLoggedIn() && data && data.election && - - } - {postIsPending &&
Submitting...
} - {postError &&
{postError}
} -
- ) -} - -export default DuplicateElection diff --git a/frontend/src/components/ElectionForm/Races/AddRace.tsx b/frontend/src/components/ElectionForm/Races/AddRace.tsx index bee1369d..782b5ac6 100644 --- a/frontend/src/components/ElectionForm/Races/AddRace.tsx +++ b/frontend/src/components/ElectionForm/Races/AddRace.tsx @@ -31,7 +31,8 @@ export default function AddRace() { variant="contained" fullWidth={false} sx={{ borderRadius: 28, backgroundColor: 'brand.green' }} - onClick={handleOpen}> + onClick={handleOpen} + disabled={election.state!=='draft'}> Add diff --git a/frontend/src/components/ElectionForm/Races/Race.tsx b/frontend/src/components/ElectionForm/Races/Race.tsx index 3f7ae727..02d475ab 100644 --- a/frontend/src/components/ElectionForm/Races/Race.tsx +++ b/frontend/src/components/ElectionForm/Races/Race.tsx @@ -5,12 +5,15 @@ import { Box, Paper } from "@mui/material" import IconButton from '@mui/material/IconButton' import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; +import VisibilityIcon from '@mui/icons-material/Visibility'; import RaceDialog from './RaceDialog'; import { useEditRace } from './useEditRace'; import RaceForm from './RaceForm'; +import useElection from '../../ElectionContextProvider'; export default function Race({ race, race_index }) { + const { election } = useElection() const { editedRace, errors, setErrors, applyRaceUpdate, onSaveRace, onDeleteRace } = useEditRace(race, race_index) const [open, setOpen] = useState(false); @@ -36,7 +39,7 @@ export default function Race({ race, race_index }) { - + {election.state==='draft' ? : }
@@ -44,7 +47,8 @@ export default function Race({ race, race_index }) { + onClick={onDeleteRace} + disabled={election.state!=='draft'}> diff --git a/frontend/src/components/ElectionForm/Races/RaceDialog.tsx b/frontend/src/components/ElectionForm/Races/RaceDialog.tsx index 374c4865..70006e66 100644 --- a/frontend/src/components/ElectionForm/Races/RaceDialog.tsx +++ b/frontend/src/components/ElectionForm/Races/RaceDialog.tsx @@ -2,10 +2,12 @@ import React from 'react' import { Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material" import { StyledButton } from '../../styles'; +import useElection from '../../ElectionContextProvider'; export default function RaceDialog({ onSaveRace, open, handleClose, children }) { + const { election } = useElection() const handleSave = () => { onSaveRace() } @@ -39,7 +41,8 @@ export default function RaceDialog({ onSaveRace, open, handleClose, children }) type='button' variant="contained" fullWidth={false} - onClick={() => handleSave()}> + onClick={() => handleSave()} + disabled={election.state!=='draft'}> Save diff --git a/frontend/src/components/ElectionForm/Races/Races.tsx b/frontend/src/components/ElectionForm/Races/Races.tsx index 7c11ef50..907c4d2c 100644 --- a/frontend/src/components/ElectionForm/Races/Races.tsx +++ b/frontend/src/components/ElectionForm/Races/Races.tsx @@ -6,7 +6,7 @@ import Race from './Race'; import AddRace from './AddRace'; export default function Races() { - const { election, refreshElection, permissions, updateElection } = useElection() + const { election } = useElection() return (