toolbar/buttons: consolidate buttons

This commit is contained in:
David Lechner
2022-03-25 15:54:04 -05:00
parent 9d995ceeef
commit 5dd61fa4c3
47 changed files with 493 additions and 278 deletions
-43
View File
@@ -1,43 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import React from 'react';
import { useDispatch } from 'react-redux';
import { toggleBluetooth } from '../ble/actions';
import { BleConnectionState } from '../ble/reducers';
import { BootloaderConnectionState } from '../lwp3-bootloader/reducers';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { I18nId } from '../toolbar/i18n';
import btConnectedIcon from './bt-connected.svg';
import btDisconnectedIcon from './bt-disconnected.svg';
type BluetoothButtonProps = Pick<ActionButtonProps, 'label'>;
const BluetoothButton: React.VoidFunctionComponent<BluetoothButtonProps> = ({
label,
}) => {
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
const isDisconnected =
bootloaderConnection === BootloaderConnectionState.Disconnected &&
bleConnection === BleConnectionState.Disconnected;
const dispatch = useDispatch();
return (
<ActionButton
label={label}
tooltip={
isDisconnected ? I18nId.BluetoothConnect : I18nId.BluetoothDisconnect
}
icon={isDisconnected ? btDisconnectedIcon : btConnectedIcon}
enabled={isDisconnected || bleConnection === BleConnectionState.Connected}
showProgress={bleConnection === BleConnectionState.Connecting}
onAction={() => dispatch(toggleBluetooth())}
/>
);
};
export default BluetoothButton;
-35
View File
@@ -1,35 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { I18nId } from '../toolbar/i18n';
import { repl } from './actions';
import { HubRuntimeState } from './reducers';
import replIcon from './repl.svg';
type ReplButtonProps = Pick<ActionButtonProps, 'label' | 'keyboardShortcut'>;
const ReplButton: React.VoidFunctionComponent<ReplButtonProps> = ({
label,
keyboardShortcut,
}) => {
const enabled = useSelector((s) => s.hub.runtime === HubRuntimeState.Idle);
const dispatch = useDispatch();
const action = useCallback(() => dispatch(repl()), [dispatch]);
return (
<ActionButton
label={label}
keyboardShortcut={keyboardShortcut}
tooltip={I18nId.Repl}
icon={replIcon}
enabled={enabled}
onAction={action}
/>
);
};
export default ReplButton;
-40
View File
@@ -1,40 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import React from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { I18nId } from '../toolbar/i18n';
import { downloadAndRun } from './actions';
import { HubRuntimeState } from './reducers';
import runIcon from './run.svg';
type RunButtonProps = Pick<ActionButtonProps, 'label' | 'keyboardShortcut'>;
const RunButton: React.VoidFunctionComponent<RunButtonProps> = ({
label,
keyboardShortcut,
}) => {
const downloadProgress = useSelector((s) => s.hub.downloadProgress);
const runtime = useSelector((s) => s.hub.runtime);
const hasEditor = useSelector((s) => s.app.hasEditor);
const dispatch = useDispatch();
return (
<ActionButton
label={label}
keyboardShortcut={keyboardShortcut}
tooltip={I18nId.Run}
progressTooltip={I18nId.RunProgress}
icon={runIcon}
enabled={hasEditor && runtime === HubRuntimeState.Idle}
showProgress={runtime === HubRuntimeState.Loading}
progress={downloadProgress === null ? undefined : downloadProgress}
onAction={() => dispatch(downloadAndRun())}
/>
);
};
export default RunButton;
-35
View File
@@ -1,35 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import React from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { I18nId } from '../toolbar/i18n';
import { stop } from './actions';
import { HubRuntimeState } from './reducers';
import stopIcon from './stop.svg';
type StopButtonProps = Pick<ActionButtonProps, 'label' | 'keyboardShortcut'>;
const StopButton: React.VoidFunctionComponent<StopButtonProps> = ({
label,
keyboardShortcut,
}) => {
const runtime = useSelector((s) => s.hub.runtime);
const dispatch = useDispatch();
return (
<ActionButton
label={label}
keyboardShortcut={keyboardShortcut}
tooltip={I18nId.Stop}
icon={stopIcon}
enabled={runtime === HubRuntimeState.Running}
onAction={() => dispatch(stop())}
/>
);
};
export default StopButton;
-25
View File
@@ -1,25 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2022 The Pybricks Authors
import React from 'react';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { I18nId } from '../toolbar/i18n';
import settingsIcon from './settings.svg';
type SettingsButtonProps = Pick<ActionButtonProps, 'label' | 'onAction'>;
const SettingsButton: React.VoidFunctionComponent<SettingsButtonProps> = ({
label,
onAction,
}) => {
return (
<ActionButton
label={label}
tooltip={I18nId.Settings}
icon={settingsIcon}
onAction={onAction}
/>
);
};
export default SettingsButton;
+4 -20
View File
@@ -10,11 +10,9 @@ import {
useHotkeys,
} from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React, { useEffect, useMemo, useState } from 'react';
import { tooltipDelay } from '../app/constants';
import { pointerEventsNone } from '../utils/react';
import { I18nId } from './i18n';
const smallScreenThreshold = 700;
@@ -24,9 +22,7 @@ export interface ActionButtonProps {
/** Keyboard shortcut. */
readonly keyboardShortcut?: string;
/** Tooltip text that appears when hovering over the button. */
readonly tooltip: I18nId;
/** Tooltip text that appears when hovering over the button and @showProgress is true. */
readonly progressTooltip?: I18nId;
readonly tooltip: string;
/** Icon shown on the button. */
readonly icon: string;
/** When true or undefined, the button is enabled. */
@@ -43,15 +39,12 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
label,
keyboardShortcut,
tooltip,
progressTooltip,
icon,
enabled,
showProgress,
progress,
onAction,
}) => {
const [i18n] = useI18n();
const [isSmallScreen, setIsSmallScreen] = useState(
window.innerWidth <= smallScreenThreshold,
);
@@ -66,15 +59,6 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
const buttonSize = isSmallScreen ? SpinnerSize.SMALL : SpinnerSize.STANDARD;
const tooltipText =
showProgress && progressTooltip
? i18n.translate(progressTooltip, {
percent:
progress === undefined ? '' : i18n.formatPercentage(progress),
})
: i18n.translate(tooltip) +
(keyboardShortcut ? ` (${keyboardShortcut})` : '');
const hotkeys = useMemo(() => {
if (!keyboardShortcut) {
return [];
@@ -86,7 +70,7 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
allowInInput: true,
preventDefault: true,
combo: keyboardShortcut.replaceAll('-', '+'),
label: i18n.translate(tooltip),
label,
onKeyDown: () => {
if (enabled) {
onAction();
@@ -94,13 +78,13 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
},
},
];
}, [keyboardShortcut, tooltip, enabled, onAction, i18n]);
}, [keyboardShortcut, tooltip, enabled, label, onAction]);
useHotkeys(hotkeys);
return (
<Tooltip2
content={tooltipText}
content={tooltip}
placement="bottom"
hoverOpenDelay={tooltipDelay}
renderTarget={({
+2 -16
View File
@@ -3,12 +3,10 @@
import { Button, IRef, Intent, Spinner, SpinnerSize } from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React, { useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { tooltipDelay } from '../app/constants';
import { pointerEventsNone } from '../utils/react';
import { I18nId } from './i18n';
const smallScreenThreshold = 700;
export interface OpenFileButtonProps {
@@ -17,7 +15,7 @@ export interface OpenFileButtonProps {
/** The accepted file extension */
readonly fileExtension: string;
/** Tooltip text that appears when hovering over the button. */
readonly tooltip: I18nId;
readonly tooltip: string;
/** Icon shown on the button. */
readonly icon: string;
/** When true or undefined, the button is enabled. */
@@ -49,8 +47,6 @@ const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
onReject,
onClick,
}) => {
const [i18n] = useI18n();
const [isSmallScreen, setIsSmallScreen] = useState(
window.innerWidth <= smallScreenThreshold,
);
@@ -101,17 +97,7 @@ const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
return (
<Tooltip2
content={i18n.translate(
tooltip,
tooltip === I18nId.FlashProgress
? {
percent:
progress === undefined
? ''
: i18n.formatPercentage(progress),
}
: undefined,
)}
content={tooltip}
placement="bottom"
hoverOpenDelay={tooltipDelay}
renderTarget={({
+12 -15
View File
@@ -3,14 +3,14 @@
import { ButtonGroup } from '@blueprintjs/core';
import React, { useState } from 'react';
import FlashButton from '../firmware/FlashButton';
import BluetoothButton from '../hub/BluetoothButton';
import ReplButton from '../hub/ReplButton';
import RunButton from '../hub/RunButton';
import StopButton from '../hub/StopButton';
import SettingsButton from '../settings/SettingsButton';
import SettingsDrawer from '../settings/SettingsDrawer';
import { preventBrowserNativeContextMenu } from '../utils/react';
import BluetoothButton from './buttons/bluetooth/BluetoothButton';
import FlashButton from './buttons/flash/FlashButton';
import ReplButton from './buttons/repl/ReplButton';
import RunButton from './buttons/run/RunButton';
import SettingsButton from './buttons/settings/SettingsButton';
import StopButton from './buttons/stop/StopButton';
import './toolbar.scss';
@@ -24,19 +24,16 @@ const Toolbar: React.VFC = (_props) => {
className="pb-toolbar"
>
<ButtonGroup className="pb-toolbar-group pb-align-left">
<FlashButton label="Flash" />
<BluetoothButton label="Bluetooth" />
<FlashButton />
<BluetoothButton />
</ButtonGroup>
<ButtonGroup className="pb-toolbar-group pb-align-left">
<RunButton label="Run" keyboardShortcut="F5" />
<StopButton label="Stop" keyboardShortcut="F6" />
<ReplButton label="REPL" />
<RunButton />
<StopButton />
<ReplButton />
</ButtonGroup>
<ButtonGroup className="pb-toolbar-group pb-align-right">
<SettingsButton
label="Settings"
onAction={() => setIsSettingsDrawerOpen(true)}
/>
<SettingsButton onAction={() => setIsSettingsDrawerOpen(true)} />
<SettingsDrawer
isOpen={isSettingsDrawerOpen}
onClose={() => setIsSettingsDrawerOpen(false)}
@@ -0,0 +1,20 @@
// 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 { toggleBluetooth } from '../../../ble/actions';
import BluetoothButton from './BluetoothButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<BluetoothButton />);
button.getByRole('button', { name: 'Bluetooth' }).click();
expect(dispatch).toHaveBeenCalledWith(toggleBluetooth());
});
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useDispatch } from 'react-redux';
import { toggleBluetooth } from '../../../ble/actions';
import { BleConnectionState } from '../../../ble/reducers';
import { BootloaderConnectionState } from '../../../lwp3-bootloader/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import connectedIcon from './connected.svg';
import disconnectedIcon from './disconnected.svg';
import { I18nId } from './i18n';
const BluetoothButton: React.VFC = () => {
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
const isDisconnected =
bootloaderConnection === BootloaderConnectionState.Disconnected &&
bleConnection === BleConnectionState.Disconnected;
const dispatch = useDispatch();
const [i18n] = useI18n();
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
tooltip={i18n.translate(
isDisconnected ? I18nId.TooltipConnect : I18nId.TooltipDisconnect,
)}
icon={isDisconnected ? disconnectedIcon : connectedIcon}
enabled={isDisconnected || bleConnection === BleConnectionState.Connected}
showProgress={bleConnection === BleConnectionState.Connecting}
onAction={() => dispatch(toggleBluetooth())}
/>
);
};
export default BluetoothButton;

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../test';
import { lookup } from '../../../../test';
import { I18nId } from './i18n';
import en from './translations/en.json';
+8
View File
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
TooltipConnect = 'tooltip.connect',
TooltipDisconnect = 'tooltip.disconnect',
}
@@ -0,0 +1,7 @@
{
"label": "Bluetooth",
"tooltip": {
"connect": "Connect using Bluetooth",
"disconnect": "Disconnect Bluetooth"
}
}
@@ -0,0 +1,20 @@
// 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 { flashFirmware } from '../../../firmware/actions';
import FlashButton from './FlashButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<FlashButton />);
button.getByRole('button', { name: 'Flash' }).click();
expect(dispatch).toHaveBeenCalledWith(flashFirmware(null, false, ''));
});
@@ -1,21 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useDispatch } from 'react-redux';
import { BleConnectionState } from '../ble/reducers';
import { BootloaderConnectionState } from '../lwp3-bootloader/reducers';
import * as notificationActions from '../notifications/actions';
import { useSelector } from '../reducers';
import { useSettingFlashCurrentProgram, useSettingHubName } from '../settings/hooks';
import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton';
import { I18nId } from '../toolbar/i18n';
import { flashFirmware } from './actions';
import firmwareIcon from './firmware.svg';
import { BleConnectionState } from '../../../ble/reducers';
import { flashFirmware } from '../../../firmware/actions';
import { BootloaderConnectionState } from '../../../lwp3-bootloader/reducers';
import * as notificationActions from '../../../notifications/actions';
import { useSelector } from '../../../reducers';
import {
useSettingFlashCurrentProgram,
useSettingHubName,
} from '../../../settings/hooks';
import OpenFileButton from '../../../toolbar/OpenFileButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
type FlashButtonProps = Pick<OpenFileButtonProps, 'label'>;
const FlashButton: React.VoidFunctionComponent<FlashButtonProps> = ({ label }) => {
const FlashButton: React.VFC = () => {
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
const flashing = useSelector((s) => s.firmware.flashing);
@@ -24,13 +26,20 @@ const FlashButton: React.VoidFunctionComponent<FlashButtonProps> = ({ label }) =
const { hubName } = useSettingHubName();
const dispatch = useDispatch();
const [i18n] = useI18n();
return (
<OpenFileButton
label={label}
label={i18n.translate(I18nId.Label)}
fileExtension=".zip"
icon={firmwareIcon}
tooltip={flashing ? I18nId.FlashProgress : I18nId.Flash}
icon={icon}
tooltip={
progress
? i18n.translate(I18nId.TooltipProgress, {
percent: i18n.formatPercentage(progress),
})
: i18n.translate(I18nId.TooltipAction)
}
enabled={
bootloaderConnection === BootloaderConnectionState.Disconnected &&
bleConnection === BleConnectionState.Disconnected
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../../../test';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+8
View File
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
TooltipAction = 'tooltip.action',
TooltipProgress = 'tooltip.progress',
}

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

@@ -0,0 +1,7 @@
{
"label": "Flash",
"tooltip": {
"action": "Install Pybricks firmware",
"progress": "Flashing… {percent}"
}
}
@@ -0,0 +1,23 @@
// 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 { repl } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import ReplButton from './ReplButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<ReplButton />, {
hub: { runtime: HubRuntimeState.Idle },
});
button.getByRole('button', { name: 'REPL' }).click();
expect(dispatch).toHaveBeenCalledWith(repl());
});
+31
View File
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { repl } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const ReplButton: React.VFC = () => {
const enabled = useSelector((s) => s.hub.runtime === HubRuntimeState.Idle);
const [i18n] = useI18n();
const dispatch = useDispatch();
const action = useCallback(() => dispatch(repl()), [dispatch]);
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
tooltip={i18n.translate(I18nId.Tooltip)}
icon={icon}
enabled={enabled}
onAction={action}
/>
);
};
export default ReplButton;
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../../../test';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+7
View File
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
Tooltip = 'tooltip',
}

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,4 @@
{
"label": "REPL",
"tooltip": "Start REPL in terminal"
}
@@ -0,0 +1,24 @@
// 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 { downloadAndRun } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import RunButton from './RunButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<RunButton />, {
app: { hasEditor: true },
hub: { runtime: HubRuntimeState.Idle },
});
button.getByRole('button', { name: 'Run' }).click();
expect(dispatch).toHaveBeenCalledWith(downloadAndRun());
});
+46
View File
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useDispatch } from 'react-redux';
import { downloadAndRun } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const RunButton: React.VFC = () => {
const downloadProgress = useSelector((s) => s.hub.downloadProgress);
const runtime = useSelector((s) => s.hub.runtime);
const hasEditor = useSelector((s) => s.app.hasEditor);
const keyboardShortcut = 'F5';
const dispatch = useDispatch();
const [i18n] = useI18n();
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
keyboardShortcut={keyboardShortcut}
tooltip={
downloadProgress
? i18n.translate(I18nId.TooltipProgress, {
percent: i18n.formatPercentage(downloadProgress),
})
: i18n.translate(I18nId.TooltipAction, { key: keyboardShortcut })
}
icon={icon}
enabled={hasEditor && runtime === HubRuntimeState.Idle}
showProgress={runtime === HubRuntimeState.Loading}
progress={downloadProgress === null ? undefined : downloadProgress}
onAction={() => dispatch(downloadAndRun())}
/>
);
};
export default RunButton;
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../../../test';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+8
View File
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
TooltipAction = 'tooltip.action',
TooltipProgress = 'tooltip.progress',
}

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,7 @@
{
"label": "Run",
"tooltip": {
"action": "Download and run this program ({key})",
"progress": "Downloading… {percent}"
}
}
@@ -0,0 +1,21 @@
// 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 SettingsButton from './SettingsButton';
afterEach(() => {
cleanup();
});
it('should invoke callback when clicked', () => {
const handleAction = jest.fn();
const [button] = testRender(<SettingsButton onAction={handleAction} />);
button.getByRole('button', { name: 'Settings' }).click();
expect(handleAction).toHaveBeenCalled();
});
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import ActionButton, { ActionButtonProps } from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
type SettingsButtonProps = Pick<ActionButtonProps, 'onAction'>;
const SettingsButton: React.VoidFunctionComponent<SettingsButtonProps> = ({
onAction,
}) => {
const [i18n] = useI18n();
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
tooltip={i18n.translate(I18nId.Tooltip)}
icon={icon}
onAction={onAction}
/>
);
};
export default SettingsButton;
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../../../test';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+7
View File
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
Tooltip = 'tooltip',
}

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,4 @@
{
"label": "Settings",
"tooltip": "Open settings and help"
}
@@ -0,0 +1,23 @@
// 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 { stop } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import StopButton from './StopButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<StopButton />, {
hub: { runtime: HubRuntimeState.Running },
});
button.getByRole('button', { name: 'Stop' }).click();
expect(dispatch).toHaveBeenCalledWith(stop());
});
+33
View File
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useDispatch } from 'react-redux';
import { stop } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const StopButton: React.VFC = () => {
const runtime = useSelector((s) => s.hub.runtime);
const keyboardShortcut = 'F6';
const dispatch = useDispatch();
const [i18n] = useI18n();
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
keyboardShortcut={keyboardShortcut}
tooltip={i18n.translate(I18nId.Tooltip, { key: keyboardShortcut })}
icon={icon}
enabled={runtime === HubRuntimeState.Running}
onAction={() => dispatch(stop())}
/>
);
};
export default StopButton;
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../../../test';
import { I18nId } from './i18n';
import en from './translations/en.json';
describe('Ensure .json file has matches for I18nId', () => {
test.each(Object.values(I18nId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+7
View File
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
Tooltip = 'tooltip',
}

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,4 @@
{
"label": "Stop",
"tooltip": "Stop everything ({key})"
}
-16
View File
@@ -1,16 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
//
// Toolbar button translation keys.
export enum I18nId {
Run = 'run.action.tooltip',
RunProgress = 'run.progress.tooltip',
Stop = 'stop.tooltip',
Repl = 'repl.tooltip',
Flash = 'flash.action.tooltip',
FlashProgress = 'flash.progress.tooltip',
BluetoothConnect = 'bluetooth.connect.tooltip',
BluetoothDisconnect = 'bluetooth.disconnect.tooltip',
Settings = 'settings.tooltip',
}
-17
View File
@@ -1,17 +0,0 @@
{
"stop": { "tooltip": "Stop everything" },
"run": {
"action": { "tooltip": "Download and run this program" },
"progress": { "tooltip": "Downloading… {percent}" }
},
"repl": { "tooltip": "Start REPL in terminal" },
"bluetooth": {
"connect": { "tooltip": "Connect using Bluetooth" },
"disconnect": { "tooltip": "Disconnect Bluetooth" }
},
"flash": {
"action": { "tooltip": "Install Pybricks firmware" },
"progress": { "tooltip": "Flashing… {percent}" }
},
"settings": { "tooltip": "Settings" }
}