From 76444c032cd5e84e681236c4339006f5300d9ea7 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 26 Jul 2022 12:55:06 -0500 Subject: [PATCH 1/9] firmware/installPybricksDialog: add warning for windows/linux usb This warns the user before failure. Otherwise they will click the flash button and no devices will appear in the scan dialog. --- src/app/constants.ts | 6 +++ .../InstallPybricksDialog.tsx | 40 ++++++++++++++++++- .../translations/en.json | 5 +++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/app/constants.ts b/src/app/constants.ts index 7c0a22af..4bf20841 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -38,6 +38,12 @@ export const pybricksBluetoothTroubleshootingUrl = export const pybricksUsbDfuTroubleshootingUrl = 'https://github.com/pybricks/support/discussions/688'; +export const pybricksUsbDfuWindowsDriverInstallUrl = + 'https://github.com/pybricks/support/discussions/688#discussioncomment-3201466'; + +export const pybricksUsbLinuxUdevRulesUrl = + 'https://github.com/pybricks/support/discussions/688#discussioncomment-3239099'; + /** Pybricks copyright statement. */ export const pybricksCopyright = 'Copyright (c) 2020-2022 The Pybricks Authors'; diff --git a/src/firmware/installPybricksDialog/InstallPybricksDialog.tsx b/src/firmware/installPybricksDialog/InstallPybricksDialog.tsx index 93772705..ec29e1ec 100644 --- a/src/firmware/installPybricksDialog/InstallPybricksDialog.tsx +++ b/src/firmware/installPybricksDialog/InstallPybricksDialog.tsx @@ -4,6 +4,7 @@ import './installPybricksDialog.scss'; import { Button, + Callout, Checkbox, Classes, ControlGroup, @@ -24,7 +25,11 @@ import { Select2 } from '@blueprintjs/select'; import classNames from 'classnames'; import React, { useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { appName } from '../../app/constants'; +import { + appName, + pybricksUsbDfuWindowsDriverInstallUrl, + pybricksUsbLinuxUdevRulesUrl, +} from '../../app/constants'; import HelpButton from '../../components/HelpButton'; import { Hub, @@ -37,6 +42,8 @@ import { HubPicker } from '../../components/hubPicker/HubPicker'; import { FileMetadata } from '../../fileStorage'; import { useFileStorageMetadata } from '../../fileStorage/hooks'; import { useSelector } from '../../reducers'; +import ExternalLinkIcon from '../../utils/ExternalLinkIcon'; +import { isLinux, isWindows } from '../../utils/os'; import { firmwareInstallPybricksDialogAccept, firmwareInstallPybricksDialogCancel, @@ -348,6 +355,37 @@ const BootloaderModePanel: React.VoidFunctionComponent return (
+ {hubHasUSB(hubType) && isLinux() && ( +

+ + {i18n.translate('bootloaderPanel.warning.linux')}{' '} + + {i18n.translate('bootloaderPanel.warning.learnMore')} + + + +

+ )} + {hubHasUSB(hubType) && isWindows() && ( +

+ + {i18n.translate('bootloaderPanel.warning.windows')}{' '} + + {i18n.translate('bootloaderPanel.warning.learnMore')} + + + +

+ )} +

{i18n.translate('bootloaderPanel.instruction1')}

    {hubHasUSB(hubType) && ( diff --git a/src/firmware/installPybricksDialog/translations/en.json b/src/firmware/installPybricksDialog/translations/en.json index 4da3c19d..5f00baed 100644 --- a/src/firmware/installPybricksDialog/translations/en.json +++ b/src/firmware/installPybricksDialog/translations/en.json @@ -55,6 +55,11 @@ }, "bootloaderPanel": { "title": "Place hub in bootloader mode", + "warning": { + "linux": "If you have never used Pybricks with USB on Linux, you will need to configure udev rules to allow permission before you can flash the hub firmware.", + "windows": " If you have never used Pybricks with USB on Windows, you may need to manually install a USB driver before you can flash the hub firmware.", + "learnMore": "Learn more." + }, "instruction1": "To flash the firmware, the hub must be placed in bootloader mode. Follow the steps below to do this:", "button": { "bluetooth": "Bluetooth button", From 4e85b5c1b85d64583fd75991a44b6328f17de55c Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 26 Jul 2022 14:07:36 -0500 Subject: [PATCH 2/9] firmware/installPybricksDialog: rename flash button This makes it clear there are two steps, connect and then flash. --- src/firmware/installPybricksDialog/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firmware/installPybricksDialog/translations/en.json b/src/firmware/installPybricksDialog/translations/en.json index 5f00baed..c3159382 100644 --- a/src/firmware/installPybricksDialog/translations/en.json +++ b/src/firmware/installPybricksDialog/translations/en.json @@ -92,6 +92,6 @@ "label": "Next" }, "flashFirmwareButton": { - "label": "Flash Firmware" + "label": "Connect and flash" } } From 8dadc8826cfbb845c5197ce7541905a339d8f01a Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 26 Jul 2022 15:15:20 -0500 Subject: [PATCH 3/9] firmware/installPybricksDialog: increase height This ensures the dialog doesn't change size when clicking next even with longer translations. --- .../installPybricksDialog/installPybricksDialog.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/firmware/installPybricksDialog/installPybricksDialog.scss b/src/firmware/installPybricksDialog/installPybricksDialog.scss index a031effc..7b0ebf53 100644 --- a/src/firmware/installPybricksDialog/installPybricksDialog.scss +++ b/src/firmware/installPybricksDialog/installPybricksDialog.scss @@ -5,7 +5,7 @@ .pb-firmware-installPybricksDialog { &-body { - min-height: bp.$pt-grid-size * 25; + min-height: bp.$pt-grid-size * 35; } &-license { @@ -17,11 +17,11 @@ &-text { flex-grow: 1; min-height: 0; - max-height: bp.$pt-grid-size * 20; + max-height: bp.$pt-grid-size * 30; overflow: auto; & .#{bp.$ns}-non-ideal-state { - min-height: bp.$pt-grid-size * 20; + min-height: bp.$pt-grid-size * 30; } } } From 1148b169d5733063bfee19d6e7e38093175da7f6 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 26 Jul 2022 15:18:44 -0500 Subject: [PATCH 4/9] firmware: add progress indication for DFU USB flash --- src/firmware/alerts/FlashProgress.tsx | 54 ++++++++++++++++++++++++ src/firmware/alerts/translations/en.json | 4 ++ src/firmware/sagas.ts | 30 +++++++++++-- 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 src/firmware/alerts/FlashProgress.tsx diff --git a/src/firmware/alerts/FlashProgress.tsx b/src/firmware/alerts/FlashProgress.tsx new file mode 100644 index 00000000..3594488e --- /dev/null +++ b/src/firmware/alerts/FlashProgress.tsx @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { Intent, ProgressBar } from '@blueprintjs/core'; +import React from 'react'; +import { CreateToast } from '../../i18nToaster'; +import { useI18n } from './i18n'; + +type FlashProgressProps = { + action: 'erase' | 'flash'; + progress: number | undefined; +}; + +const FlashProgress: React.VoidFunctionComponent = ({ + action, + progress, +}) => { + const i18n = useI18n(); + + return ( + <> + {action === 'erase' && ( +

    + {i18n.translate('flashProgress.erasing', { + percent: progress ? i18n.formatPercentage(progress) : '', + })} +

    + )} + + {action === 'flash' && ( +

    + {i18n.translate('flashProgress.flashing', { + percent: progress ? i18n.formatPercentage(progress) : '', + })} +

    + )} + +

    + +

    + + ); +}; + +export const flashProgress: CreateToast = (onAction, props) => { + return { + message: , + icon: 'download', + intent: Intent.PRIMARY, + // close one second after progress is complete + timeout: (props.progress ?? 0) < 1 ? 0 : 1000, + onDismiss: () => onAction('dismiss'), + }; +}; diff --git a/src/firmware/alerts/translations/en.json b/src/firmware/alerts/translations/en.json index df5a2aa4..7a8f2f77 100644 --- a/src/firmware/alerts/translations/en.json +++ b/src/firmware/alerts/translations/en.json @@ -17,5 +17,9 @@ }, "firmwareMismatch": { "message": "Cannot flash firmware. The firmware file is for a different kind of hub." + }, + "flashProgress": { + "erasing": "Erasing internal flash memory: {percent}", + "flashing": "Writing new firmware: {percent}" } } diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts index 42cfd915..6f7ca1ae 100644 --- a/src/firmware/sagas.ts +++ b/src/firmware/sagas.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020-2022 The Pybricks Authors +import { IToaster } from '@blueprintjs/core'; import { FirmwareReader, FirmwareReaderError, @@ -75,6 +76,7 @@ import { firmwareInstallPybricks, flashFirmware, } from './actions'; +import { flashProgress } from './alerts/FlashProgress'; import { firmwareInstallPybricksDialogAccept, firmwareInstallPybricksDialogCancel, @@ -615,12 +617,10 @@ function* handleFlashUsbDfu(action: ReturnType): Gen // forceInterfacesName is needed to get the flash layout map { forceInterfacesName: true }, { + // NB: info and progress are never called in dfu v0.1.5 info: console.debug, warning: console.warn, - progress: (progress, total) => { - // TODO: bind to eventChannel and dispatch progress actions - console.log(progress, total); - }, + progress: console.debug, }, ); @@ -656,6 +656,28 @@ function* handleFlashUsbDfu(action: ReturnType): Gen dfu.dfuseStartAddress = dfuFirmwareStartAddress; const writeProc = dfu.write(1024, firmware, true); + const toaster = yield* getContext('toaster'); + + writeProc.events.on('erase/process', (sent, total) => { + toaster.show( + flashProgress(() => undefined, { + action: 'erase', + progress: sent / total, + }), + 'firmware.dfu.progress', + ); + }); + + writeProc.events.on('write/process', (sent, total) => { + toaster.show( + flashProgress(() => undefined, { + action: 'flash', + progress: sent / total, + }), + 'firmware.dfu.progress', + ); + }); + writeProc.events.on('error', console.error); // REVISIT: we could possibly race the 'write/end' and 'error' events From 2b7f1b1cbf8c7dc3c0ba085384098568fd76214f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 26 Jul 2022 16:05:41 -0500 Subject: [PATCH 5/9] firmware/sagas: show progress for BLE flashing --- src/firmware/alerts/ReleaseButton.tsx | 21 +++++++++++ src/firmware/alerts/index.ts | 2 ++ src/firmware/alerts/translations/en.json | 3 ++ src/firmware/sagas.test.ts | 46 +++++++++++++++++++++++- src/firmware/sagas.ts | 32 +++++++++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/firmware/alerts/ReleaseButton.tsx diff --git a/src/firmware/alerts/ReleaseButton.tsx b/src/firmware/alerts/ReleaseButton.tsx new file mode 100644 index 00000000..a42b6b2a --- /dev/null +++ b/src/firmware/alerts/ReleaseButton.tsx @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { Intent } from '@blueprintjs/core'; +import React from 'react'; +import { CreateToast } from '../../i18nToaster'; +import { useI18n } from './i18n'; + +const ReleaseButton: React.VoidFunctionComponent = () => { + const i18n = useI18n(); + return

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

    ; +}; + +export const releaseButton: CreateToast = (onAction) => { + return { + message: , + icon: 'info-sign', + intent: Intent.PRIMARY, + onDismiss: () => onAction('dismiss'), + }; +}; diff --git a/src/firmware/alerts/index.ts b/src/firmware/alerts/index.ts index 82ea36ab..eba5d418 100644 --- a/src/firmware/alerts/index.ts +++ b/src/firmware/alerts/index.ts @@ -5,10 +5,12 @@ import { firmwareMismatch } from './FirmwareMismatch'; import { noDfuHub } from './NoDfuHub'; import { noDfuInterface } from './NoDfuInterface'; import { noWebUsb } from './NoWebUsb'; +import { releaseButton } from './ReleaseButton'; export default { firmwareMismatch, noDfuHub, noDfuInterface, noWebUsb, + releaseButton, }; diff --git a/src/firmware/alerts/translations/en.json b/src/firmware/alerts/translations/en.json index 7a8f2f77..4777c5b2 100644 --- a/src/firmware/alerts/translations/en.json +++ b/src/firmware/alerts/translations/en.json @@ -21,5 +21,8 @@ "flashProgress": { "erasing": "Erasing internal flash memory: {percent}", "flashing": "Writing new firmware: {percent}" + }, + "releaseButton": { + "message": "You may release the power button on the hub now." } } diff --git a/src/firmware/sagas.test.ts b/src/firmware/sagas.test.ts index 5d0a4ba6..e9ae1c9c 100644 --- a/src/firmware/sagas.test.ts +++ b/src/firmware/sagas.test.ts @@ -1,13 +1,16 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2021-2022 The Pybricks Authors +import { IToaster } from '@blueprintjs/core'; import { FirmwareMetadata, FirmwareReaderError, FirmwareReaderErrorCode, } from '@pybricks/firmware'; +import { mock } from 'jest-mock-extended'; import JSZip from 'jszip'; import { AsyncSaga } from '../../test'; +import { alertsShowAlert } from '../alerts/actions'; import { BootloaderConnectionFailureReason, checksumRequest, @@ -78,6 +81,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -130,6 +134,9 @@ describe('flashFirmware', () => { // erase first + action = await saga.take(); + expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton')); + action = await saga.take(); expect(action).toEqual(eraseRequest(1, /* isCityHub */ false)); @@ -224,6 +231,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -273,6 +281,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -340,6 +349,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -403,6 +413,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -469,6 +480,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -528,6 +540,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -593,6 +606,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -675,6 +689,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -740,6 +755,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -790,6 +806,9 @@ describe('flashFirmware', () => { // erase first + action = await saga.take(); + expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton')); + action = await saga.take(); expect(action).toEqual(eraseRequest(1, /* isCityHub */ false)); @@ -835,6 +854,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -885,6 +905,9 @@ describe('flashFirmware', () => { // erase first + action = await saga.take(); + expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton')); + action = await saga.take(); expect(action).toEqual(eraseRequest(1, /* isCityHub */ false)); @@ -939,6 +962,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -989,6 +1013,9 @@ describe('flashFirmware', () => { // erase first + action = await saga.take(); + expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton')); + action = await saga.take(); expect(action).toEqual(eraseRequest(1, /* isCityHub */ false)); @@ -1083,6 +1110,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -1133,6 +1161,9 @@ describe('flashFirmware', () => { // erase first + action = await saga.take(); + expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton')); + action = await saga.take(); expect(action).toEqual(eraseRequest(1, /* isCityHub */ false)); @@ -1225,6 +1256,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -1283,6 +1315,9 @@ describe('flashFirmware', () => { // erase first + action = await saga.take(); + expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton')); + action = await saga.take(); expect(action).toEqual(eraseRequest(1, /* isCityHub */ false)); @@ -1374,6 +1409,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -1421,6 +1457,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -1467,6 +1504,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -1527,6 +1565,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -1588,6 +1627,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -1652,6 +1692,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -1741,6 +1782,7 @@ describe('flashFirmware', () => { const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc(), + toaster: mock(), }); // saga is triggered by this action @@ -1791,12 +1833,14 @@ describe('flashFirmware', () => { // erase first + action = await saga.take(); + expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton')); + action = await saga.take(); expect(action).toEqual(eraseRequest(1, /* isCityHub */ false)); saga.put(didRequest(1)); saga.put(eraseResponse(Result.OK)); - // then write the new firmware const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8; diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts index 6f7ca1ae..8cfeed59 100644 --- a/src/firmware/sagas.ts +++ b/src/firmware/sagas.ts @@ -94,6 +94,10 @@ const firmwareZipMap = new Map([ * parent task). */ function* disconnectAndCancel(): SagaGenerator { + const toaster = yield* getContext('toaster'); + + toaster.dismiss('firmware.ble.progress'); + const connection = yield* select((s: RootState) => s.bootloader.connection); if (connection === BootloaderConnectionState.Connected) { @@ -328,6 +332,8 @@ function* loadFirmware( * @param action The action that triggered this saga. */ function* handleFlashFirmware(action: ReturnType): Generator { + const toaster = yield* getContext('toaster'); + try { let firmware: Uint8Array | undefined = undefined; let deviceId: HubType | undefined = undefined; @@ -418,6 +424,16 @@ function* handleFlashFirmware(action: ReturnType): Generat yield* put(didStart()); + toaster.show( + flashProgress(() => undefined, { + action: 'erase', + progress: undefined, + }), + 'firmware.ble.progress', + ); + + yield* put(alertsShowAlert('firmware', 'releaseButton')); + const eraseAction = yield* put( eraseRequest(nextMessageId(), deviceId === HubType.CityHub), ); @@ -469,6 +485,14 @@ function* handleFlashFirmware(action: ReturnType): Generat yield* put(didProgress(offset / firmware.length)); + toaster.show( + flashProgress(() => undefined, { + action: 'flash', + progress: offset / firmware.length, + }), + 'firmware.ble.progress', + ); + // we don't want to request checksum if this is the last packet since // the bootloader will send a response to the program request already. offset += maxDataSize; @@ -542,6 +566,14 @@ function* handleFlashFirmware(action: ReturnType): Generat yield* put(didProgress(1)); + toaster.show( + flashProgress(() => undefined, { + action: 'flash', + progress: 1, + }), + 'firmware.ble.progress', + ); + // this will cause the remote device to disconnect and reboot const rebootAction = yield* put(rebootRequest(nextMessageId())); yield* waitForDidRequest(rebootAction.id); From 0414e08e4f8b9a6f4203d4b4e5eea32f8e23b8f8 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 26 Jul 2022 16:43:55 -0500 Subject: [PATCH 6/9] drop blueprints-icons we no longer override the styles, so this is no longer needed --- blueprints-icons/16px/chevron-right.svg | 9 --------- blueprints-icons/16px/more.svg | 10 ---------- blueprints-icons/16px/small-minus.svg | 9 --------- blueprints-icons/16px/small-tick.svg | 9 --------- blueprints-icons/README.md | 3 --- 5 files changed, 40 deletions(-) delete mode 100644 blueprints-icons/16px/chevron-right.svg delete mode 100644 blueprints-icons/16px/more.svg delete mode 100644 blueprints-icons/16px/small-minus.svg delete mode 100644 blueprints-icons/16px/small-tick.svg delete mode 100644 blueprints-icons/README.md diff --git a/blueprints-icons/16px/chevron-right.svg b/blueprints-icons/16px/chevron-right.svg deleted file mode 100644 index 2e4fa2b8..00000000 --- a/blueprints-icons/16px/chevron-right.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/blueprints-icons/16px/more.svg b/blueprints-icons/16px/more.svg deleted file mode 100644 index 28c0caac..00000000 --- a/blueprints-icons/16px/more.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/blueprints-icons/16px/small-minus.svg b/blueprints-icons/16px/small-minus.svg deleted file mode 100644 index ee4cad78..00000000 --- a/blueprints-icons/16px/small-minus.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/blueprints-icons/16px/small-tick.svg b/blueprints-icons/16px/small-tick.svg deleted file mode 100644 index e2e4529a..00000000 --- a/blueprints-icons/16px/small-tick.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/blueprints-icons/README.md b/blueprints-icons/README.md deleted file mode 100644 index f36331e8..00000000 --- a/blueprints-icons/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Icons copied from: https://github.com/palantir/blueprint/tree/develop/resources/icons/16px - -Needed to work around: https://github.com/palantir/blueprint/issues/4759 From 8bece7d9573fd7855799a79166c2c028746d4d33 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Wed, 27 Jul 2022 15:18:58 -0500 Subject: [PATCH 7/9] tour: add new tour component --- CHANGELOG.md | 1 + package.json | 1 + src/activities/Activities.test.tsx | 3 +- src/activities/Activities.tsx | 20 +- src/activities/hooks.ts | 22 ++ src/app/App.tsx | 2 + src/components/Button.tsx | 4 + src/explorer/Explorer.tsx | 6 +- src/reducers.ts | 2 + src/settings/Settings.tsx | 2 + src/toolbar/Toolbar.tsx | 11 +- src/toolbar/buttons/tour/TourButton.test.tsx | 23 ++ src/toolbar/buttons/tour/TourButton.tsx | 29 ++ src/toolbar/buttons/tour/i18n.ts | 12 + src/toolbar/buttons/tour/icon.svg | 1 + src/toolbar/buttons/tour/translations/en.json | 4 + src/tour/Tour.tsx | 255 ++++++++++++++++++ src/tour/actions.ts | 4 + src/tour/i18n.ts | 12 + src/tour/reducers.ts | 6 + src/tour/redux/tour.ts | 28 ++ src/tour/translations/en.json | 33 +++ yarn.lock | 93 ++++++- 23 files changed, 552 insertions(+), 22 deletions(-) create mode 100644 src/activities/hooks.ts create mode 100644 src/toolbar/buttons/tour/TourButton.test.tsx create mode 100644 src/toolbar/buttons/tour/TourButton.tsx create mode 100644 src/toolbar/buttons/tour/i18n.ts create mode 100644 src/toolbar/buttons/tour/icon.svg create mode 100644 src/toolbar/buttons/tour/translations/en.json create mode 100644 src/tour/Tour.tsx create mode 100644 src/tour/actions.ts create mode 100644 src/tour/i18n.ts create mode 100644 src/tour/reducers.ts create mode 100644 src/tour/redux/tour.ts create mode 100644 src/tour/translations/en.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f1aa2e3..d823ff4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Added better error message when no files to backup ([support#681]). - Added multi-step firmware flashing dialog. - Added support for flashing firmware via USB DFU. +- Added an interactive introductory tour of the app. ### Fixed - Fixed deleting files that are not open in the editor. diff --git a/package.json b/package.json index 9f41c77a..657e0607 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "react-dev-utils": "^12.0.1", "react-dom": "^16.13.1", "react-dropzone": "^14.2.2", + "react-joyride": "^2.5.0", "react-monaco-editor": "^0.49.0", "react-popper": "^2.3.0", "react-redux": "^8.0.2", diff --git a/src/activities/Activities.test.tsx b/src/activities/Activities.test.tsx index d5f0c0dd..315fbeb0 100644 --- a/src/activities/Activities.test.tsx +++ b/src/activities/Activities.test.tsx @@ -4,7 +4,8 @@ import { cleanup } from '@testing-library/react'; import React from 'react'; import { testRender } from '../../test'; -import Activities, { Activity } from './Activities'; +import Activities from './Activities'; +import { Activity } from './hooks'; afterEach(() => { cleanup(); diff --git a/src/activities/Activities.tsx b/src/activities/Activities.tsx index 7eb27f31..9eb60787 100644 --- a/src/activities/Activities.tsx +++ b/src/activities/Activities.tsx @@ -4,32 +4,18 @@ import './activities.scss'; import { Icon, Tab, Tabs } from '@blueprintjs/core'; import React, { useCallback, useEffect, useRef } from 'react'; -import { useLocalStorage } from 'usehooks-ts'; import Explorer from '../explorer/Explorer'; import Settings from '../settings/Settings'; +import { Activity, useActivitiesSelectedActivity } from './hooks'; import { useI18n } from './i18n'; -/** Indicates the selected activity. */ -export enum Activity { - /** No activity is selected. */ - None = 'activity.none', - /** The explorer activity is selected. */ - Explorer = 'activity.explorer', - /** The settings activity is selected. */ - Settings = 'activity.settings', -} - /** * React component that acts as a tab control to select activities. */ const Activities: React.VoidFunctionComponent = () => { + const [selectedActivity, setSelectedActivity] = useActivitiesSelectedActivity(); const i18n = useI18n(); - const [selectedActivity, setSelectedActivity] = useLocalStorage( - 'activities.selectedActivity', - Activity.Explorer, - ); - const handleAction = useCallback( (newActivity: Activity) => { // if activity is already selected, select none @@ -110,6 +96,7 @@ const Activities: React.VoidFunctionComponent = () => { ref={tabsRef} > { onMouseDown={(e) => e.stopPropagation()} /> { @@ -196,6 +197,7 @@ const App: React.VFC = () => {
+ ); }; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 63b13b21..7bf1235d 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -8,6 +8,8 @@ import React, { useRef } from 'react'; import { FocusRing, useButton } from 'react-aria'; type ButtonProps = { + /** Optional DOM ID for the button. */ + id?: string; /** The label for the button. */ label: string; /** If true, the label will not be visible (will use aria-label instead). */ @@ -28,6 +30,7 @@ type ButtonProps = { /** Similar to Blueprint.js button with better accessibility. */ export const Button: React.VoidFunctionComponent = ({ + id, label, hideLabel, description, @@ -59,6 +62,7 @@ export const Button: React.VoidFunctionComponent = ({ {/* useButton() breaks the native browser :focus-visible :-/ */}