Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding confirmation dialog provider #373

Merged
merged 2 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import { Alert, Box, CssBaseline, Snackbar } from '@mui/material'
import { useAuthSession } from './hooks/useAuthSession'
import { Isnack, SnackbarContext } from './components/SnackbarContext'
import Footer from './components/Footer'
import { ConfirmDialogProvider } from './components/ConfirmationDialogProvider'
import About from './components/About'

const App = () => {
const authSession = useAuthSession()
const [snack, setSnack] = useState({
Expand All @@ -33,6 +35,7 @@ const App = () => {
return (
<Router>
<ThemeProvider theme={theme}>
<ConfirmDialogProvider>
<SnackbarContext.Provider value={{ snack, setSnack }}>
<Snackbar open={snack.open} autoHideDuration={snack.autoHideDuration} onClose={handleClose} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}>
<Alert severity={snack.severity} onClose={handleClose}>
Expand Down Expand Up @@ -61,6 +64,7 @@ const App = () => {
<Footer />
</Box>
</SnackbarContext.Provider>
</ConfirmDialogProvider>
</ThemeProvider>
</Router>
);
Expand Down
84 changes: 84 additions & 0 deletions frontend/src/components/ConfirmationDialogProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, {
createContext,
useCallback,
useContext,
useRef,
useState,
} from 'react';
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'
import { StyledButton } from './styles';
// Built from this example buth with MUI dialogs: https://akashhamirwasia.com/blog/building-expressive-confirm-dialog-api-in-react/
// Uses a context provider to allow any component to access a confirm dialog component using the useConfirm hook
// Example:
// import useConfirm from '../../ConfirmationDialogProvider';
// const confirm = useConfirm()
// const confirmed = await confirm(
// {
// title: 'Confirm This Action',
// message: "Are you sure you want to do this?"
// })

interface ConfirmData {
title: string
message: string
}

type confirmContext = (data: ConfirmData) => Promise<boolean>

const ConfirmDialog = createContext<confirmContext>(null);

export function ConfirmDialogProvider({ children }) {


const [state, setState] = useState({ isOpen: false, title: '', message: '' });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

One thing we need to be careful of with this pattern is, manipulating state (like state.isOpen = true), won't trigger a refresh. Only reinstantiating the object with trigger a refresh

Looks like that's what's happening here, and I see that it makes the code cleaner, but I wanted to mention it anyway so that we're careful with it in the future

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the setState function in the callback should reinstate the object though right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it's being reinstantiated in the callback, so it's fine

const fn = useRef((choice: boolean) => { });

const confirm = useCallback(
(data: ConfirmData) => {
return new Promise((resolve: (value: boolean) => void) => {
setState({ ...data, isOpen: true });
fn.current = (choice) => {
resolve(choice);
setState({ isOpen: false, title: '', message: '' });
};
});
},
[setState]
);

return (
<ConfirmDialog.Provider value={confirm}>
{children}
<Dialog
open={state.isOpen}
fullWidth
>
<DialogTitle>{state.title}</DialogTitle>
<DialogContent>
<DialogContentText>{state.message}</DialogContentText>
</DialogContent>
<DialogActions>
<StyledButton
type='button'
variant="contained"
width="100%"
fullWidth={false}
onClick={() => fn.current(false)}>
Cancel
</StyledButton>
<StyledButton
type='button'
variant="contained"
fullWidth={false}
onClick={() => fn.current(true)}>
Yes
</StyledButton>
</DialogActions>
</Dialog>
</ConfirmDialog.Provider>
);
}

export default function useConfirm() {
return useContext(ConfirmDialog);
}
30 changes: 22 additions & 8 deletions frontend/src/components/Election/Admin/AdminHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Election } from '../../../../../domain_model/Election';
import ShareButton from "../ShareButton";
import { useArchiveEleciton, useFinalizeEleciton, useSetPublicResults } from "../../../hooks/useAPI";
import { formatDate } from '../../util';
import useConfirm from '../../ConfirmationDialogProvider';
const hasPermission = (permissions: string[], requiredPermission: string) => {
return (permissions && permissions.includes(requiredPermission))
}
Expand Down Expand Up @@ -367,8 +368,17 @@ const AdminHome = ({ election, permissions, fetchElection }: Props) => {
}
const { makeRequest: finalize } = useFinalizeEleciton(election.election_id)
const { makeRequest: archive } = useArchiveEleciton(election.election_id)

const confirm = useConfirm()

const finalizeElection = async () => {
console.log("finalizing election")
const confirmed = await confirm(
{
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
try {
await finalize()
await fetchElection()
Expand All @@ -379,14 +389,18 @@ const AdminHome = ({ election, permissions, fetchElection }: Props) => {

const archiveElection = async () => {
console.log("archiving election")
if (window.confirm('Are you sure you wish to archive this election? This action cannot be undone')){
console.log('confirmed')
try {
await archive()
await fetchElection()
} catch (err) {
console.log(err)
}
const confirmed = await confirm(
{
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)
}
}

Expand Down