Skip to content

Commit fe85d4c

Browse files
authored
Merge pull request #1438 from ArendPeter/sandcastle/issue-1436
Remove WizardExtra (#1405)
2 parents 15289d3 + 5e82545 commit fe85d4c

8 files changed

Lines changed: 188 additions & 311 deletions

File tree

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/frontend/src/components/ConfirmationDialogProvider.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import {
55
useRef,
66
useState,
77
} from 'react';
8-
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'
8+
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, IconButton } from '@mui/material'
9+
import CloseIcon from '@mui/icons-material/Close';
910
import { PrimaryButton, SecondaryButton } from './styles';
1011
import { useSubstitutedTranslation } from './util';
1112
// Built from this example buth with MUI dialogs: https://akashhamirwasia.com/blog/building-expressive-confirm-dialog-api-in-react/
1213
// Uses a context provider to allow any component to access a confirm dialog component using the useConfirm hook
13-
// Example:
14+
// Example:
1415
// import useConfirm from '../../ConfirmationDialogProvider';
1516
// const confirm = useConfirm()
1617
// const confirmed = await confirm(
1718
// {
18-
// title: 'Confirm This Action',
19+
// title: 'Confirm This Action',
1920
// message: "Are you sure you want to do this?"
2021
// })
2122

@@ -24,26 +25,27 @@ interface ConfirmData {
2425
message: string
2526
cancel?: string
2627
submit?: string
28+
dismissable?: boolean
2729
}
2830

29-
type confirmContext = (data: ConfirmData) => Promise<boolean>
31+
type confirmContext = (data: ConfirmData) => Promise<boolean | null>
3032

3133
const ConfirmDialog = createContext<confirmContext>(null);
3234

3335
export function ConfirmDialogProvider({ children }: { children: React.ReactNode }) {
34-
const [state, setState] = useState({ isOpen: false, title: '', message: '', submit: null, cancel: null});
36+
const [state, setState] = useState({ isOpen: false, title: '', message: '', submit: null, cancel: null, dismissable: false });
3537
// eslint-disable-next-line @typescript-eslint/no-unused-vars
36-
const fn = useRef((choice: boolean) => { });
38+
const fn = useRef((choice: boolean | null) => { });
3739

3840
const {t} = useSubstitutedTranslation();
3941

4042
const confirm = useCallback(
4143
(data: ConfirmData) => {
42-
return new Promise((resolve: (value: boolean) => void) => {
43-
setState({ ...state, ...data, isOpen: true });
44+
return new Promise((resolve: (value: boolean | null) => void) => {
45+
setState({ ...state, ...data, isOpen: true, dismissable: data.dismissable ?? false });
4446
fn.current = (choice) => {
4547
resolve(choice);
46-
setState({ isOpen: false, title: '', message: '', submit: null, cancel: null });
48+
setState({ isOpen: false, title: '', message: '', submit: null, cancel: null, dismissable: false });
4749
};
4850
});
4951
},
@@ -56,8 +58,20 @@ export function ConfirmDialogProvider({ children }: { children: React.ReactNode
5658
<Dialog
5759
open={state.isOpen}
5860
fullWidth
61+
onClose={state.dismissable ? () => fn.current(null) : undefined}
5962
>
60-
<DialogTitle>{state.title}</DialogTitle>
63+
<DialogTitle>
64+
{state.title}
65+
{state.dismissable && (
66+
<IconButton
67+
aria-label={t('keyword.close')}
68+
onClick={() => fn.current(null)}
69+
sx={{ position: 'absolute', right: 8, top: 8 }}
70+
>
71+
<CloseIcon />
72+
</IconButton>
73+
)}
74+
</DialogTitle>
6175
<DialogContent>
6276
<DialogContentText>{state.message}</DialogContentText>
6377
</DialogContent>

packages/frontend/src/components/ElectionForm/Races/RaceForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ const TitleAndDescription = ({setErrors, errors, editedRace, applyRaceUpdate, op
343343

344344
<Box>
345345
<UtilityButton onClick={() => setShowDescription(d => !d)}>
346-
{showDescription? '-' : '+'} Description (Optional)
346+
{showDescription? '-' : '+'} {t('race_form.description_title')}
347347
</UtilityButton>
348348
{showDescription && <>
349349
<TextField

packages/frontend/src/components/ElectionForm/Wizard/Wizard.tsx

Lines changed: 73 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
import { useState, useMemo } from 'react';
22
import { useNavigate } from "react-router";
3-
import { PrimaryButton } from '../../styles.js';
4-
import { Box, Breakpoint, Paper, Typography, useMediaQuery } from '@mui/material';
3+
import { PrimaryButton, UtilityButton } from '../../styles.js';
4+
import { Box, Paper, TextField, Typography } from '@mui/material';
55
import { usePostElection } from '~/hooks/useAPI';
66
import { setCookie, useCookie } from '~/hooks/useCookie';
77
import { NewElection } from '@equal-vote/star-vote-shared/domain_model/Election';
88
import { setVoterAuthenticationMode } from '@equal-vote/star-vote-shared/domain_model/VoterAuthenticationMode';
99
import { makeUniqueIDSync, makeID, ID_PREFIXES, ID_LENGTHS } from '@equal-vote/star-vote-shared/utils/makeID';
1010

11-
import { hashString, scrollToElement, StringObject, TransitionBox, useSubstitutedTranslation } from '../../util.js';
11+
import { hashString, TransitionBox, useSubstitutedTranslation } from '../../util.js';
1212
import useAuthSession from '../../AuthSessionContextProvider.js';
1313
import RaceForm from '../Races/RaceForm.js';
1414
import useConfirm from '../../ConfirmationDialogProvider.js';
15-
import WizardExtra from './WizardExtra.js';
16-
import { ElectionContextProvider } from '../../ElectionContextProvider.js';
15+
import useElection, { ElectionContextProvider } from '../../ElectionContextProvider.js';
1716
import WizardBasics from './WizardBasics.js';
18-
import { useTheme } from '@mui/material';
1917

2018
export const makeDefaultElection = () => {
2119
const ids = [];
2220
for(let i = 0; i < 1; i++){
2321
ids.push(makeUniqueIDSync(
24-
ID_PREFIXES.CANDIDATE,
22+
ID_PREFIXES.CANDIDATE,
2523
ID_LENGTHS.CANDIDATE,
2624
(id: string) => ids.includes(id)
2725
));
@@ -34,7 +32,7 @@ export const makeDefaultElection = () => {
3432
owner_id: '0',
3533
is_public: false,
3634
ballot_source: 'live_election',
37-
races: [ {
35+
races: [ {
3836
title: '',
3937
race_id: '0',
4038
num_winners: undefined,
@@ -46,7 +44,7 @@ export const makeDefaultElection = () => {
4644
precincts: undefined,
4745
} ],
4846
settings: {
49-
voter_access: undefined, // the wizard is responsible for setting this one
47+
voter_access: undefined, // onCustomize is responsible for setting this
5048
voter_authentication: {
5149
voter_id: true,
5250
},
@@ -60,13 +58,55 @@ export const makeDefaultElection = () => {
6058
} as NewElection
6159
};
6260

61+
const MultiRaceTitleSection = ({ onCustomize }: { onCustomize: (election: NewElection) => Promise<void> }) => {
62+
const { election, updateElection, t } = useElection();
63+
const [showDescription, setShowDescription] = useState(false);
64+
65+
return (
66+
<Box sx={{ textAlign: 'left', pl: 1 }}>
67+
<Typography variant='h6'>{t('election_details.title')}</Typography>
68+
<TextField
69+
required
70+
label={t('election_details.title')}
71+
value={election.title}
72+
fullWidth
73+
sx={{ mt: 1, mb: 1, boxShadow: 2 }}
74+
onChange={(e) => updateElection(el => { el.title = e.target.value })}
75+
slotProps={{ htmlInput: { 'aria-label': 'Title' } }}
76+
/>
77+
<UtilityButton onClick={() => setShowDescription(d => !d)}>
78+
{showDescription ? '-' : '+'} {t('wizard.description_title')}
79+
</UtilityButton>
80+
{showDescription && (
81+
<TextField
82+
multiline
83+
fullWidth
84+
label="Description"
85+
value={election.description ?? ''}
86+
minRows={3}
87+
sx={{ mt: 1, mb: 1, boxShadow: 2 }}
88+
onChange={(e) => updateElection(el => { el.description = e.target.value })}
89+
/>
90+
)}
91+
<Typography sx={{ mt: 1 }}>{t('wizard.add_races_later')}</Typography>
92+
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'row', justifyContent: 'flex-end', gap: 1 }}>
93+
<PrimaryButton
94+
disabled={!election.title.trim()}
95+
onClick={() => onCustomize(election)}
96+
>
97+
Next
98+
</PrimaryButton>
99+
</Box>
100+
</Box>
101+
);
102+
};
103+
63104
const Wizard = () => {
64105
const authSession = useAuthSession();
65106
const defaultTempId = useMemo(() => makeID(ID_PREFIXES.VOTER, ID_LENGTHS.VOTER), []);
66107
const [tempID] = useCookie('temp_id', defaultTempId);
67108
const navigate = useNavigate()
68-
const [page, setPage] = useState(0);
69-
const { isPending, makeRequest: postElection } = usePostElection()
109+
const { makeRequest: postElection } = usePostElection()
70110
const [election, setElection] = useState<NewElection>(makeDefaultElection())
71111
const [multiRace, setMultiRace] = useState(undefined);
72112

@@ -97,15 +137,13 @@ const Wizard = () => {
97137
navigate(`/${newElection.election.election_id}${subPage}`)
98138
}
99139

100-
const theme = useTheme();
101-
102-
const width: StringObject = {xs: '300px', sm: '500px'};
103-
const getWidth = () => {
104-
const keys: Breakpoint[] = ['sm', 'xs']; // biggest to smallest, must match width keys
105-
// NOTE: I'm precomputing ups so that we don't get an error for variable number of hooks
106-
const ups = keys.map(key => useMediaQuery(theme.breakpoints.up(key), {noSsr: true}));
107-
return Number(width[keys.find((_, i) => ups[i])].replace('px', ''));
108-
}
140+
const onCustomize = async (electionToSubmit: NewElection) => {
141+
const finalElection = {
142+
...electionToSubmit,
143+
settings: setVoterAuthenticationMode(electionToSubmit.settings, 'open_unique_cookie'),
144+
};
145+
await onAddElection(finalElection, '/admin/build_ballot');
146+
};
109147

110148
const onNext = async (editedRace) => {
111149
const updatedElection = {
@@ -114,23 +152,25 @@ const Wizard = () => {
114152
title: editedRace.title,
115153
description: editedRace.description,
116154
}
117-
const confirmed = await confirm(t('wizard.publish_confirm'));
155+
const confirmed = await confirm({...t('wizard.publish_confirm'), dismissable: true});
156+
if (confirmed === null) {
157+
return; // dialog dismissed — stay on wizard with inputs intact
158+
}
118159
if (confirmed) {
119160
onAddElection({...updatedElection, owner_id: null, state: 'finalized', settings: setVoterAuthenticationMode(updatedElection.settings, 'open_unique_cookie')}, '/')
120-
}else{
121-
scrollToElement(document.querySelector('.wizard'));
122-
setElection(updatedElection)
123-
setPage(1);
161+
} else {
162+
await onCustomize(updatedElection);
124163
}
125164
}
126165

166+
const width = {xs: '300px', sm: '500px'};
167+
127168
const pageSX = {
128169
display: 'flex',
129170
gap: 0,
130171
width: width,
131172
flexDirection: 'column',
132173
textAlign: 'center',
133-
//backgroundColor: //'lightShade.main',
134174
padding: 3,
135175
borderRadius: '20px',
136176
minWidth: {xs: '0px', md: '400px'},
@@ -142,32 +182,17 @@ const Wizard = () => {
142182

143183
return <ElectionContextProvider id={undefined} localElection={election} setLocalElection={setElection}>
144184
<Paper className='wizard' elevation={5} sx={{
145-
//maxWidth: '613px',
146185
width: width,
147186
margin: 'auto',
148187
overflow: 'clip',
149188
}}>
150-
<Box
151-
sx={{
152-
position: 'relative',
153-
width: `${getWidth()*2}px`,
154-
left: `-${page*getWidth()}px`,
155-
transition: 'left 1s',
156-
display: 'flex',
157-
flexDirection: 'row',
158-
}}
159-
>
160-
<Box sx={pageSX}>
161-
<Typography variant='h5' sx={{ color: 'lightShade.contrastText' }}>{t('wizard.title')}</Typography>
162-
<WizardBasics multiRace={multiRace} setMultiRace={setMultiRace}/>
163-
<Box sx={{position: 'relative'}}>
164-
<TransitionBox absolute enabled={multiRace === true} sx={{textAlign: 'left', pl: 1}}>
165-
{t('wizard.add_races_later')}
166-
<Box sx={{ mt: 3, display: "flex", flexDirection: "row", justifyContent: "flex-end", gap: 1 }}>
167-
<PrimaryButton onClick={() => setPage(1)}>Next</PrimaryButton>
168-
</Box>
169-
</TransitionBox>
170-
</Box>
189+
<Box sx={pageSX}>
190+
<Typography variant='h5' color={'lightShade.contrastText'}>{t('wizard.title')}</Typography>
191+
<WizardBasics multiRace={multiRace} setMultiRace={setMultiRace}/>
192+
<Box sx={{ position: 'relative' }}>
193+
<TransitionBox absolute enabled={multiRace === true}>
194+
<MultiRaceTitleSection onCustomize={onCustomize} />
195+
</TransitionBox>
171196
<TransitionBox enabled={multiRace === false}>
172197
<RaceForm
173198
raceIndex={0}
@@ -176,9 +201,6 @@ const Wizard = () => {
176201
/>
177202
</TransitionBox>
178203
</Box>
179-
<Box sx={{...pageSX, textAlign: 'left'}}>
180-
<WizardExtra onBack={() => setPage(pg => pg-1)} multiRace={multiRace} onAddElection={onAddElection}/>
181-
</Box>
182204
</Box>
183205
</Paper>
184206
</ElectionContextProvider>

0 commit comments

Comments
 (0)