firmware/dfuWindowsDriverInstallDialog: move instructions in app

This replaces the link for Windows DFU USB driver installation
instructions with a new in-app dialog containing the instructions.

Fixes: https://github.com/pybricks/support/issues/858
This commit is contained in:
David Lechner
2022-12-19 15:17:31 -06:00
committed by David Lechner
parent 601b111ec4
commit 5d3c74b777
16 changed files with 654 additions and 40 deletions
@@ -0,0 +1,43 @@
// 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 DfuWindowsDriverInstallDialog from './DfuWindowsDriverInstallDialog';
import { firmwareDfuWindowsDriverInstallDialogDialogHide } from './actions';
afterEach(() => {
cleanup();
jest.resetAllMocks();
localStorage.clear();
sessionStorage.clear();
});
it('should dispatch action when the close button is pressed', async () => {
const [user, dialog, dispatch] = testRender(<DfuWindowsDriverInstallDialog />, {
firmware: { dfuWindowsDriverInstallDialog: { isOpen: true } },
});
await user.click(dialog.getByRole('button', { name: 'Close' }));
expect(dispatch).toHaveBeenCalledWith(
firmwareDfuWindowsDriverInstallDialogDialogHide(),
);
});
it('should navigate when next button is pressed and dispatch action when the done button is pressed', async () => {
const [user, dialog, dispatch] = testRender(<DfuWindowsDriverInstallDialog />, {
firmware: { dfuWindowsDriverInstallDialog: { isOpen: true } },
});
for (let i = 1; i < 9; i++) {
await user.click(dialog.getByRole('button', { name: 'Next' }));
}
await user.click(dialog.getByRole('button', { name: 'Done' }));
expect(dispatch).toHaveBeenCalledWith(
firmwareDfuWindowsDriverInstallDialogDialogHide(),
);
});
@@ -0,0 +1,407 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import './dfuWindowsDriverInstallDialog.scss';
import {
Card,
Classes,
DialogStep,
Elevation,
MultistepDialog,
} from '@blueprintjs/core';
import React from 'react';
import { useDispatch } from 'react-redux';
import { Hub } from '../../components/hubPicker';
import { useHubPickerSelectedHub } from '../../components/hubPicker/hooks';
import { useSelector } from '../../reducers';
import { firmwareDfuWindowsDriverInstallDialogDialogHide } from './actions';
import { useI18n } from './i18n';
const dfuWindows1OpenDeviceManagerUrl = new URL(
'@pybricks/images/dfu_windows_1_open_device_manager.png',
import.meta.url,
);
const dfuWindows2LargeHubOpenPropertiesUrl = new URL(
'@pybricks/images/dfu_windows_2_large_hub_open_properties.png',
import.meta.url,
);
const dfuWindows2SmallHubOpenPropertiesUrl = new URL(
'@pybricks/images/dfu_windows_2_small_hub_open_properties.png',
import.meta.url,
);
const dfuWindows3LargeHubPropertiesUrl = new URL(
'@pybricks/images/dfu_windows_3_large_hub_properties.png',
import.meta.url,
);
const dfuWindows3SmallHubPropertiesUrl = new URL(
'@pybricks/images/dfu_windows_3_small_hub_properties.png',
import.meta.url,
);
const dfuWindows4LargeHubUpdateDriverUrl = new URL(
'@pybricks/images/dfu_windows_4_large_hub_update_driver.png',
import.meta.url,
);
const dfuWindows4SmallHubUpdateDriverUrl = new URL(
'@pybricks/images/dfu_windows_4_small_hub_update_driver.png',
import.meta.url,
);
const dfuWindows5LargeHubUpdateDriverPickUrl = new URL(
'@pybricks/images/dfu_windows_5_large_hub_update_driver_pick.png',
import.meta.url,
);
const dfuWindows5SmallHubUpdateDriverPickUrl = new URL(
'@pybricks/images/dfu_windows_5_small_hub_update_driver_pick.png',
import.meta.url,
);
const dfuWindows6LargeHubUpdateDriverSelectTypeUrl = new URL(
'@pybricks/images/dfu_windows_6_large_hub_update_driver_select_type.png',
import.meta.url,
);
const dfuWindows6SmallHubUpdateDriverSelectTypeUrl = new URL(
'@pybricks/images/dfu_windows_6_small_hub_update_driver_select_type.png',
import.meta.url,
);
const dfuWindows7LargeHubUpdateDriverSelectDriverUrl = new URL(
'@pybricks/images/dfu_windows_7_large_hub_update_driver_select_driver.png',
import.meta.url,
);
const dfuWindows7SmallHubUpdateDriverSelectDriverUrl = new URL(
'@pybricks/images/dfu_windows_7_small_hub_update_driver_select_driver.png',
import.meta.url,
);
const dfuWindows8UpdateDriverWarningUrl = new URL(
'@pybricks/images/dfu_windows_8_update_driver_warning.png',
import.meta.url,
);
const dfuWindows9LargeHubUpdateDriverDoneUrl = new URL(
'@pybricks/images/dfu_windows_9_large_hub_update_driver_done.png',
import.meta.url,
);
const dfuWindows9SmallHubUpdateDriverDoneUrl = new URL(
'@pybricks/images/dfu_windows_9_small_hub_update_driver_done.png',
import.meta.url,
);
// These names are hard-coded in the hub bootloader, so they don't get translated
const dfuSmallHubUsbName = 'LEGO Technic Small Hub in DFU Mode';
const dfuLargeHubUsbName = 'LEGO Technic Large Hub in DFU Mode';
type ScreenshotProps = {
url: URL;
};
const Screenshot: React.VoidFunctionComponent<ScreenshotProps> = ({ url }) => {
return (
<Card elevation={Elevation.FOUR}>
<img src={url.toString()} alt="Screenshot" width={673} height={523} />
</Card>
);
};
type HubTypeProps = {
hub: Hub;
};
const Step1: React.VoidFunctionComponent = () => {
const i18n = useI18n();
return (
<div className={Classes.DIALOG_BODY}>
<Screenshot url={dfuWindows1OpenDeviceManagerUrl} />
<div className="pb-spacer" />
<div className={Classes.RUNNING_TEXT}>
<p>
{i18n.translate('step.1.message', {
startMenu: <em>{i18n.translate('step.1.startMenu')}</em>,
deviceManager: (
<em>{i18n.translate('step.1.deviceManager')}</em>
),
})}
</p>
</div>
</div>
);
};
const Step2: React.VoidFunctionComponent<HubTypeProps> = ({ hub }) => {
const i18n = useI18n();
return (
<div className={Classes.DIALOG_BODY}>
<Screenshot
url={
hub === Hub.Essential
? dfuWindows2SmallHubOpenPropertiesUrl
: dfuWindows2LargeHubOpenPropertiesUrl
}
/>
<div className="pb-spacer" />
<div className={Classes.RUNNING_TEXT}>
<p>
{i18n.translate('step.2.message', {
dfuUsbHubName: (
<em>
{hub === Hub.Essential
? dfuSmallHubUsbName
: dfuLargeHubUsbName}
</em>
),
otherDevices: <em>{i18n.translate('step.2.otherDevices')}</em>,
properties: <em>{i18n.translate('step.2.properties')}</em>,
})}
</p>
</div>
</div>
);
};
const Step3: React.VoidFunctionComponent<HubTypeProps> = ({ hub }) => {
const i18n = useI18n();
return (
<div className={Classes.DIALOG_BODY}>
<Screenshot
url={
hub === Hub.Essential
? dfuWindows3SmallHubPropertiesUrl
: dfuWindows3LargeHubPropertiesUrl
}
/>
<div className="pb-spacer" />
<div className={Classes.RUNNING_TEXT}>
<p>
{i18n.translate('step.3.message', {
properties: (
<em>
{i18n.translate('step.3.properties', {
dfuUsbHubName: (
<em>
{hub === Hub.Essential
? dfuSmallHubUsbName
: dfuLargeHubUsbName}
</em>
),
})}
</em>
),
updateDriver: <em>{i18n.translate('step.3.updateDriver')}</em>,
})}
</p>
</div>
</div>
);
};
const Step4: React.VoidFunctionComponent<HubTypeProps> = ({ hub }) => {
const i18n = useI18n();
return (
<div className={Classes.DIALOG_BODY}>
<Screenshot
url={
hub === Hub.Essential
? dfuWindows4SmallHubUpdateDriverUrl
: dfuWindows4LargeHubUpdateDriverUrl
}
/>
<div className="pb-spacer" />
<div className={Classes.RUNNING_TEXT}>
<p>
{i18n.translate('step.4.message', {
updateDrivers: (
<em>{i18n.translate('step.4.updateDrivers')}</em>
),
browse: <em>{i18n.translate('step.4.browse')}</em>,
})}
</p>
</div>
</div>
);
};
const Step5: React.VoidFunctionComponent<HubTypeProps> = ({ hub }) => {
const i18n = useI18n();
return (
<div className={Classes.DIALOG_BODY}>
<Screenshot
url={
hub === Hub.Essential
? dfuWindows5SmallHubUpdateDriverPickUrl
: dfuWindows5LargeHubUpdateDriverPickUrl
}
/>
<div className="pb-spacer" />
<div className={Classes.RUNNING_TEXT}>
<p>
{i18n.translate('step.5.message', {
letMePick: <em>{i18n.translate('step.5.letMePick')}</em>,
})}
</p>
</div>
</div>
);
};
const Step6: React.VoidFunctionComponent<HubTypeProps> = ({ hub }) => {
const i18n = useI18n();
return (
<div className={Classes.DIALOG_BODY}>
<Screenshot
url={
hub === Hub.Essential
? dfuWindows6SmallHubUpdateDriverSelectTypeUrl
: dfuWindows6LargeHubUpdateDriverSelectTypeUrl
}
/>
<div className="pb-spacer" />
<div className={Classes.RUNNING_TEXT}>
<p>
{i18n.translate('step.6.message', {
usbDevices: <em>{i18n.translate('step.6.usbDevices')}</em>,
next: <em>{i18n.translate('step.6.next')}</em>,
})}
</p>
</div>
</div>
);
};
const Step7: React.VoidFunctionComponent<HubTypeProps> = ({ hub }) => {
const i18n = useI18n();
return (
<div className={Classes.DIALOG_BODY}>
<Screenshot
url={
hub === Hub.Essential
? dfuWindows7SmallHubUpdateDriverSelectDriverUrl
: dfuWindows7LargeHubUpdateDriverSelectDriverUrl
}
/>
<div className="pb-spacer" />
<div className={Classes.RUNNING_TEXT}>
<p>
{i18n.translate('step.7.message', {
manufacturer: <em>{i18n.translate('step.7.manufacturer')}</em>,
winUsbDevice: <em>{i18n.translate('step.7.winUsbDevice')}</em>,
model: <em>{i18n.translate('step.7.model')}</em>,
next: <em>{i18n.translate('step.7.next')}</em>,
})}
</p>
</div>
</div>
);
};
const Step8: React.VoidFunctionComponent = () => {
const i18n = useI18n();
return (
<div className={Classes.DIALOG_BODY}>
<Screenshot url={dfuWindows8UpdateDriverWarningUrl} />
<div className="pb-spacer" />
<div className={Classes.RUNNING_TEXT}>
<p>
{i18n.translate('step.8.message', {
yes: <em>{i18n.translate('step.8.yes')}</em>,
})}
</p>
</div>
</div>
);
};
const Step9: React.VoidFunctionComponent<HubTypeProps> = ({ hub }) => {
const i18n = useI18n();
return (
<div className={Classes.DIALOG_BODY}>
<Screenshot
url={
hub === Hub.Essential
? dfuWindows9SmallHubUpdateDriverDoneUrl
: dfuWindows9LargeHubUpdateDriverDoneUrl
}
/>
<div className="pb-spacer" />
<div className={Classes.RUNNING_TEXT}>
<p>
{i18n.translate('step.9.message', {
close: <em>{i18n.translate('step.9.close')}</em>,
})}
</p>
</div>
</div>
);
};
const DfuWindowsDriverInstallDialog: React.VoidFunctionComponent = () => {
const [hub] = useHubPickerSelectedHub();
const { isOpen } = useSelector((s) => s.firmware.dfuWindowsDriverInstallDialog);
const dispatch = useDispatch();
const i18n = useI18n();
return (
<MultistepDialog
title={i18n.translate('title')}
className="pb-dfu-windows-driver-install-dialog"
isOpen={isOpen}
onClose={() => dispatch(firmwareDfuWindowsDriverInstallDialogDialogHide())}
finalButtonProps={{
text: i18n.translate('doneButton.label'),
onClick: () =>
dispatch(firmwareDfuWindowsDriverInstallDialogDialogHide()),
}}
>
<DialogStep
id="1"
panel={<Step1 />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
<DialogStep
id="2"
panel={<Step2 hub={hub} />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
<DialogStep
id="3"
panel={<Step3 hub={hub} />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
<DialogStep
id="4"
panel={<Step4 hub={hub} />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
<DialogStep
id="5"
panel={<Step5 hub={hub} />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
<DialogStep
id="6"
panel={<Step6 hub={hub} />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
<DialogStep
id="7"
panel={<Step7 hub={hub} />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
<DialogStep
id="8"
panel={<Step8 />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
<DialogStep
id="9"
panel={<Step9 hub={hub} />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
</MultistepDialog>
);
};
export default DfuWindowsDriverInstallDialog;
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { createAction } from '../../actions';
/** Actions that request the DFU Windows USB driver install dialog to be shown. */
export const firmwareDfuWindowsDriverInstallDialogDialogShow = createAction(() => ({
type: 'firmware.dfuWindowsDriverInstallDialog.action.show',
}));
/** Actions that request the DFU Windows USB driver install dialog to be hidden. */
export const firmwareDfuWindowsDriverInstallDialogDialogHide = createAction(() => ({
type: 'firmware.dfuWindowsDriverInstallDialog.action.hide',
}));
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
@use '@blueprintjs/core/lib/scss/variables' as bp;
.pb-dfu-windows-driver-install-dialog {
& .#{bp.$ns}-multistep-dialog-panels {
min-height: bp.$pt-grid-size * 75;
& .#{bp.$ns}-multistep-dialog-left-panel {
display: none;
}
& .#{bp.$ns}-multistep-dialog-right-panel {
display: flex;
flex-direction: column;
& .#{bp.$ns}-card {
margin-block: bp.$pt-grid-size * 1.5;
margin-inline: auto;
padding: unset;
border-radius: unset;
width: fit-content;
height: fit-content;
& img {
display: block;
}
}
}
}
}
@@ -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<typeof translations> {
// istanbul ignore next: babel-loader rewrites this line
const [i18n] = useShopifyI18n();
return i18n;
}
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { Reducer, combineReducers } from '@reduxjs/toolkit';
import {
firmwareDfuWindowsDriverInstallDialogDialogHide,
firmwareDfuWindowsDriverInstallDialogDialogShow,
} from './actions';
/** Controls the flash Pybricks firmware dialog open state. */
const isOpen: Reducer<boolean> = (state = false, action) => {
if (firmwareDfuWindowsDriverInstallDialogDialogShow.matches(action)) {
return true;
}
if (firmwareDfuWindowsDriverInstallDialogDialogHide.matches(action)) {
return false;
}
return state;
};
export default combineReducers({ isOpen });
@@ -0,0 +1,55 @@
{
"title": "Windows DFU USB driver installation instructions",
"nextButton": {
"label": "Next"
},
"doneButton": {
"label": "Done"
},
"step": {
"1": {
"message": "Right-click the Windows {startMenu} to open the context menu, then click {deviceManager} to open the Windows Device Manager.",
"startMenu": "Start Menu",
"deviceManager": "Device Manager"
},
"2": {
"message": "In the Device Manager window, look for {dfuUsbHubName} under {otherDevices}. If it is there, right-click it and select {properties}. If it is not there, then the driver is already installed and you do not need to continue.",
"otherDevices": "Other Devices",
"properties": "Properties"
},
"3": {
"message": "In the {properties} dialog, click the {updateDriver} button.",
"properties": "{dfuUsbHubName} Properties",
"updateDriver": "Update Driver..."
},
"4": {
"message": "In the {updateDrivers} dialog, click the {browse} button.",
"updateDrivers": "Update Drivers",
"browse": "Browse my computer for drivers"
},
"5": {
"message": "Then click the {letMePick} button.",
"letMePick": "Let me pick from a list of available drivers on my computer"
},
"6": {
"message": "In the list of device types, select {usbDevices}, then click the {next} button.",
"usbDevices": "Universal Serial Bus devices",
"next": "Next"
},
"7": {
"message": "In the {manufacturer} list, select {winUsbDevice}. Then in the {model} list, also select {winUsbDevice}. Then click the {next} button.",
"manufacturer": "Manufacturer",
"winUsbDevice": "WinUsb Device",
"model": "Model",
"next": "Next"
},
"8": {
"message": "A warning dialog will open stating that the driver is not recommended. Click the {yes} button to use the driver anyway. The the driver is compatible.",
"yes": "Yes"
},
"9": {
"message": "The driver installation is complete. Click the {close} button.",
"close": "Close"
}
}
}