Merge pull request #329 from fervillarrealm/feature/52-save-changes-user-leaving-dashboard

feat(ui): save changes when user leaving dashboard
This commit is contained in:
mv-turtle
2023-02-13 19:47:10 -08:00
committed by GitHub
4 changed files with 99 additions and 76 deletions

View File

@@ -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?';

View File

@@ -1,2 +1,3 @@
export { usePopUp } from './usePopUp';
export { useToggle } from './useToggle';
export { useLeaveConfirm } from './useLeaveConfirm';

View File

@@ -0,0 +1,55 @@
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<SetStateAction<boolean>>,
}
export function useLeaveConfirm({
initialValue,
message = leaveConfirmDefaultMessage,
}: LeaveConfirmProps): LeaveConfirmReturn {
const router = useRouter()
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(initialValue);
const onRouteChangeStart = useCallback(() => {
if (hasUnsavedChanges) {
if (window.confirm(message)) {
return true
}
throw new Error("Abort route change by user's confirmation.")
}
return false;
}, [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,
};
}

View File

@@ -35,9 +35,11 @@ import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import { IconButton } from '@app/components/v2';
import { leaveConfirmDefaultMessage } from '@app/const';
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 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<SecretDataProps[] | null>();
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
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<SecretDataProps[]>();
const [projectTags, setProjectTags] = useState<Tag[]>([]);
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,26 @@ export default function Dashboard() {
deleteRow({ ids, secretName });
};
const handleOnEnvironmentChange = (envName: string) => {
if(hasUnsavedChanges) {
if (!window.confirm(leaveConfirmDefaultMessage)) return;
}
const selectedWorkspaceEnv = workspaceEnvs.find(({ name }: { name: string }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
};
if (selectedWorkspaceEnv) {
if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv);
else setSelectedEnv(selectedWorkspaceEnv);
}
setHasUnsavedChanges(false);
};
return data ? (
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
<Head>
@@ -655,16 +647,7 @@ export default function Dashboard() {
<ListBox
isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
onChange={handleOnEnvironmentChange}
/>
)}
</div>
@@ -681,14 +664,14 @@ export default function Dashboard() {
icon={faClockRotateLeft}
/>}
</div>
{(data?.length !== 0 || buttonReady) && !snapshotData && (
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
<div className="flex justify-start max-w-sm mt-1">
<Button
text={String(t('common:save-changes'))}
onButtonPressed={savePush}
color="primary"
size="md"
active={buttonReady}
active={hasUnsavedChanges}
iconDisabled={faCheck}
textDisabled={String(t('common:saved'))}
loading={saveLoading}
@@ -723,11 +706,11 @@ export default function Dashboard() {
text: `Rollback has been performed successfully.`,
type: 'success'
});
setButtonReady(false);
setHasUnsavedChanges(false);
}}
color="primary"
size="md"
active={buttonReady}
active={hasUnsavedChanges}
/>
</div>
)}
@@ -742,31 +725,13 @@ export default function Dashboard() {
<ListBox
isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
onChange={handleOnEnvironmentChange}
/>
) : (
<ListBox
isSelected={selectedSnapshotEnv?.name || ''}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedSnapshotEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
onChange={handleOnEnvironmentChange}
/>
)}
<div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
@@ -970,7 +935,7 @@ export default function Dashboard() {
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
setButtonReady={setHasUnsavedChanges}
keysExist
numCurrentRows={data.length}
/>
@@ -986,7 +951,7 @@ export default function Dashboard() {
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
setButtonReady={setHasUnsavedChanges}
numCurrentRows={data.length}
keysExist={false}
/>
@@ -1013,7 +978,7 @@ export default function Dashboard() {
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
buttonReady={hasUnsavedChanges}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}