Skip to content
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
42 changes: 27 additions & 15 deletions web/src/components/FormEditor/FormEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -205,7 +205,7 @@ interface FormEditorProps {
const FormEditor: FC<FormEditorProps> = ({ 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,
Expand Down Expand Up @@ -350,7 +350,7 @@ const FormEditor: FC<FormEditorProps> = ({ hasCitizenReportingOption, formData,
}

return undefined;
});
}), [formData]);

const form = useForm<EditFormType>({
resolver: zodResolver(ZEditFormType),
Expand All @@ -366,21 +366,16 @@ const FormEditor: FC<FormEditorProps> = ({ 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({
Expand All @@ -395,6 +390,23 @@ const FormEditor: FC<FormEditorProps> = ({ 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 (
<Form {...form}>
<form
Expand Down
31 changes: 20 additions & 11 deletions web/src/components/FormTranslationEditor/FormTranslationEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { FormTemplateFull } from '@/features/form-templates/models';
import { FormFull } from '@/features/forms/models';
import { cn, ensureTranslatedStringCorrectness } from '@/lib/utils';
import { useBlocker } from '@tanstack/react-router';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { EditFormType, ZEditFormType } from '../FormEditor/FormEditor';
import FormDetailsTranslationEditor from './FormDetailsTranslationEditor';

Expand All @@ -50,7 +50,7 @@ export default function FormTranslationEditor({
const confirm = useConfirm();
const [navigateAwayAfterSave, setNavigateAwayAfterSave] = useState(false);

const formQuestions = formData.questions.map((question) => {
const formQuestions = useMemo(() => formData.questions.map((question) => {
if (isNumberQuestion(question)) {
const numberQuestion: EditNumberQuestionType = {
$questionType: QuestionType.NumberQuestionType,
Expand Down Expand Up @@ -196,7 +196,7 @@ export default function FormTranslationEditor({
}

return undefined;
});
}), [formData]);

const form = useForm<EditFormType>({
resolver: zodResolver(ZEditFormType),
Expand All @@ -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 (
<Form {...form}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ function SelectQuestionTranslationEditor({ questionIndex }: SelectQuestionTransl
/>
<div className='inline-flex items-center space-x-2 ml-2 w-[100px] '>
<FlagIcon
className={cn('h-4 w-4 cursor-pointer', {
'text-slate-700 hover:text-red-600': !option.isFlagged,
'text-red-600 hover:text-slate-00': option.isFlagged,
className={cn('h-4 w-4', {
'text-slate-700': !option.isFlagged,
'text-red-600': option.isFlagged,
})}
/>
</div>
Expand Down
25 changes: 17 additions & 8 deletions web/src/features/election-event/components/Guides/AddGuideForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 15 additions & 17 deletions web/src/features/forms/components/FormEdit/FormEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,20 @@ function FormEdit() {
}: {
electionRoundId: string;
form: UpdateFormRequest;
shouldNavigateAwayAfterSubmit: boolean;
}) => {
return authApi.put<void>(`/election-rounds/${electionRoundId}/forms/${form.id}`, {
...form,
});
},

onSuccess: async (_, { electionRoundId, shouldNavigateAwayAfterSubmit }) => {
onSuccess: async (_, { electionRoundId }) => {
toast({
title: 'Success',
description: 'Form updated successfully',
});

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: () => {
Expand All @@ -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,
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ function FormTranslationEdit() {
const navigate = useNavigate();
const router = useRouter();
const { toast } = useToast();
const confirm = useConfirm();

const updateFormMutation = useMutation({
mutationFn: ({
Expand All @@ -37,32 +36,20 @@ function FormTranslationEdit() {
}: {
electionRoundId: string;
form: UpdateFormRequest;
shouldNavigateAwayAfterSubmit: boolean;
}) => {
return authApi.put<void>(`/election-rounds/${electionRoundId}/forms/${form.id}`, {
...form,
});
},

onSuccess: async (_, { electionRoundId, shouldNavigateAwayAfterSubmit }) => {
onSuccess: async (_, { electionRoundId }) => {
toast({
title: 'Success',
description: 'Form updated successfully',
});

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: () => {
Expand All @@ -75,26 +62,28 @@ 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,
icon: isNilOrWhitespace(formData.icon) ? undefined : formData.icon,
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 (
Expand Down
Loading
Loading