toolbar/buttons: consolidate buttons
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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={({
|
||||
|
||||
@@ -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={({
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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})"
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||