From 90b459d5dbf4f1fd2e9db75f735ea6612db857d7 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Wed, 27 Jul 2022 18:17:20 -0500 Subject: [PATCH] firmware/restoreOfficialDialog: add new dialog --- CHANGELOG.md | 1 + src/app/constants.ts | 5 ++ src/firmware/actions.ts | 21 -------- src/firmware/reducers.test.ts | 3 ++ src/firmware/reducers.ts | 8 ++- .../RestoreOfficialDialog.tsx | 54 +++++++++++++++++++ src/firmware/restoreOfficialDialog/actions.ts | 7 +++ src/firmware/restoreOfficialDialog/i18n.ts | 12 +++++ .../restoreOfficialDialog/reducers.ts | 6 +++ src/firmware/restoreOfficialDialog/redux.ts | 28 ++++++++++ .../translations/en.json | 13 +++++ src/index.scss | 10 +++- src/settings/Settings.test.tsx | 5 +- src/settings/Settings.tsx | 7 ++- 14 files changed, 152 insertions(+), 28 deletions(-) create mode 100644 src/firmware/restoreOfficialDialog/RestoreOfficialDialog.tsx create mode 100644 src/firmware/restoreOfficialDialog/actions.ts create mode 100644 src/firmware/restoreOfficialDialog/i18n.ts create mode 100644 src/firmware/restoreOfficialDialog/reducers.ts create mode 100644 src/firmware/restoreOfficialDialog/redux.ts create mode 100644 src/firmware/restoreOfficialDialog/translations/en.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1054ed28..de8e8fb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added multi-step firmware flashing dialog. - Added support for flashing firmware via USB DFU ([support#659]). - Added an interactive introductory tour of the app. +- Added restore official LEGO firmware dialog. ### Changed - Updated dependencies. diff --git a/src/app/constants.ts b/src/app/constants.ts index 4bf20841..d66af0d3 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -44,6 +44,11 @@ export const pybricksUsbDfuWindowsDriverInstallUrl = export const pybricksUsbLinuxUdevRulesUrl = 'https://github.com/pybricks/support/discussions/688#discussioncomment-3239099'; +export const pybricksBleFirmwareRestoreVideoUrl = + 'https://pybricks.com/install/technic-boost-city/#restoring-the-original-firmware'; + +export const pybricksDfuRestoreUrl = 'https://dfu.pybricks.com'; + /** Pybricks copyright statement. */ export const pybricksCopyright = 'Copyright (c) 2020-2022 The Pybricks Authors'; diff --git a/src/firmware/actions.ts b/src/firmware/actions.ts index ab2343e8..9d058d08 100644 --- a/src/firmware/actions.ts +++ b/src/firmware/actions.ts @@ -395,24 +395,3 @@ export const firmwareDidInstallPybricks = createAction(() => ({ export const firmwareDidFailToInstallPybricks = createAction(() => ({ type: 'firmware.action.didFailToInstallPybricks', })); - -/** - * Action that triggers the restore LEGO firmware saga. - */ -export const firmwareRestoreLego = createAction(() => ({ - type: 'firmware.action.restoreLego', -})); - -/** - * Action that indicates {@link firmwareRestoreLego} succeeded. - */ -export const firmwareDidRestoreLego = createAction(() => ({ - type: 'firmware.action.didRestoreLego', -})); - -/** - * Action that indicates {@link firmwareRestoreLego} failed. - */ -export const firmwareDidFailToRestoreLego = createAction(() => ({ - type: 'firmware.action.didFailToRestoreLego', -})); diff --git a/src/firmware/reducers.test.ts b/src/firmware/reducers.test.ts index c264aa26..7c06c01f 100644 --- a/src/firmware/reducers.test.ts +++ b/src/firmware/reducers.test.ts @@ -21,6 +21,9 @@ test('initial state', () => { "isOpen": false, }, "progress": null, + "restoreOfficialDialog": Object { + "isOpen": false, + }, } `); }); diff --git a/src/firmware/reducers.ts b/src/firmware/reducers.ts index 19ad63c1..dd836bfe 100644 --- a/src/firmware/reducers.ts +++ b/src/firmware/reducers.ts @@ -4,6 +4,7 @@ import { Reducer, combineReducers } from 'redux'; import { didFailToFinish, didFinish, didProgress, didStart } from './actions'; import installPybricksDialog from './installPybricksDialog/reducers'; +import restoreOfficialDialog from './restoreOfficialDialog/reducers'; const flashing: Reducer = (state = false, action) => { if (didStart.matches(action)) { @@ -29,4 +30,9 @@ const progress: Reducer = (state = null, action) => { return state; }; -export default combineReducers({ installPybricksDialog, flashing, progress }); +export default combineReducers({ + installPybricksDialog, + restoreOfficialDialog, + flashing, + progress, +}); diff --git a/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.tsx b/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.tsx new file mode 100644 index 00000000..d59b5951 --- /dev/null +++ b/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.tsx @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { Classes, Dialog } from '@blueprintjs/core'; +import classNames from 'classnames'; +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + pybricksBleFirmwareRestoreVideoUrl, + pybricksDfuRestoreUrl, +} from '../../app/constants'; +import { useSelector } from '../../reducers'; +import ExternalLinkIcon from '../../utils/ExternalLinkIcon'; +import { firmwareRestoreOfficialDialogHide } from './actions'; +import { useI18n } from './i18n'; + +const RestoreOfficialDialog: React.VoidFunctionComponent = () => { + const { isOpen } = useSelector((s) => s.firmware.restoreOfficialDialog); + const dispatch = useDispatch(); + const i18n = useI18n(); + + return ( + dispatch(firmwareRestoreOfficialDialogHide())} + > +
+

{i18n.translate('poweredUpHubs.title')}

+

{i18n.translate('poweredUpHubs.message')}

+

+ + {i18n.translate('poweredUpHubs.action')} + + +

+

{i18n.translate('spikeHubs.title')}

+

{i18n.translate('spikeHubs.message')}

+

+ + {i18n.translate('spikeHubs.action')} + + +

+
+
+ ); +}; + +export default RestoreOfficialDialog; diff --git a/src/firmware/restoreOfficialDialog/actions.ts b/src/firmware/restoreOfficialDialog/actions.ts new file mode 100644 index 00000000..e63090de --- /dev/null +++ b/src/firmware/restoreOfficialDialog/actions.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +export { + show as firmwareRestoreOfficialDialogShow, + hide as firmwareRestoreOfficialDialogHide, +} from './redux'; diff --git a/src/firmware/restoreOfficialDialog/i18n.ts b/src/firmware/restoreOfficialDialog/i18n.ts new file mode 100644 index 00000000..eb8dc486 --- /dev/null +++ b/src/firmware/restoreOfficialDialog/i18n.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; + +export function useI18n(): TypedI18n { + // istanbul ignore next: babel-loader rewrites this line + const [i18n] = useShopifyI18n(); + return i18n; +} diff --git a/src/firmware/restoreOfficialDialog/reducers.ts b/src/firmware/restoreOfficialDialog/reducers.ts new file mode 100644 index 00000000..88063b7d --- /dev/null +++ b/src/firmware/restoreOfficialDialog/reducers.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import reducer from './redux'; + +export default reducer; diff --git a/src/firmware/restoreOfficialDialog/redux.ts b/src/firmware/restoreOfficialDialog/redux.ts new file mode 100644 index 00000000..f848d574 --- /dev/null +++ b/src/firmware/restoreOfficialDialog/redux.ts @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { createSlice } from '@reduxjs/toolkit'; + +type State = { + isOpen: boolean; +}; + +const initialState: State = { + isOpen: false, +}; + +const slice = createSlice({ + name: 'restoreOfficialDialog', + initialState, + reducers: { + show(state) { + state.isOpen = true; + }, + hide(state) { + state.isOpen = false; + }, + }, +}); + +export const { show, hide } = slice.actions; +export default slice.reducer; diff --git a/src/firmware/restoreOfficialDialog/translations/en.json b/src/firmware/restoreOfficialDialog/translations/en.json new file mode 100644 index 00000000..cd36ad08 --- /dev/null +++ b/src/firmware/restoreOfficialDialog/translations/en.json @@ -0,0 +1,13 @@ +{ + "title": "Restore official LEGO® Firmware", + "poweredUpHubs": { + "title": "Powered Up Hubs", + "message": "The official firmware can be restored on Powered Up hubs by putting the hub in bootloader mode and connecting to the hub using one of the official LEGO apps.", + "action": "Watch video." + }, + "spikeHubs": { + "title": "SPIKE/MINDSTORMS Hubs", + "message": "The official LEGO software for these hubs does not have a way to restore the firmware on these hubs. So, we have provided a special site to do this for you.", + "action": "Pybricks DFU restore tool." + } +} diff --git a/src/index.scss b/src/index.scss index 795d8739..79a9e6bd 100644 --- a/src/index.scss +++ b/src/index.scss @@ -136,8 +136,14 @@ a.#{bp.$ns}-button { min-width: math.div(bp.$pt-grid-size, 2); } -.#{bp.$ns}-running-text h4:first-child { - margin-top: 0; +.#{bp.$ns}-running-text { + *:first-child { + margin-top: 0; + } + + *:last-child { + margin-bottom: 0; + } } // make scrollbars fit our style diff --git a/src/settings/Settings.test.tsx b/src/settings/Settings.test.tsx index 5f19301d..b6cf7063 100644 --- a/src/settings/Settings.test.tsx +++ b/src/settings/Settings.test.tsx @@ -4,7 +4,8 @@ import { cleanup, getByLabelText, waitFor } from '@testing-library/react'; import React from 'react'; import { testRender } from '../../test'; -import { firmwareInstallPybricks, firmwareRestoreLego } from '../firmware/actions'; +import { firmwareInstallPybricks } from '../firmware/actions'; +import { firmwareRestoreOfficialDialogShow } from '../firmware/restoreOfficialDialog/actions'; import Settings from './Settings'; afterEach(() => { @@ -62,7 +63,7 @@ describe('firmware', () => { }); await user.click(button); - expect(dispatch).toHaveBeenCalledWith(firmwareRestoreLego()); + expect(dispatch).toHaveBeenCalledWith(firmwareRestoreOfficialDialogShow()); }); }); diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 7656a03d..b31f823e 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -21,8 +21,10 @@ import { } from '../app/constants'; import { Button } from '../components/Button'; import HelpButton from '../components/HelpButton'; -import { firmwareInstallPybricks, firmwareRestoreLego } from '../firmware/actions'; +import { firmwareInstallPybricks } from '../firmware/actions'; import { InstallPybricksDialog } from '../firmware/installPybricksDialog/InstallPybricksDialog'; +import RestoreOfficialDialog from '../firmware/restoreOfficialDialog/RestoreOfficialDialog'; +import { firmwareRestoreOfficialDialogShow } from '../firmware/restoreOfficialDialog/actions'; import { pseudolocalize } from '../i18n'; import { useSelector } from '../reducers'; import ExternalLinkIcon from '../utils/ExternalLinkIcon'; @@ -108,8 +110,9 @@ const Settings: React.VoidFunctionComponent = () => { minimal={true} icon="download" label={i18n.translate('firmware.flashLegoButton.label')} - onPress={() => dispatch(firmwareRestoreLego())} + onPress={() => dispatch(firmwareRestoreOfficialDialogShow())} /> +