Skip to content

Commit

Permalink
Feat/add confirm before close dialog (#893)
Browse files Browse the repository at this point in the history
* Feat: add now confirm before close dialog
  • Loading branch information
kylebonnici authored Feb 23, 2024
1 parent e41a04f commit f1f8325
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 2 deletions.
8 changes: 7 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ This project does _not_ adhere to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) but contrary to it
every new version is a new major version.

## 159.0.0 - UNRELEASED
## 159.0.0 - 2024-02-23

### Added

- `minWidth` parameter to `Dropdown` component.
- `transparentButtonBg` parameter to `Dropdown` component.
- `NumberInput` component (provides text, input, optional unit, and slider).
- Common way to queue ongoing pending tasks. If an app is closed, a dialog is
prompted to alert users before clo sing app. Redux states for this are:
- `addConfirmBeforeClose`
- `clearConfirmBeforeClose`
- `preventAppCloseUntilComplete` can be used to wrap some promise and
secure app from closing until promise is resolved

### Removed

Expand Down
2 changes: 2 additions & 0 deletions src/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Reducer } from 'redux';
import { inMain as openWindow } from '../../ipc/openWindow';
import { setNrfutilLogger } from '../../nrfutil/nrfutilLogger';
import About from '../About/About';
import ConfirmCloseDialog from '../ConfirmBeforeClose/ConfirmCloseDialog';
import BrokenDeviceDialog from '../Device/BrokenDeviceDialog/BrokenDeviceDialog';
import { setAutoReselect } from '../Device/deviceAutoSelectSlice';
import {
Expand Down Expand Up @@ -181,6 +182,7 @@ const ConnectedApp: FC<ConnectedAppProps> = ({

<ErrorDialog />
<BrokenDeviceDialog />
<ConfirmCloseDialog />
{children}
</div>
);
Expand Down
85 changes: 85 additions & 0 deletions src/ConfirmBeforeClose/ConfirmCloseDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2015 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-4-Clause
*/

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getCurrentWindow } from '@electron/remote';

import { ConfirmationDialog } from '../Dialog/Dialog';
import { AppThunk } from '../store';
import {
addConfirmBeforeClose,
clearConfirmBeforeClose,
ConfirmBeforeCloseApp,
getNextConfirmDialog,
getShowConfirmCloseDialog,
setShowCloseDialog,
} from './confirmBeforeCloseSlice';

export default () => {
const dispatch = useDispatch();
const [confirmedDialogs, setConfirmedDialogs] = useState<
ConfirmBeforeCloseApp[]
>([]);

const showCloseDialog = useSelector(getShowConfirmCloseDialog);
const nextConfirmDialog = useSelector(getNextConfirmDialog);

useEffect(() => {
if (!nextConfirmDialog && showCloseDialog) {
confirmedDialogs.forEach(confirmedDialog => {
if (confirmedDialog.onClose) confirmedDialog.onClose();
});
setConfirmedDialogs([]);
getCurrentWindow().close();
}
}, [nextConfirmDialog, dispatch, showCloseDialog, confirmedDialogs]);

useEffect(() => {
const action = (ev: BeforeUnloadEvent) =>
dispatch<AppThunk>((_, getState) => {
const hasToGetExplicitConform =
getState().confirmBeforeCloseDialog.confirmCloseApp.length >
0;
if (hasToGetExplicitConform) {
dispatch(setShowCloseDialog(true));
ev.returnValue = true;
}
});

window.addEventListener('beforeunload', action, true);

return () => {
window.removeEventListener('beforeunload', action);
};
}, [dispatch]);

return (
<ConfirmationDialog
headerIcon="alert-outline"
title="Closing nPM PowerUP"
isVisible={showCloseDialog && !!nextConfirmDialog}
onConfirm={() => {
if (nextConfirmDialog) {
setConfirmedDialogs([
...confirmedDialogs,
nextConfirmDialog,
]);
dispatch(clearConfirmBeforeClose(nextConfirmDialog.id));
}
}}
onCancel={() => {
dispatch(setShowCloseDialog(false));
confirmedDialogs.forEach(confirmedDialog =>
dispatch(addConfirmBeforeClose(confirmedDialog))
);
setConfirmedDialogs([]);
}}
>
{nextConfirmDialog?.message}
</ConfirmationDialog>
);
};
96 changes: 96 additions & 0 deletions src/ConfirmBeforeClose/confirmBeforeCloseSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-4-Clause
*/

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { v4 as uuid } from 'uuid';

import type { AppThunk, RootState } from '../store';

export interface ConfirmBeforeCloseApp {
id: string;
message: React.ReactNode;
onClose?: () => void;
}

export interface ConfirmBeforeCloseState {
confirmCloseApp: ConfirmBeforeCloseApp[];
showCloseDialog: boolean;
}

const initialState: ConfirmBeforeCloseState = {
confirmCloseApp: [],
showCloseDialog: false,
};

const slice = createSlice({
name: 'confirmBeforeCloseDialog',
initialState,
reducers: {
addConfirmBeforeClose(
state,
action: PayloadAction<ConfirmBeforeCloseApp>
) {
const index = state.confirmCloseApp.findIndex(
confirmCloseApp => confirmCloseApp.id === action.payload.id
);

if (index !== -1) {
state.confirmCloseApp[index] = action.payload;
} else {
state.confirmCloseApp = [
action.payload,
...state.confirmCloseApp,
];
}
},
clearConfirmBeforeClose(state, action: PayloadAction<string>) {
state.confirmCloseApp = state.confirmCloseApp.filter(
confirmCloseApp => confirmCloseApp.id !== action.payload
);
},
setShowCloseDialog(state, action: PayloadAction<boolean>) {
state.showCloseDialog = action.payload;
},
},
});

export const {
reducer,
actions: {
addConfirmBeforeClose,
setShowCloseDialog,
clearConfirmBeforeClose,
},
} = slice;

export const getNextConfirmDialog = (state: RootState) =>
state.confirmBeforeCloseDialog.confirmCloseApp.length > 0
? state.confirmBeforeCloseDialog.confirmCloseApp[0]
: undefined;

export const getShowConfirmCloseDialog = (state: RootState) =>
state.confirmBeforeCloseDialog.showCloseDialog;

export const preventAppCloseUntilComplete =
(
dialogInfo: Omit<ConfirmBeforeCloseApp, 'id'>,
promise: Promise<unknown>,
abortController?: AbortController
): AppThunk =>
dispatch => {
const id = uuid();
dispatch(
addConfirmBeforeClose({
...dialogInfo,
id,
onClose: () => {
dialogInfo.onClose?.();
abortController?.abort();
},
})
);
promise.finally(() => dispatch(clearConfirmBeforeClose(id)));
};
14 changes: 13 additions & 1 deletion src/Device/deviceSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: LicenseRef-Nordic-4-Clause
*/
import { DeviceInfo } from '../../nrfutil/device/deviceInfo';
import { preventAppCloseUntilComplete } from '../ConfirmBeforeClose/confirmBeforeCloseSlice';
import logger from '../logging';
import describeError from '../logging/describeError';
import { AppThunk, RootState } from '../store';
Expand Down Expand Up @@ -151,7 +152,7 @@ export const prepareDevice =
if (!selectedDeviceSetup) {
onFail('No firmware was selected'); // Should never happen
} else {
dispatch(
const task = dispatch(
selectedDeviceSetup.programDevice(
(progress: number, message?: string) => {
dispatch(setDeviceSetupProgress(progress));
Expand All @@ -164,6 +165,17 @@ export const prepareDevice =
)
.then(onSuccessWrapper)
.catch(onFail);

dispatch(
preventAppCloseUntilComplete(
{
message: `The device is being programmed.
Closing application right now might result in some unknown behavior and might also brick the device.
Are you sure you want to continue?`,
},
task
)
);
}
};

Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ export {
getWaitForDevice,
clearWaitForDevice,
} from './Device/deviceAutoSelectSlice';

export {
addConfirmBeforeClose,
clearConfirmBeforeClose,
preventAppCloseUntilComplete,
} from './ConfirmBeforeClose/confirmBeforeCloseSlice';
export { deviceInfo } from './Device/deviceInfo/deviceInfo';
export { isDeviceInDFUBootloader } from './Device/sdfuOperations';
export {
Expand Down
2 changes: 2 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Reducer } from 'redux';

import { reducer as shortcuts } from './About/shortcutSlice';
import { reducer as appLayout } from './App/appLayout';
import { reducer as confirmBeforeCloseDialog } from './ConfirmBeforeClose/confirmBeforeCloseSlice';
import { reducer as brokenDeviceDialog } from './Device/BrokenDeviceDialog/brokenDeviceDialogSlice';
import { reducer as deviceAutoSelect } from './Device/deviceAutoSelectSlice';
import { reducer as deviceSetup } from './Device/deviceSetupSlice';
Expand All @@ -33,6 +34,7 @@ export const rootReducerSpec = (appReducer: Reducer = noopReducer) => ({
log,
shortcuts,
flashMessages,
confirmBeforeCloseDialog,
});

const store = (appReducer?: Reducer) =>
Expand Down

0 comments on commit f1f8325

Please sign in to comment.