mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-11 00:17:59 -05:00
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:
@@ -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?';
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { usePopUp } from './usePopUp';
|
||||
export { useToggle } from './useToggle';
|
||||
export { useLeaveConfirm } from './useLeaveConfirm';
|
||||
|
||||
55
frontend/src/hooks/useLeaveConfirm.tsx
Normal file
55
frontend/src/hooks/useLeaveConfirm.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user