-
Notifications
You must be signed in to change notification settings - Fork 414
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
feat: add warning when navigating away with unsaved changes in environment settings #5049
base: main
Are you sure you want to change the base?
Changes from 11 commits
b48b8ca
5a4ea6e
84061ab
64d946a
ba0ee6a
eb709d6
c0b0b70
9a5d7fd
9410fc8
7a1da9e
974a20e
e5cd0dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export type Webhook = { | ||
id: string | ||
url: string | ||
secret: string | ||
enabled: boolean | ||
created_at: string | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome! I guess we can start using this in a lot of places There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool that you like it! Let me know if it requires some changes |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { useState, useEffect, useRef, useCallback } from "react" | ||
import { RouterChildContext } from "react-router-dom" | ||
import { Modal, ModalHeader, ModalBody } from "reactstrap" | ||
|
||
/** | ||
* useFormNotSavedModal | ||
* @param {history: RouterChildContext['router']['history']} history - The history object | ||
* @param {string} warningMessage - The message to show when user attempts to leave | ||
* @returns {[React.FC, Function, boolean]} | ||
*/ | ||
|
||
type UseFormNotSavedModalReturn = [React.FC, React.Dispatch<React.SetStateAction<boolean>>, boolean] | ||
|
||
interface UseFormNotSavedModalOptions { | ||
warningMessage?: string | ||
onDiscard?: () => void | ||
} | ||
|
||
const useFormNotSavedModal = ( | ||
history: RouterChildContext['router']['history'], | ||
options: UseFormNotSavedModalOptions = {} | ||
): UseFormNotSavedModalReturn => { | ||
const { onDiscard, warningMessage = "You have unsaved changes, are you sure you want to leave?" } = options | ||
|
||
const [isDirty, setIsDirty] = useState(false) | ||
const [isNavigating, setIsNavigating] = useState(false); | ||
const [nextLocation, setNextLocation] = useState<Location | null>(null); | ||
|
||
const unblockRef = useRef<(() => void) | null>(null); | ||
useEffect(() => { | ||
if (!isDirty) return; | ||
|
||
const unblock = history.block((transition: Location) => { | ||
setNextLocation(transition); | ||
setIsNavigating(true); | ||
return false; | ||
}); | ||
|
||
unblockRef.current = unblock; | ||
return () => { | ||
if (unblockRef.current) { | ||
unblockRef.current(); | ||
} | ||
unblockRef.current = null; | ||
}; | ||
}, [isDirty, history]); | ||
|
||
const discardAndConfirmNavigation = useCallback(() => { | ||
// allow the route change to happen | ||
if (unblockRef.current) { | ||
unblockRef.current(); // unblocks | ||
unblockRef.current = null; | ||
} | ||
// navigate | ||
if (nextLocation) { | ||
history.push(`${nextLocation.pathname}${nextLocation.search}`); | ||
} | ||
if (onDiscard) { | ||
onDiscard() | ||
} | ||
setIsDirty(false) | ||
setIsNavigating(false); | ||
setNextLocation(null); | ||
}, [nextLocation, history, onDiscard]); | ||
|
||
const cancelNavigation = useCallback(() => { | ||
history.push(`${history.location.pathname}${history.location.search}`); | ||
setIsNavigating(false); | ||
setNextLocation(null); | ||
}, [history]); | ||
|
||
// Listen for browser/tab close (the 'beforeunload' event) | ||
useEffect(() => { | ||
const handleBeforeUnload = (event: BeforeUnloadEvent) => { | ||
if (!isDirty) return | ||
event.preventDefault() | ||
event.returnValue = warningMessage | ||
} | ||
|
||
window.addEventListener("beforeunload", handleBeforeUnload) | ||
return () => { | ||
window.removeEventListener("beforeunload", handleBeforeUnload) | ||
} | ||
}, [isDirty, warningMessage]) | ||
|
||
const DirtyFormModal = () => ( | ||
<Modal isOpen={isDirty && isNavigating} toggle={cancelNavigation}> | ||
<ModalHeader> | ||
<h5>Unsaved Changes</h5> | ||
</ModalHeader> | ||
<ModalBody> | ||
<p>{warningMessage}</p> | ||
</ModalBody> | ||
<div className="modal-footer"> | ||
<Button onClick={discardAndConfirmNavigation} theme="secondary" className="mr-2">Yes, discard changes</Button> | ||
<Button onClick={cancelNavigation} class="btn-primary">Cancel</Button> | ||
Comment on lines
+95
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me know if I need to invert the colors/theming! |
||
</div> | ||
</Modal> | ||
) | ||
|
||
return [DirtyFormModal, setIsDirty, isDirty] | ||
} | ||
|
||
export default useFormNotSavedModal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very specific implementation related to the hook. The selected tab would keep the focus on after interacting with the newly implemented modal