diff --git a/src/app/Pages/Admin/Faders.tsx b/src/app/Pages/Admin/Faders.tsx index 7f0e74bf..eec920c4 100644 --- a/src/app/Pages/Admin/Faders.tsx +++ b/src/app/Pages/Admin/Faders.tsx @@ -28,6 +28,7 @@ import { useAppSelector } from './../../apis/redux/mainStore' import { DatabaseFader } from './../../../database/repository/fader' import { ApiCall } from './../../apis/wrapper' import { showNotification } from '@mantine/notifications' +import { usePrompt } from '../../apis/utilities/usePrompt' interface FormValues { faders: Array @@ -120,6 +121,8 @@ export const FadersConfigurationPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [faders]) const saveByUserNeeded = formOriginalValues !== JSON.stringify(form.values) // Does the user have unsaved changes + usePrompt(saveByUserNeeded ? 'You have unsaved changes, are you sure you want to leave this page?' : false) + // Handle the submit button const handleSubmit = (values: FormValues) => { setLoadingOverlayVisible(true) @@ -133,6 +136,7 @@ export const FadersConfigurationPage = () => { }) }) } + const fields = form.values.faders.map((_, index) => ( {provided => ( diff --git a/src/app/Pages/Admin/Folders.tsx b/src/app/Pages/Admin/Folders.tsx index ef64b832..f526af0d 100644 --- a/src/app/Pages/Admin/Folders.tsx +++ b/src/app/Pages/Admin/Folders.tsx @@ -28,6 +28,7 @@ import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' import { ButtonIconSelectItem, availableIcons } from '../../Components/ControlPanel/ButtonIcon' import { DatabaseFolder } from './../../../database/repository/folder' import { useAppSelector } from './../../apis/redux/mainStore' +import { usePrompt } from './../../apis/utilities/usePrompt' import { ApiCall } from './../../apis/wrapper' interface FormValues { @@ -88,6 +89,7 @@ export const FoldersConfigurationPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [folders]) const saveByUserNeeded = formOriginalValues !== JSON.stringify(form.values) // Does the user have unsaved changes + usePrompt(saveByUserNeeded ? 'You have unsaved changes, are you sure you want to leave this page?' : false) // Handle the submit button const handleSubmit = (values: typeof form.values) => { diff --git a/src/app/Pages/Admin/Presets.tsx b/src/app/Pages/Admin/Presets.tsx index d1a1fb0d..47bc964a 100644 --- a/src/app/Pages/Admin/Presets.tsx +++ b/src/app/Pages/Admin/Presets.tsx @@ -35,6 +35,7 @@ import { FaSpaceShuttle } from '@react-icons/all-files/fa/FaSpaceShuttle' import { FaTrash } from '@react-icons/all-files/fa/FaTrash' import React, { useEffect, useState } from 'react' import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' +import { usePrompt } from '../../apis/utilities/usePrompt' import { DatabasePreset, PresetTypes } from './../../../database/repository/preset' import { E131PresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/E131' import { HTTPPresetEditModal } from './../../Components/Admin/Controls/Presets/EditModal/HTTP' @@ -96,6 +97,8 @@ export const PresetsConfigurationPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [presets]) const saveByUserNeeded = formOriginalValues !== JSON.stringify(form.values) // Does the user have unsaved changes + usePrompt(saveByUserNeeded ? 'You have unsaved changes, are you sure you want to leave this page?' : false) + // Handle the submit button const handleSubmit = (values: typeof form.values) => { setLoadingOverlayVisible(true) diff --git a/src/app/apis/utilities/usePrompt.tsx b/src/app/apis/utilities/usePrompt.tsx new file mode 100644 index 00000000..e17610c4 --- /dev/null +++ b/src/app/apis/utilities/usePrompt.tsx @@ -0,0 +1,45 @@ +import * as React from 'react' +import { useBeforeUnload, unstable_useBlocker as useBlocker } from 'react-router-dom' + +// From https://gist.github.com/chaance/2f3c14ec2351a175024f62fd6ba64aa6 + +// You can abstract `useBlocker` to use the browser's `window.confirm` dialog to +// determine whether or not the user should navigate within the current origin. +// `useBlocker` can also be used in conjunction with `useBeforeUnload` to +// prevent navigation away from the current origin. +// +// IMPORTANT: There are edge cases with this behavior in which React Router +// cannot reliably access the correct location in the history stack. In such +// cases the user may attempt to stay on the page but the app navigates anyway, +// or the app may stay on the correct page but the browser's history stack gets +// out of whack. You should test your own implementation thoroughly to make sure +// the trade offs are right for your users. + +export const usePrompt = ( + message: string | null | undefined | false, + { beforeUnload }: { beforeUnload?: boolean } = {} +) => { + const blocker = useBlocker( + React.useCallback(() => (typeof message === 'string' ? !window.confirm(message) : false), [message]) + ) + const prevState = React.useRef(blocker.state) + React.useEffect(() => { + if (blocker.state === 'blocked') { + blocker.reset() + } + prevState.current = blocker.state + }, [blocker]) + + useBeforeUnload( + React.useCallback( + event => { + if (beforeUnload && typeof message === 'string') { + event.preventDefault() + event.returnValue = message + } + }, + [message, beforeUnload] + ), + { capture: true } + ) +}