components/hubPicker: back state with localStorage

This saves the selected hub in localStorage. This will "remember" the
state between app launches and also synchronize the state between
various users of the component. Both of these should mean fewer clicks
required by the user.
This commit is contained in:
David Lechner
2022-07-27 15:30:39 -05:00
parent 8bece7d957
commit 3953816350
5 changed files with 28 additions and 32 deletions
+5 -10
View File
@@ -3,21 +3,16 @@
import { Radio, RadioGroup } from '@blueprintjs/core';
import React from 'react';
import { useHubPickerSelectedHub } from './hooks';
import { Hub } from '.';
type HubPickerProps = {
hubType: Hub;
onChange: (hubType: Hub) => void;
};
export const HubPicker: React.VoidFunctionComponent = () => {
const [selectedHub, setSelectedHub] = useHubPickerSelectedHub();
export const HubPicker: React.VoidFunctionComponent<HubPickerProps> = ({
hubType,
onChange,
}) => {
return (
<RadioGroup
selectedValue={hubType}
onChange={(e) => onChange(e.currentTarget.value as Hub)}
selectedValue={selectedHub}
onChange={(e) => setSelectedHub(e.currentTarget.value as Hub)}
>
<Radio value={Hub.Move}>BOOST Move Hub</Radio>
<Radio value={Hub.City}>City Hub</Radio>
+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { useLocalStorage } from 'usehooks-ts';
import { Hub } from '.';
/**
* Hook for {@link HubPicker} state backed by local storage.
* @returns Tuple of the current state and setter (like useState()).
*/
export function useHubPickerSelectedHub() {
return useLocalStorage('hubPicker.selectedHub', Hub.Move);
}
@@ -27,7 +27,7 @@ describe('accept', () => {
await user.click(button);
expect(dispatch).toHaveBeenCalledWith(
newFileWizardDidAccept('test', '.py', Hub.Technic),
newFileWizardDidAccept('test', '.py', Hub.Move),
);
});
@@ -42,7 +42,7 @@ describe('accept', () => {
);
expect(dispatch).toHaveBeenCalledWith(
newFileWizardDidAccept('test', '.py', Hub.Technic),
newFileWizardDidAccept('test', '.py', Hub.Move),
);
});
});
+3 -6
View File
@@ -5,8 +5,8 @@ import { Button, Classes, Dialog, FormGroup } from '@blueprintjs/core';
import React, { useCallback, useRef, useState } from 'react';
import { useId } from 'react-aria';
import { useDispatch } from 'react-redux';
import { Hub } from '../../components/hubPicker';
import { HubPicker } from '../../components/hubPicker/HubPicker';
import { useHubPickerSelectedHub } from '../../components/hubPicker/hooks';
import { useFileStorageMetadata } from '../../fileStorage/hooks';
import {
FileNameValidationResult,
@@ -18,9 +18,6 @@ import FileNameFormGroup from '../fileNameFormGroup/FileNameFormGroup';
import { newFileWizardDidAccept, newFileWizardDidCancel } from './actions';
import { useI18n } from './i18n';
// This should be set to the most commonly used hub.
const defaultHub = Hub.Technic;
const NewFileWizard: React.VoidFunctionComponent = () => {
const i18n = useI18n();
const dispatch = useDispatch();
@@ -33,8 +30,8 @@ const NewFileWizard: React.VoidFunctionComponent = () => {
pythonFileExtension,
files.map((f) => f.path),
);
const [hubType, setHubType] = useState(defaultHub);
const [hubType] = useHubPickerSelectedHub();
const fileNameInputRef = useRef<HTMLInputElement>(null);
const handleSubmit = useCallback<React.FormEventHandler>(
@@ -70,7 +67,7 @@ const NewFileWizard: React.VoidFunctionComponent = () => {
onChange={setFileName}
/>
<FormGroup label={i18n.translate('smartHub.label')}>
<HubPicker hubType={hubType} onChange={setHubType} />
<HubPicker />
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
@@ -39,6 +39,7 @@ import {
hubHasUSB,
} from '../../components/hubPicker';
import { HubPicker } from '../../components/hubPicker/HubPicker';
import { useHubPickerSelectedHub } from '../../components/hubPicker/hooks';
import { FileMetadata } from '../../fileStorage';
import { useFileStorageMetadata } from '../../fileStorage/hooks';
import { useSelector } from '../../reducers';
@@ -57,21 +58,13 @@ const dialogBody = classNames(
'pb-firmware-installPybricksDialog-body',
);
type SelectHubPanelProps = {
hubType: Hub;
onChange: (hubType: Hub) => void;
};
const SelectHubPanel: React.VoidFunctionComponent<SelectHubPanelProps> = ({
hubType,
onChange,
}) => {
const SelectHubPanel: React.VoidFunctionComponent = () => {
const i18n = useI18n();
return (
<div className={dialogBody}>
<p>{i18n.translate('selectHubPanel.message')}</p>
<HubPicker hubType={hubType} onChange={onChange} />
<HubPicker />
<Popover2
popoverClassName={Classes2.POPOVER2_CONTENT_SIZING}
placement="right-end"
@@ -436,16 +429,14 @@ const BootloaderModePanel: React.VoidFunctionComponent<BootloaderModePanelProps>
);
};
const defaultHubType = Hub.Technic;
export const InstallPybricksDialog: React.VoidFunctionComponent = () => {
const { isOpen } = useSelector((s) => s.firmware.installPybricksDialog);
const dispatch = useDispatch();
const [hubType, setHubType] = useState(defaultHubType);
const [hubName, setHubName] = useState('');
const [includeProgram, setIncludeProgram] = useState(false);
const [selectedIncludeFile, setSelectedIncludeFile] = useState<FileMetadata>();
const [licenseAccepted, setLicenseAccepted] = useState(false);
const [hubType] = useHubPickerSelectedHub();
const { data } = useFirmware(hubType);
const i18n = useI18n();
@@ -470,7 +461,7 @@ export const InstallPybricksDialog: React.VoidFunctionComponent = () => {
<DialogStep
id="hub"
title={i18n.translate('selectHubPanel.title')}
panel={<SelectHubPanel hubType={hubType} onChange={setHubType} />}
panel={<SelectHubPanel />}
nextButtonProps={{ text: i18n.translate('nextButton.label') }}
/>
<DialogStep