diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 4583d1dbb..bec4f619f 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -44,3 +44,5 @@ const plansProd: Mapping = { }; export const plans = plansProd || plansDev; + +export const leaveConfirmDefaultMessage = 'Do you want to save your results before leaving this page?'; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index c34dc4e41..b4b2f7e84 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -1,2 +1,3 @@ export { usePopUp } from './usePopUp'; export { useToggle } from './useToggle'; +export { useLeaveConfirm } from './useLeaveConfirm'; diff --git a/frontend/src/hooks/useLeaveConfirm.tsx b/frontend/src/hooks/useLeaveConfirm.tsx new file mode 100644 index 000000000..9a64f7a4d --- /dev/null +++ b/frontend/src/hooks/useLeaveConfirm.tsx @@ -0,0 +1,54 @@ +import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; + +import { leaveConfirmDefaultMessage } from '@app/const'; + +type LeaveConfirmProps = { + initialValue: boolean, + message?: string +} + +interface LeaveConfirmReturn { + hasUnsavedChanges: boolean, + setHasUnsavedChanges: Dispatch>, +} + +export function useLeaveConfirm({ + initialValue, + message = leaveConfirmDefaultMessage, +}: LeaveConfirmProps): LeaveConfirmReturn { + const router = useRouter() + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(initialValue); + + const onRouteChangeStart = useCallback(() => { + if (hasUnsavedChanges) { + if (confirm(message)) { + return true + } + throw "Abort route change by user's confirmation." + } + }, [hasUnsavedChanges]) + + const handleWindowClose = useCallback((e: any) => { + if (!hasUnsavedChanges) { + return; + } + e.preventDefault(); + e.returnValue = message; + }); + + useEffect(() => { + router.events.on("routeChangeStart", onRouteChangeStart); + window.addEventListener('beforeunload', handleWindowClose); + + return () => { + router.events.off("routeChangeStart", onRouteChangeStart); + window.removeEventListener('beforeunload', handleWindowClose); + } + }, [onRouteChangeStart, handleWindowClose]); + + return { + hasUnsavedChanges, + setHasUnsavedChanges, + }; +} diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx index 1f3385860..55472d50f 100644 --- a/frontend/src/pages/dashboard/[id].tsx +++ b/frontend/src/pages/dashboard/[id].tsx @@ -38,6 +38,8 @@ import { IconButton } from '@app/components/v2'; import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount'; import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback'; import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar'; +import { useLeaveConfirm } from '@app/hooks'; +import { leaveConfirmDefaultMessage } from '@app/const'; import addSecrets from '../api/files/AddSecrets'; import deleteSecrets from '../api/files/DeleteSecrets'; @@ -116,7 +118,6 @@ function findDuplicates(arr: any[]) { export default function Dashboard() { const [data, setData] = useState(); const [initialData, setInitialData] = useState([]); - const [buttonReady, setButtonReady] = useState(false); const router = useRouter(); const [blurred, setBlurred] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true); @@ -137,6 +138,7 @@ export default function Dashboard() { const [dropZoneData, setDropZoneData] = useState(); const [projectTags, setProjectTags] = useState([]); + const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({initialValue: false}); const { t } = useTranslation(); const { createNotification } = useNotificationContext(); @@ -153,36 +155,6 @@ export default function Dashboard() { setAtSecretsAreaTop(false); } }; - // #TODO: fix save message for changing reroutes - // const beforeRouteHandler = (url) => { - // const warningText = - // "Do you want to save your results bfore leaving this page?"; - // if (!buttonReady) return; - // if (router.asPath !== url && !confirm(warningText)) { - // // router.events.emit('routeChangeError'); - // // setData(data) - // savePush(); - // throw `Route change to "${url}" was aborted (this error can be safely ignored).`; - // } else { - // setButtonReady(false); - // } - // }; - - // prompt the user if they try and leave with unsaved changes - useEffect(() => { - const warningText = 'Do you want to save your results before leaving this page?'; - const handleWindowClose = (e: any) => { - if (!buttonReady) return; - e.preventDefault(); - e.returnValue = warningText; - }; - window.addEventListener('beforeunload', handleWindowClose); - // router.events.on('routeChangeStart', beforeRouteHandler); - return () => { - window.removeEventListener('beforeunload', handleWindowClose); - // router.events.off('routeChangeStart', beforeRouteHandler); - }; - }, [buttonReady]); // TODO(akhilmhdh): change to FP const sortValuesHandler = ( @@ -318,7 +290,7 @@ export default function Dashboard() { }; const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => { - setButtonReady(true); + setHasUnsavedChanges(true); toggleSidebar('None'); createNotification({ text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`, @@ -332,27 +304,27 @@ export default function Dashboard() { const modifyValue = (value: string, pos: number) => { setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e))); - setButtonReady(true); + setHasUnsavedChanges(true); }; const modifyValueOverride = (valueOverride: string | undefined, pos: number) => { setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e))); - setButtonReady(true); + setHasUnsavedChanges(true); }; const modifyKey = (key: string, pos: number) => { setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e))); - setButtonReady(true); + setHasUnsavedChanges(true); }; const modifyComment = (comment: string, pos: number) => { setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e))); - setButtonReady(true); + setHasUnsavedChanges(true); }; const modifyTags = (tags: Tag[], pos: number) => { setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e))); - setButtonReady(true); + setHasUnsavedChanges(true); }; // For speed purposes and better perforamance, we are using useCallback @@ -422,7 +394,7 @@ export default function Dashboard() { } // Once "Save changes" is clicked, disable that button - setButtonReady(false); + setHasUnsavedChanges(false); const secretsToBeDeleted = initialData! .filter( @@ -566,7 +538,7 @@ export default function Dashboard() { ); return filteredOldData.concat(filteredNewData); }); - setButtonReady(true); + setHasUnsavedChanges(true); }; const addData = (newData: SecretDataProps[]) => { @@ -587,6 +559,23 @@ export default function Dashboard() { deleteRow({ ids, secretName }); }; + const handleOnEnvironmentChange = (envName: string) => { + if(hasUnsavedChanges) { + if (!confirm(leaveConfirmDefaultMessage)) return; + } + + const selectedWorkspaceEnv = workspaceEnvs.find(({ name }: { name: string }) => envName === name) || { + name: 'unknown', + slug: 'unknown', + isWriteDenied: false + }; + + if (selectedWorkspaceEnv) { + if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv); + else setSelectedEnv(selectedWorkspaceEnv); + } + }; + return data ? (
@@ -655,16 +644,7 @@ export default function Dashboard() { name)} - onChange={(envName) => - setSelectedEnv( - workspaceEnvs.find(({ name }) => envName === name) || { - name: 'unknown', - slug: 'unknown', - isWriteDenied: false, - isReadDenied: false - } - ) - } + onChange={handleOnEnvironmentChange} /> )}
@@ -681,14 +661,14 @@ export default function Dashboard() { icon={faClockRotateLeft} />} - {(data?.length !== 0 || buttonReady) && !snapshotData && ( + {(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
)} @@ -742,31 +722,13 @@ export default function Dashboard() { name)} - onChange={(envName) => - setSelectedEnv( - workspaceEnvs.find(({ name }) => envName === name) || { - name: 'unknown', - slug: 'unknown', - isWriteDenied: false, - isReadDenied: false - } - ) - } + onChange={handleOnEnvironmentChange} /> ) : ( name)} - onChange={(envName) => - setSelectedSnapshotEnv( - workspaceEnvs.find(({ name }) => envName === name) || { - name: 'unknown', - slug: 'unknown', - isWriteDenied: false, - isReadDenied: false - } - ) - } + onChange={handleOnEnvironmentChange} /> )}
@@ -970,7 +932,7 @@ export default function Dashboard() { setErrorDragAndDrop={setErrorDragAndDrop} createNewFile={addRow} errorDragAndDrop={errorDragAndDrop} - setButtonReady={setButtonReady} + setButtonReady={setHasUnsavedChanges} keysExist numCurrentRows={data.length} /> @@ -986,7 +948,7 @@ export default function Dashboard() { setErrorDragAndDrop={setErrorDragAndDrop} createNewFile={addRow} errorDragAndDrop={errorDragAndDrop} - setButtonReady={setButtonReady} + setButtonReady={setHasUnsavedChanges} numCurrentRows={data.length} keysExist={false} /> @@ -1013,7 +975,7 @@ export default function Dashboard() { modifyValue={listenChangeValue} modifyValueOverride={listenChangeValueOverride} modifyComment={listenChangeComment} - buttonReady={buttonReady} + buttonReady={hasUnsavedChanges} workspaceEnvs={workspaceEnvs} selectedEnv={selectedEnv!} workspaceId={workspaceId}