diff --git a/packages/shared/src/components/modals/DirtyFormModal.tsx b/packages/shared/src/components/modals/DirtyFormModal.tsx index 88e9111e30..36db5c7d52 100644 --- a/packages/shared/src/components/modals/DirtyFormModal.tsx +++ b/packages/shared/src/components/modals/DirtyFormModal.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import type { LazyModalCommonProps } from './common/Modal'; import { Modal } from './common/Modal'; import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; @@ -12,7 +12,7 @@ import { useLazyModal } from '../../hooks/useLazyModal'; interface DirtyFormModalProps extends LazyModalCommonProps { onDiscard: () => void; - onSave: () => void; + onSave: () => void | Promise; } export default function DirtyFormModal({ @@ -22,12 +22,23 @@ export default function DirtyFormModal({ onSave, }: DirtyFormModalProps): ReactElement { const { closeModal } = useLazyModal(); + const [isSaving, setIsSaving] = useState(false); - const handleSave = () => { - if (onSave) { - onSave(); + const handleSave = async () => { + if (!onSave) { + closeModal(); + return; + } + + setIsSaving(true); + try { + await onSave(); + closeModal(); + } catch { + // Error handled by caller's onError - keep modal open + } finally { + setIsSaving(false); } - closeModal(); }; const handleDiscard = () => { @@ -67,6 +78,7 @@ export default function DirtyFormModal({ variant={ButtonVariant.Secondary} size={ButtonSize.Medium} onClick={handleDiscard} + disabled={isSaving} > Discard @@ -75,6 +87,8 @@ export default function DirtyFormModal({ variant={ButtonVariant.Primary} size={ButtonSize.Medium} onClick={handleSave} + loading={isSaving} + disabled={isSaving} > Save changes diff --git a/packages/shared/src/hooks/useDirtyForm.ts b/packages/shared/src/hooks/useDirtyForm.ts index a7a3822632..974caae3e6 100644 --- a/packages/shared/src/hooks/useDirtyForm.ts +++ b/packages/shared/src/hooks/useDirtyForm.ts @@ -4,7 +4,7 @@ import { useLazyModal } from './useLazyModal'; import { LazyModal } from '../components/modals/common/types'; export interface UseDirtyFormOptions { - onSave: () => void; + onSave: () => void | Promise; onDiscard?: () => void; } @@ -30,6 +30,17 @@ export const useDirtyForm = ( } }, [onDiscard, router]); + const handleSave = useCallback(async () => { + await onSave(); + + allowNavigationRef.current = true; + + if (pendingUrlRef.current) { + router.push(pendingUrlRef.current); + pendingUrlRef.current = null; + } + }, [onSave, router]); + useEffect(() => { const handleRouteChangeStart = (url: string) => { if (allowNavigationRef.current || !isDirty || url === router.asPath) { @@ -43,7 +54,7 @@ export const useDirtyForm = ( type: LazyModal.DirtyForm, props: { onDiscard: handleDiscard, - onSave, + onSave: handleSave, }, }); @@ -58,7 +69,36 @@ export const useDirtyForm = ( return () => { router.events.off('routeChangeStart', handleRouteChangeStart); }; - }, [isDirty, router, openModal, handleDiscard, onSave]); + }, [isDirty, router, openModal, handleDiscard, handleSave]); + + useEffect(() => { + const handleBeforePopState = ({ url }: { url: string }) => { + if (allowNavigationRef.current || !isDirty) { + allowNavigationRef.current = false; + return true; + } + + // Store the URL we're trying to navigate to + pendingUrlRef.current = url; + + openModal({ + type: LazyModal.DirtyForm, + props: { + onDiscard: handleDiscard, + onSave: handleSave, + }, + }); + + // Prevent the navigation + return false; + }; + + router.beforePopState(handleBeforePopState); + + return () => { + router.beforePopState(() => true); + }; + }, [isDirty, router, openModal, handleDiscard, handleSave]); useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { @@ -89,6 +129,6 @@ export const useDirtyForm = ( }, hasPendingNavigation: () => pendingUrlRef.current !== null, navigateToPending, - save: onSave, + save: handleSave, }; }; diff --git a/packages/shared/src/hooks/useUserExperienceForm.ts b/packages/shared/src/hooks/useUserExperienceForm.ts index 038c07ad0b..3f5f72754b 100644 --- a/packages/shared/src/hooks/useUserExperienceForm.ts +++ b/packages/shared/src/hooks/useUserExperienceForm.ts @@ -127,7 +127,7 @@ const useUserExperienceForm = ({ { condition: isNewExperience }, ); - const { mutate, isPending } = useMutation({ + const { mutateAsync, isPending } = useMutation({ mutationFn: (data: UserExperience | UserExperienceWork) => type === UserExperienceType.Work ? upsertUserWorkExperience(data as UserExperienceWork, id) @@ -147,7 +147,11 @@ const useUserExperienceForm = ({ queryKey: generateQueryKey(RequestKey.UserExperience, user, 'profile'), exact: false, }); - router.push(`${webappUrl}settings/profile/experience/${type}`); + + // Only navigate to default location if there's no pending navigation from dirty form + if (!dirtyFormRef.current?.hasPendingNavigation()) { + router.push(`${webappUrl}settings/profile/experience/${type}`); + } }, onError: (error: ApiErrorResult) => { if ( @@ -166,9 +170,9 @@ const useUserExperienceForm = ({ }, }); const dirtyForm = useDirtyForm(methods.formState.isDirty, { - onSave: () => { + onSave: async () => { const formData = methods.getValues(); - mutate(formData); + await mutateAsync(formData); }, onDiscard: () => { methods.reset();