diff --git a/web/src/components/FormEditor/FormEditor.tsx b/web/src/components/FormEditor/FormEditor.tsx index 05c14c00f..cf58bf8c9 100644 --- a/web/src/components/FormEditor/FormEditor.tsx +++ b/web/src/components/FormEditor/FormEditor.tsx @@ -21,7 +21,7 @@ import { Button } from '@/components/ui/button'; import { LanguageBadge } from '@/components/ui/language-badge'; import { cn, ensureTranslatedStringCorrectness, isNilOrWhitespace, isNotNilOrWhitespace } from '@/lib/utils'; import { useBlocker } from '@tanstack/react-router'; -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useMemo, useState } from 'react'; import { FormFull } from '../../features/forms/models'; import { @@ -205,7 +205,7 @@ interface FormEditorProps { const FormEditor: FC = ({ hasCitizenReportingOption, formData, onSaveForm }) => { const confirm = useConfirm(); const [navigateAwayAfterSave, setNavigateAwayAfterSave] = useState(false); - const editQuestions = formData?.questions.map((question) => { + const editQuestions = useMemo(() => formData?.questions.map((question) => { if (isNumberQuestion(question)) { const numberQuestion: EditNumberQuestionType = { $questionType: QuestionType.NumberQuestionType, @@ -350,7 +350,7 @@ const FormEditor: FC = ({ hasCitizenReportingOption, formData, } return undefined; - }); + }), [formData]); const form = useForm({ resolver: zodResolver(ZEditFormType), @@ -366,21 +366,16 @@ const FormEditor: FC = ({ hasCitizenReportingOption, formData, icon: formData?.icon ?? '', }, mode: 'all', + reValidateMode: 'onChange', }); - useBlocker({ - shouldBlockFn: async () => { - if (!form.formState.isDirty || form.formState.isSubmitting) { - return false; - } + // Subscribe to isDirty by accessing it in component body + // This ensures React tracks form state changes + const isDirty = form.formState.isDirty; - return await confirm({ - title: `Unsaved Changes Detected`, - body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', - actionButton: 'Leave', - cancelButton: 'Stay', - }); - }, + const { proceed, reset, status } = useBlocker({ + shouldBlockFn: () => isDirty, + withResolver: true, }); const languageCode = useWatch({ @@ -395,6 +390,23 @@ const FormEditor: FC = ({ hasCitizenReportingOption, formData, } }, [form.formState.isSubmitSuccessful, form.reset]); + useEffect(() => { + if (status === 'blocked') { + confirm({ + title: `Unsaved Changes Detected`, + body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', + actionButton: 'Leave', + cancelButton: 'Stay', + }).then((confirmed) => { + if (confirmed) { + proceed(); + } else { + reset(); + } + }); + } + }, [status, confirm, proceed, reset]); + return (
{ + const formQuestions = useMemo(() => formData.questions.map((question) => { if (isNumberQuestion(question)) { const numberQuestion: EditNumberQuestionType = { $questionType: QuestionType.NumberQuestionType, @@ -196,7 +196,7 @@ export default function FormTranslationEditor({ } return undefined; - }); + }), [formData]); const form = useForm({ resolver: zodResolver(ZEditFormType), @@ -223,20 +223,29 @@ export default function FormTranslationEditor({ } }, [form.formState.isSubmitSuccessful, form.reset]); - useBlocker({ - shouldBlockFn: async () => { - if (!form.formState.isDirty || form.formState.isSubmitting) { - return false; - } + const isDirty = form.formState.isDirty; + + const { proceed, reset, status } = useBlocker({ + shouldBlockFn: () => isDirty, + withResolver: true, + }); - return await confirm({ + useEffect(() => { + if (status === 'blocked') { + confirm({ title: `Unsaved Changes Detected`, body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', actionButton: 'Leave', cancelButton: 'Stay', + }).then((confirmed) => { + if (confirmed) { + proceed(); + } else { + reset(); + } }); - }, - }); + } + }, [status, confirm, proceed, reset]); return ( diff --git a/web/src/components/FormTranslationEditor/SelectQuestionTranslationEditor.tsx b/web/src/components/FormTranslationEditor/SelectQuestionTranslationEditor.tsx index 422e342f9..98c6cf09d 100644 --- a/web/src/components/FormTranslationEditor/SelectQuestionTranslationEditor.tsx +++ b/web/src/components/FormTranslationEditor/SelectQuestionTranslationEditor.tsx @@ -76,9 +76,9 @@ function SelectQuestionTranslationEditor({ questionIndex }: SelectQuestionTransl />
diff --git a/web/src/features/election-event/components/Guides/AddGuideForm.tsx b/web/src/features/election-event/components/Guides/AddGuideForm.tsx index 2067a2bfb..a5d3f0fa8 100644 --- a/web/src/features/election-event/components/Guides/AddGuideForm.tsx +++ b/web/src/features/election-event/components/Guides/AddGuideForm.tsx @@ -160,20 +160,29 @@ export default function AddGuideForm({ uploadGuideMutation.mutate({ electionRoundId: currentElectionRoundId, guidePageType, guide }); } - useBlocker({ - shouldBlockFn: async () => { - if (!form.formState.isDirty) { - return false; - } + const isDirty = form.formState.isDirty; + + const { proceed, reset, status } = useBlocker({ + shouldBlockFn: () => isDirty, + withResolver: true, + }); - return await confirm({ + useEffect(() => { + if (status === 'blocked') { + confirm({ title: `Unsaved Changes Detected`, body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', actionButton: 'Leave', cancelButton: 'Stay', + }).then((confirmed) => { + if (confirmed) { + proceed(); + } else { + reset(); + } }); - }, - }); + } + }, [status, confirm, proceed, reset]); useEffect(() => { if (form.formState.isSubmitSuccessful) { diff --git a/web/src/features/election-event/components/Guides/EditGuideForm.tsx b/web/src/features/election-event/components/Guides/EditGuideForm.tsx index 4ea102932..00c1fc33c 100644 --- a/web/src/features/election-event/components/Guides/EditGuideForm.tsx +++ b/web/src/features/election-event/components/Guides/EditGuideForm.tsx @@ -147,20 +147,29 @@ export default function EditGuideForm({ }, }); - useBlocker({ - shouldBlockFn: async () => { - if (!form.formState.isDirty) { - return false; - } + const isDirty = form.formState.isDirty; + + const { proceed, reset, status } = useBlocker({ + shouldBlockFn: () => isDirty, + withResolver: true, + }); - return await confirm({ + useEffect(() => { + if (status === 'blocked') { + confirm({ title: `Unsaved Changes Detected`, body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', actionButton: 'Leave', cancelButton: 'Stay', + }).then((confirmed) => { + if (confirmed) { + proceed(); + } else { + reset(); + } }); - }, - }); + } + }, [status, confirm, proceed, reset]); useEffect(() => { if (form.formState.isSubmitSuccessful) { diff --git a/web/src/features/forms/components/FormEdit/FormEdit.tsx b/web/src/features/forms/components/FormEdit/FormEdit.tsx index 4449008c4..fe782be2e 100644 --- a/web/src/features/forms/components/FormEdit/FormEdit.tsx +++ b/web/src/features/forms/components/FormEdit/FormEdit.tsx @@ -36,14 +36,13 @@ function FormEdit() { }: { electionRoundId: string; form: UpdateFormRequest; - shouldNavigateAwayAfterSubmit: boolean; }) => { return authApi.put(`/election-rounds/${electionRoundId}/forms/${form.id}`, { ...form, }); }, - onSuccess: async (_, { electionRoundId, shouldNavigateAwayAfterSubmit }) => { + onSuccess: async (_, { electionRoundId }) => { toast({ title: 'Success', description: 'Form updated successfully', @@ -51,17 +50,6 @@ function FormEdit() { await queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId), type: 'all' }); router.invalidate(); - - if (shouldNavigateAwayAfterSubmit) { - if ( - await confirm({ - title: 'Changes made to form template in base language', - body: 'Please note that changes have been made to the form in base language, which can impact the translation(s). All new questions or response options which you have added have been copied to translations but in the base language. Access each translation of the form and manually translate each of the changes.', - }) - ) { - await navigate({ to: '/election-event/$tab', params: { tab: 'observer-forms' } }); - } - } }, onError: () => { @@ -74,7 +62,7 @@ function FormEdit() { }); const saveForm = useCallback( - (formData: EditFormType, shouldNavigateAwayAfterSubmit: boolean) => { + async (formData: EditFormType, shouldNavigateAwayAfterSubmit: boolean) => { const updatedForm: UpdateFormRequest = { id: formId, code: formData.code, @@ -87,13 +75,23 @@ function FormEdit() { questions: formData.questions.map(mapToQuestionRequest), }; - updateFormMutation.mutate({ + await updateFormMutation.mutateAsync({ electionRoundId: currentElectionRoundId, form: updatedForm, - shouldNavigateAwayAfterSubmit, }); + + if (shouldNavigateAwayAfterSubmit) { + if ( + await confirm({ + title: 'Changes made to form template in base language', + body: 'Please note that changes have been made to the form in base language, which can impact the translation(s). All new questions or response options which you have added have been copied to translations but in the base language. Access each translation of the form and manually translate each of the changes.', + }) + ) { + await navigate({ to: '/election-event/$tab', params: { tab: 'observer-forms' } }); + } + } }, - [formId, currentElectionRoundId] + [updateFormMutation, formId, currentElectionRoundId] ); return ( diff --git a/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx b/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx index 414a4be8d..67cf65880 100644 --- a/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx +++ b/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx @@ -28,7 +28,6 @@ function FormTranslationEdit() { const navigate = useNavigate(); const router = useRouter(); const { toast } = useToast(); - const confirm = useConfirm(); const updateFormMutation = useMutation({ mutationFn: ({ @@ -37,14 +36,13 @@ function FormTranslationEdit() { }: { electionRoundId: string; form: UpdateFormRequest; - shouldNavigateAwayAfterSubmit: boolean; }) => { return authApi.put(`/election-rounds/${electionRoundId}/forms/${form.id}`, { ...form, }); }, - onSuccess: async (_, { electionRoundId, shouldNavigateAwayAfterSubmit }) => { + onSuccess: async (_, { electionRoundId }) => { toast({ title: 'Success', description: 'Form updated successfully', @@ -52,17 +50,6 @@ function FormTranslationEdit() { await queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId), type: 'all' }); router.invalidate(); - - if (shouldNavigateAwayAfterSubmit) { - if ( - await confirm({ - title: 'Changes made to form in base language', - body: 'Please note that changes have been made to the form in base language, which can impact the translation(s). All new questions or response options which you have added have been copied to translations but in the base language. Access each translation of the form and manually translate each of the changes.', - }) - ) { - await navigate({ to: '/election-event/$tab', params: { tab: 'observer-forms' } }); - } - } }, onError: () => { @@ -75,12 +62,12 @@ function FormTranslationEdit() { }); const saveForm = useCallback( - (formData: EditFormType, shouldNavigateAwayAfterSubmit: boolean) => { + async (formData: EditFormType, shouldNavigateAwayAfterSubmit: boolean) => { const updatedForm: UpdateFormRequest = { id: formId, code: formData.code, name: formData.name, - defaultLanguage: formData.languageCode, + defaultLanguage: form.defaultLanguage, description: formData.description, formType: formData.formType, languages: formData.languages, @@ -88,13 +75,15 @@ function FormTranslationEdit() { questions: formData.questions.map(mapToQuestionRequest), }; - updateFormMutation.mutate({ + await updateFormMutation.mutateAsync({ electionRoundId: currentElectionRoundId, form: updatedForm, - shouldNavigateAwayAfterSubmit, }); + if (shouldNavigateAwayAfterSubmit) { + await navigate({ to: '/election-event/$tab', params: { tab: 'observer-forms' } }); + } }, - [formId, currentElectionRoundId] + [updateFormMutation, formId, currentElectionRoundId] ); return ( diff --git a/web/src/features/ngos/components/EditNgo.tsx b/web/src/features/ngos/components/EditNgo.tsx index cbb7d7075..7990bbf4a 100644 --- a/web/src/features/ngos/components/EditNgo.tsx +++ b/web/src/features/ngos/components/EditNgo.tsx @@ -54,20 +54,29 @@ export const EditNgo: FC = ({ existingData, ngoId }) => { }); }; - useBlocker({ - shouldBlockFn: async () => { - if (!form.formState.isDirty || form.formState.isSubmitting) { - return false; - } + const isDirty = form.formState.isDirty; + + const { proceed, reset, status } = useBlocker({ + shouldBlockFn: () => isDirty, + withResolver: true, + }); - return !(await confirm({ + useEffect(() => { + if (status === 'blocked') { + confirm({ title: `Unsaved Changes Detected`, body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', actionButton: 'Leave', cancelButton: 'Stay', - })); - }, - }); + }).then((confirmed) => { + if (confirmed) { + proceed(); + } else { + reset(); + } + }); + } + }, [status, confirm, proceed, reset]); return ( = ({ data }) => { } breadcrumbs={<>}> - + Organization details Admin users Monitored elections diff --git a/web/src/features/ngos/components/admins/AddNgoAdminDialog.tsx b/web/src/features/ngos/components/admins/AddNgoAdminDialog.tsx index fd97bef1e..bae8561f0 100644 --- a/web/src/features/ngos/components/admins/AddNgoAdminDialog.tsx +++ b/web/src/features/ngos/components/admins/AddNgoAdminDialog.tsx @@ -48,20 +48,29 @@ function AddNgoAdminDialog({ open, onOpenChange, ngoId }: AddNgoAdminDialogProps [onOpenChange] ); - useBlocker({ - shouldBlockFn: async () => { - if (!form.formState.isDirty) { - return false; - } + const isDirty = form.formState.isDirty; + + const { proceed, reset, status } = useBlocker({ + shouldBlockFn: () => isDirty, + withResolver: true, + }); - return !(await confirm({ + useEffect(() => { + if (status === 'blocked') { + confirm({ title: `Unsaved Changes Detected`, body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', actionButton: 'Leave', cancelButton: 'Stay', - })); - }, - }); + }).then((confirmed) => { + if (confirmed) { + proceed(); + } else { + reset(); + } + }); + } + }, [status, confirm, proceed, reset]); function onSubmit(values: NgoAdminFormData) { createNgoAdminMutation.mutate({ diff --git a/web/src/features/ngos/components/admins/EditNgoAdmin.tsx b/web/src/features/ngos/components/admins/EditNgoAdmin.tsx index 5eedce642..3686bc234 100644 --- a/web/src/features/ngos/components/admins/EditNgoAdmin.tsx +++ b/web/src/features/ngos/components/admins/EditNgoAdmin.tsx @@ -48,20 +48,29 @@ export const EditNgoAdmin: FC = ({ ngoAdmin }) => { } }, [form.formState.isSubmitSuccessful, form.reset]); - useBlocker({ - shouldBlockFn: async () => { - if (!form.formState.isDirty) { - return false; - } + const isDirty = form.formState.isDirty; + + const { proceed, reset, status } = useBlocker({ + shouldBlockFn: () => isDirty, + withResolver: true, + }); - return !(await confirm({ + useEffect(() => { + if (status === 'blocked') { + confirm({ title: `Unsaved Changes Detected`, body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', actionButton: 'Leave', cancelButton: 'Stay', - })); - }, - }); + }).then((confirmed) => { + if (confirmed) { + proceed(); + } else { + reset(); + } + }); + } + }, [status, confirm, proceed, reset]); function onSubmit(values: EditNgoAdminFormData) { editNgoAdminMutation.mutate({ adminId, values }); diff --git a/web/src/features/observers/components/EditObserver/EditObserver.tsx b/web/src/features/observers/components/EditObserver/EditObserver.tsx index ee93392de..6e9f1134a 100644 --- a/web/src/features/observers/components/EditObserver/EditObserver.tsx +++ b/web/src/features/observers/components/EditObserver/EditObserver.tsx @@ -15,6 +15,7 @@ import { useBlocker, useNavigate } from '@tanstack/react-router'; import { useForm } from 'react-hook-form'; import { useObserverMutations } from '../../hooks/observers-queries'; import { EditObserverFormData, editObserverFormSchema } from '../../models/observer'; +import { useEffect } from 'react'; export default function EditObserver() { const navigate = useNavigate(); @@ -34,20 +35,29 @@ export default function EditObserver() { }, }); - useBlocker({ - shouldBlockFn: async () => { - if (!form.formState.isDirty) { - return false; - } + const isDirty = form.formState.isDirty; - return !(await confirm({ + const { proceed, reset, status } = useBlocker({ + shouldBlockFn: () => isDirty, + withResolver: true, + }); + + useEffect(() => { + if (status === 'blocked') { + confirm({ title: `Unsaved Changes Detected`, body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', actionButton: 'Leave', cancelButton: 'Stay', - })); - }, - }); + }).then((confirmed) => { + if (confirmed) { + proceed(); + } else { + reset(); + } + }); + } + }, [status, confirm, proceed, reset]); function onSubmit(values: EditObserverFormData) { editObserverMutation.mutate({