From 71c8605773b297a862a6092d63300fafedb8b65d Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 18 Nov 2022 15:22:46 -0600 Subject: [PATCH] firmware/restoreOfficialDialog: change to multistep dialog This gives instructions on how to put the hub in bootloader mode like the install Pybricks dialog and sets the stage for doing the DFU restore without going to another website. Issue: https://github.com/pybricks/pybricks-code/issues/1104 --- src/firmware/actions.ts | 23 ++++ .../RestoreOfficialDialog.test.tsx | 60 ++++++++ .../RestoreOfficialDialog.tsx | 130 ++++++++++++++---- .../translations/en.json | 27 ++-- 4 files changed, 204 insertions(+), 36 deletions(-) create mode 100644 src/firmware/restoreOfficialDialog/RestoreOfficialDialog.test.tsx diff --git a/src/firmware/actions.ts b/src/firmware/actions.ts index a339e39c..051a95e8 100644 --- a/src/firmware/actions.ts +++ b/src/firmware/actions.ts @@ -3,6 +3,7 @@ import { FirmwareReaderError } from '@pybricks/firmware'; import { createAction } from '../actions'; +import { Hub } from '../components/hubPicker'; export enum MetadataProblem { Missing = 'metadata.missing', @@ -406,3 +407,25 @@ export const firmwareDidInstallPybricks = createAction(() => ({ export const firmwareDidFailToInstallPybricks = createAction(() => ({ type: 'firmware.action.didFailToInstallPybricks', })); + +/** + * Action that triggers the restore official DFU firmware saga. + */ +export const firmwareRestoreOfficialDfu = createAction((hub: Hub) => ({ + type: 'firmware.action.restoreOfficialDfu', + hub, +})); + +/** + * Action that indicates {@link firmwareRestoreOfficialDfu} succeeded. + */ +export const firmwareDidRestoreOfficialDfu = createAction(() => ({ + type: 'firmware.action.didRestoreOfficialDfu', +})); + +/** + * Action that indicates {@link firmwareRestoreOfficialDfu} failed. + */ +export const firmwareDidFailToRestoreOfficialDfu = createAction(() => ({ + type: 'firmware.action.didFailToRestoreOfficialDfu', +})); diff --git a/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.test.tsx b/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.test.tsx new file mode 100644 index 00000000..1320af01 --- /dev/null +++ b/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.test.tsx @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { cleanup } from '@testing-library/react'; +import React from 'react'; +import { testRender } from '../../../test'; +import { Hub } from '../../components/hubPicker'; +import { firmwareRestoreOfficialDfu } from '../actions'; +import RestoreOfficialDialog from './RestoreOfficialDialog'; +import { firmwareRestoreOfficialDialogHide } from './actions'; + +afterEach(() => { + cleanup(); + jest.resetAllMocks(); + localStorage.clear(); +}); + +describe('closing', () => { + it('should close when close button is clicked', async () => { + const [user, dialog, dispatch] = testRender(, { + firmware: { restoreOfficialDialog: { isOpen: true } }, + }); + + await user.click(dialog.getByRole('button', { name: 'Close' })); + + expect(dispatch).toHaveBeenCalledWith(firmwareRestoreOfficialDialogHide()); + }); + + it('should close when done button is clicked', async () => { + const [user, dialog, dispatch] = testRender(, { + firmware: { restoreOfficialDialog: { isOpen: true } }, + }); + + await user.click(dialog.getByRole('button', { name: 'Next' })); + await user.click(dialog.getByRole('button', { name: 'Done' })); + + expect(dispatch).toHaveBeenCalledWith(firmwareRestoreOfficialDialogHide()); + }); +}); + +describe('flashing', () => { + it.each([ + ['SPIKE Prime Hub', Hub.Prime], + ['SPIKE Essential Hub', Hub.Essential], + ['MINDSTORMS Robot Inventor Hub', Hub.Inventor], + ])( + 'should flash %s when flash button is clicked', + async (hubName: string, hub: Hub) => { + const [user, dialog, dispatch] = testRender(, { + firmware: { restoreOfficialDialog: { isOpen: true } }, + }); + + await user.click(dialog.getByRole('radio', { name: hubName })); + await user.click(dialog.getByRole('button', { name: 'Next' })); + await user.click(dialog.getByRole('button', { name: 'Flash' })); + + expect(dispatch).toHaveBeenCalledWith(firmwareRestoreOfficialDfu(hub)); + }, + ); +}); diff --git a/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.tsx b/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.tsx index d59b5951..26b581f5 100644 --- a/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.tsx +++ b/src/firmware/restoreOfficialDialog/RestoreOfficialDialog.tsx @@ -1,53 +1,129 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -import { Classes, Dialog } from '@blueprintjs/core'; +import { + Button, + Classes, + DialogStep, + Intent, + MultistepDialog, +} from '@blueprintjs/core'; import classNames from 'classnames'; import React from 'react'; import { useDispatch } from 'react-redux'; import { + legoRegisteredTrademark, pybricksBleFirmwareRestoreVideoUrl, - pybricksDfuRestoreUrl, } from '../../app/constants'; +import { hubHasUSB } from '../../components/hubPicker'; +import { HubPicker } from '../../components/hubPicker/HubPicker'; +import { useHubPickerSelectedHub } from '../../components/hubPicker/hooks'; import { useSelector } from '../../reducers'; import ExternalLinkIcon from '../../utils/ExternalLinkIcon'; +import { firmwareRestoreOfficialDfu } from '../actions'; +import BootloaderInstructions from '../bootloaderInstructions/BootloaderInstructions'; import { firmwareRestoreOfficialDialogHide } from './actions'; import { useI18n } from './i18n'; +const SelectHubPanel: React.VoidFunctionComponent = () => { + const i18n = useI18n(); + + return ( +
+

+ {i18n.translate('selectHubPanel.message', { + lego: legoRegisteredTrademark, + next: ( + {i18n.translate('selectHubPanel.nextButton')} + ), + })} +

+
+ +
+ ); +}; + +const RestoreFirmwarePanel: React.VoidFunctionComponent = () => { + const [hubType] = useHubPickerSelectedHub(); + const dispatch = useDispatch(); + const i18n = useI18n(); + + return ( +
+ + {hubHasUSB(hubType) ? ( + <> +

+ {i18n.translate('restoreFirmwarePanel.instruction2.dfu', { + flashFirmware: ( + + {i18n.translate('restoreFirmwarePanel.flashButton')} + + ), + lego: legoRegisteredTrademark, + })} +

+
+ + + ) : ( +

+ {i18n.translate('restoreFirmwarePanel.instruction2.ble.message', { + lego: legoRegisteredTrademark, + thisVideo: ( + <> + + {i18n.translate( + 'restoreFirmwarePanel.instruction2.ble.thisVideo', + )} + + + + ), + })} +

+ )} +
+ ); +}; + const RestoreOfficialDialog: React.VoidFunctionComponent = () => { const { isOpen } = useSelector((s) => s.firmware.restoreOfficialDialog); const dispatch = useDispatch(); const i18n = useI18n(); return ( - dispatch(firmwareRestoreOfficialDialogHide())} + finalButtonProps={{ + text: i18n.translate('doneButton.label'), + onClick: () => 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')} - - -

-
-
+ } + nextButtonProps={{ text: i18n.translate('selectHubPanel.nextButton') }} + /> + } + /> + ); }; diff --git a/src/firmware/restoreOfficialDialog/translations/en.json b/src/firmware/restoreOfficialDialog/translations/en.json index cd36ad08..95727a3e 100644 --- a/src/firmware/restoreOfficialDialog/translations/en.json +++ b/src/firmware/restoreOfficialDialog/translations/en.json @@ -1,13 +1,22 @@ { - "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." + "title": "Restore official {lego} Firmware", + "doneButton": { + "label": "Done" }, - "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." + "selectHubPanel": { + "title": "Select hub", + "message": "Select the hub to restore the official {lego} firmware on, then click {next}.", + "nextButton": "Next" + }, + "restoreFirmwarePanel": { + "title": "Restore firmware", + "instruction2": { + "ble": { + "message": "Then use the official {lego} app for this hub to connect to the hub and restore the firmware. Watch {thisVideo} to see how.", + "thisVideo": "this video" + }, + "dfu": "Then click the {flashFirmware} button below to connect to the hub and flash the firmware. After flashing is complete, connect the hub to one of the official {lego} apps and use it to update to the latest official firmware." + }, + "flashButton": "Flash" } }