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
This commit is contained in:
David Lechner
2022-11-18 19:28:00 -06:00
committed by David Lechner
parent 44169bd178
commit 71c8605773
4 changed files with 204 additions and 36 deletions
+23
View File
@@ -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',
}));
@@ -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(<RestoreOfficialDialog />, {
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(<RestoreOfficialDialog />, {
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(<RestoreOfficialDialog />, {
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));
},
);
});
@@ -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 (
<div className={classNames(Classes.DIALOG_BODY, Classes.RUNNING_TEXT)}>
<p>
{i18n.translate('selectHubPanel.message', {
lego: legoRegisteredTrademark,
next: (
<strong>{i18n.translate('selectHubPanel.nextButton')}</strong>
),
})}
</p>
<div className="pb-spacer" />
<HubPicker />
</div>
);
};
const RestoreFirmwarePanel: React.VoidFunctionComponent = () => {
const [hubType] = useHubPickerSelectedHub();
const dispatch = useDispatch();
const i18n = useI18n();
return (
<div className={classNames(Classes.DIALOG_BODY, Classes.RUNNING_TEXT)}>
<BootloaderInstructions hubType={hubType} />
{hubHasUSB(hubType) ? (
<>
<p>
{i18n.translate('restoreFirmwarePanel.instruction2.dfu', {
flashFirmware: (
<strong>
{i18n.translate('restoreFirmwarePanel.flashButton')}
</strong>
),
lego: legoRegisteredTrademark,
})}
</p>
<div className="pb-spacer" />
<Button
intent={Intent.PRIMARY}
onClick={() => dispatch(firmwareRestoreOfficialDfu(hubType))}
>
{i18n.translate('restoreFirmwarePanel.flashButton')}
</Button>
</>
) : (
<p>
{i18n.translate('restoreFirmwarePanel.instruction2.ble.message', {
lego: legoRegisteredTrademark,
thisVideo: (
<>
<a
href={pybricksBleFirmwareRestoreVideoUrl}
target="_blank"
rel="noreferrer"
>
{i18n.translate(
'restoreFirmwarePanel.instruction2.ble.thisVideo',
)}
</a>
<ExternalLinkIcon />
</>
),
})}
</p>
)}
</div>
);
};
const RestoreOfficialDialog: React.VoidFunctionComponent = () => {
const { isOpen } = useSelector((s) => s.firmware.restoreOfficialDialog);
const dispatch = useDispatch();
const i18n = useI18n();
return (
<Dialog
<MultistepDialog
isOpen={isOpen}
title={i18n.translate('title')}
title={i18n.translate('title', { lego: legoRegisteredTrademark })}
onClose={() => dispatch(firmwareRestoreOfficialDialogHide())}
finalButtonProps={{
text: i18n.translate('doneButton.label'),
onClick: () => dispatch(firmwareRestoreOfficialDialogHide()),
}}
>
<div className={classNames(Classes.DIALOG_BODY, Classes.RUNNING_TEXT)}>
<h4>{i18n.translate('poweredUpHubs.title')}</h4>
<p>{i18n.translate('poweredUpHubs.message')}</p>
<p>
<a
href={pybricksBleFirmwareRestoreVideoUrl}
target="_blank"
rel="noreferrer"
>
{i18n.translate('poweredUpHubs.action')}
</a>
<ExternalLinkIcon />
</p>
<h4>{i18n.translate('spikeHubs.title')}</h4>
<p>{i18n.translate('spikeHubs.message')}</p>
<p>
<a href={pybricksDfuRestoreUrl} target="_blank" rel="noreferrer">
{i18n.translate('spikeHubs.action')}
</a>
<ExternalLinkIcon />
</p>
</div>
</Dialog>
<DialogStep
id="hub"
title={i18n.translate('selectHubPanel.title')}
panel={<SelectHubPanel />}
nextButtonProps={{ text: i18n.translate('selectHubPanel.nextButton') }}
/>
<DialogStep
id="restore"
title={i18n.translate('restoreFirmwarePanel.title')}
panel={<RestoreFirmwarePanel />}
/>
</MultistepDialog>
);
};
@@ -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"
}
}