diff --git a/src/hub/BluetoothButton.tsx b/src/hub/BluetoothButton.tsx deleted file mode 100644 index 1bad7a4a..00000000 --- a/src/hub/BluetoothButton.tsx +++ /dev/null @@ -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; - -const BluetoothButton: React.VoidFunctionComponent = ({ - 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 ( - dispatch(toggleBluetooth())} - /> - ); -}; - -export default BluetoothButton; diff --git a/src/hub/ReplButton.tsx b/src/hub/ReplButton.tsx deleted file mode 100644 index 30e496be..00000000 --- a/src/hub/ReplButton.tsx +++ /dev/null @@ -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; - -const ReplButton: React.VoidFunctionComponent = ({ - label, - keyboardShortcut, -}) => { - const enabled = useSelector((s) => s.hub.runtime === HubRuntimeState.Idle); - const dispatch = useDispatch(); - const action = useCallback(() => dispatch(repl()), [dispatch]); - - return ( - - ); -}; - -export default ReplButton; diff --git a/src/hub/RunButton.tsx b/src/hub/RunButton.tsx deleted file mode 100644 index bf5982c5..00000000 --- a/src/hub/RunButton.tsx +++ /dev/null @@ -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; - -const RunButton: React.VoidFunctionComponent = ({ - 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 ( - dispatch(downloadAndRun())} - /> - ); -}; - -export default RunButton; diff --git a/src/hub/StopButton.tsx b/src/hub/StopButton.tsx deleted file mode 100644 index a54a1dc7..00000000 --- a/src/hub/StopButton.tsx +++ /dev/null @@ -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; - -const StopButton: React.VoidFunctionComponent = ({ - label, - keyboardShortcut, -}) => { - const runtime = useSelector((s) => s.hub.runtime); - - const dispatch = useDispatch(); - - return ( - dispatch(stop())} - /> - ); -}; - -export default StopButton; diff --git a/src/settings/SettingsButton.tsx b/src/settings/SettingsButton.tsx deleted file mode 100644 index 43692b3d..00000000 --- a/src/settings/SettingsButton.tsx +++ /dev/null @@ -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; - -const SettingsButton: React.VoidFunctionComponent = ({ - label, - onAction, -}) => { - return ( - - ); -}; - -export default SettingsButton; diff --git a/src/toolbar/ActionButton.tsx b/src/toolbar/ActionButton.tsx index 4c6d5c5c..a4bfadb2 100644 --- a/src/toolbar/ActionButton.tsx +++ b/src/toolbar/ActionButton.tsx @@ -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 = ({ 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 = ({ 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 = ({ allowInInput: true, preventDefault: true, combo: keyboardShortcut.replaceAll('-', '+'), - label: i18n.translate(tooltip), + label, onKeyDown: () => { if (enabled) { onAction(); @@ -94,13 +78,13 @@ const ActionButton: React.VoidFunctionComponent = ({ }, }, ]; - }, [keyboardShortcut, tooltip, enabled, onAction, i18n]); + }, [keyboardShortcut, tooltip, enabled, label, onAction]); useHotkeys(hotkeys); return ( = ({ onReject, onClick, }) => { - const [i18n] = useI18n(); - const [isSmallScreen, setIsSmallScreen] = useState( window.innerWidth <= smallScreenThreshold, ); @@ -101,17 +97,7 @@ const OpenFileButton: React.VoidFunctionComponent = ({ return ( { className="pb-toolbar" > - - + + - - - + + + - setIsSettingsDrawerOpen(true)} - /> + setIsSettingsDrawerOpen(true)} /> setIsSettingsDrawerOpen(false)} diff --git a/src/toolbar/buttons/bluetooth/BluetoothButton.test.tsx b/src/toolbar/buttons/bluetooth/BluetoothButton.test.tsx new file mode 100644 index 00000000..5c2b5dbd --- /dev/null +++ b/src/toolbar/buttons/bluetooth/BluetoothButton.test.tsx @@ -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(); + + button.getByRole('button', { name: 'Bluetooth' }).click(); + + expect(dispatch).toHaveBeenCalledWith(toggleBluetooth()); +}); diff --git a/src/toolbar/buttons/bluetooth/BluetoothButton.tsx b/src/toolbar/buttons/bluetooth/BluetoothButton.tsx new file mode 100644 index 00000000..d70733cb --- /dev/null +++ b/src/toolbar/buttons/bluetooth/BluetoothButton.tsx @@ -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 ( + dispatch(toggleBluetooth())} + /> + ); +}; + +export default BluetoothButton; diff --git a/src/hub/bt-connected.svg b/src/toolbar/buttons/bluetooth/connected.svg similarity index 100% rename from src/hub/bt-connected.svg rename to src/toolbar/buttons/bluetooth/connected.svg diff --git a/src/hub/bt-disconnected.svg b/src/toolbar/buttons/bluetooth/disconnected.svg similarity index 100% rename from src/hub/bt-disconnected.svg rename to src/toolbar/buttons/bluetooth/disconnected.svg diff --git a/src/toolbar/i18n.test.ts b/src/toolbar/buttons/bluetooth/i18n.test.ts similarity index 88% rename from src/toolbar/i18n.test.ts rename to src/toolbar/buttons/bluetooth/i18n.test.ts index d0ceda4a..b8f901e0 100644 --- a/src/toolbar/i18n.test.ts +++ b/src/toolbar/buttons/bluetooth/i18n.test.ts @@ -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'; diff --git a/src/toolbar/buttons/bluetooth/i18n.ts b/src/toolbar/buttons/bluetooth/i18n.ts new file mode 100644 index 00000000..4b021874 --- /dev/null +++ b/src/toolbar/buttons/bluetooth/i18n.ts @@ -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', +} diff --git a/src/toolbar/buttons/bluetooth/translations/en.json b/src/toolbar/buttons/bluetooth/translations/en.json new file mode 100644 index 00000000..e1508bed --- /dev/null +++ b/src/toolbar/buttons/bluetooth/translations/en.json @@ -0,0 +1,7 @@ +{ + "label": "Bluetooth", + "tooltip": { + "connect": "Connect using Bluetooth", + "disconnect": "Disconnect Bluetooth" + } +} diff --git a/src/toolbar/buttons/flash/FlashButton.test.tsx b/src/toolbar/buttons/flash/FlashButton.test.tsx new file mode 100644 index 00000000..e514f9f7 --- /dev/null +++ b/src/toolbar/buttons/flash/FlashButton.test.tsx @@ -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(); + + button.getByRole('button', { name: 'Flash' }).click(); + + expect(dispatch).toHaveBeenCalledWith(flashFirmware(null, false, '')); +}); diff --git a/src/firmware/FlashButton.tsx b/src/toolbar/buttons/flash/FlashButton.tsx similarity index 61% rename from src/firmware/FlashButton.tsx rename to src/toolbar/buttons/flash/FlashButton.tsx index 870bcb0a..d1bdaf92 100644 --- a/src/firmware/FlashButton.tsx +++ b/src/toolbar/buttons/flash/FlashButton.tsx @@ -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; - -const FlashButton: React.VoidFunctionComponent = ({ 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 = ({ label }) = const { hubName } = useSettingHubName(); const dispatch = useDispatch(); + const [i18n] = useI18n(); return ( { + test.each(Object.values(I18nId))('%s', (id) => { + expect(lookup(en, id)).toBeDefined(); + }); +}); diff --git a/src/toolbar/buttons/flash/i18n.ts b/src/toolbar/buttons/flash/i18n.ts new file mode 100644 index 00000000..4b6d51f7 --- /dev/null +++ b/src/toolbar/buttons/flash/i18n.ts @@ -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', +} diff --git a/src/firmware/firmware.svg b/src/toolbar/buttons/flash/icon.svg similarity index 100% rename from src/firmware/firmware.svg rename to src/toolbar/buttons/flash/icon.svg diff --git a/src/toolbar/buttons/flash/translations/en.json b/src/toolbar/buttons/flash/translations/en.json new file mode 100644 index 00000000..782c231a --- /dev/null +++ b/src/toolbar/buttons/flash/translations/en.json @@ -0,0 +1,7 @@ +{ + "label": "Flash", + "tooltip": { + "action": "Install Pybricks firmware", + "progress": "Flashing… {percent}" + } +} diff --git a/src/toolbar/buttons/repl/ReplButton.test.tsx b/src/toolbar/buttons/repl/ReplButton.test.tsx new file mode 100644 index 00000000..ffd9ae41 --- /dev/null +++ b/src/toolbar/buttons/repl/ReplButton.test.tsx @@ -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(, { + hub: { runtime: HubRuntimeState.Idle }, + }); + + button.getByRole('button', { name: 'REPL' }).click(); + + expect(dispatch).toHaveBeenCalledWith(repl()); +}); diff --git a/src/toolbar/buttons/repl/ReplButton.tsx b/src/toolbar/buttons/repl/ReplButton.tsx new file mode 100644 index 00000000..045ecb57 --- /dev/null +++ b/src/toolbar/buttons/repl/ReplButton.tsx @@ -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 ( + + ); +}; + +export default ReplButton; diff --git a/src/toolbar/buttons/repl/i18n.test.ts b/src/toolbar/buttons/repl/i18n.test.ts new file mode 100644 index 00000000..cb4367f6 --- /dev/null +++ b/src/toolbar/buttons/repl/i18n.test.ts @@ -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(); + }); +}); diff --git a/src/toolbar/buttons/repl/i18n.ts b/src/toolbar/buttons/repl/i18n.ts new file mode 100644 index 00000000..8a962ff9 --- /dev/null +++ b/src/toolbar/buttons/repl/i18n.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020-2022 The Pybricks Authors + +export enum I18nId { + Label = 'label', + Tooltip = 'tooltip', +} diff --git a/src/hub/repl.svg b/src/toolbar/buttons/repl/icon.svg similarity index 100% rename from src/hub/repl.svg rename to src/toolbar/buttons/repl/icon.svg diff --git a/src/toolbar/buttons/repl/translations/en.json b/src/toolbar/buttons/repl/translations/en.json new file mode 100644 index 00000000..442bb175 --- /dev/null +++ b/src/toolbar/buttons/repl/translations/en.json @@ -0,0 +1,4 @@ +{ + "label": "REPL", + "tooltip": "Start REPL in terminal" +} diff --git a/src/toolbar/buttons/run/RunButton.test.tsx b/src/toolbar/buttons/run/RunButton.test.tsx new file mode 100644 index 00000000..3b6b8ff6 --- /dev/null +++ b/src/toolbar/buttons/run/RunButton.test.tsx @@ -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(, { + app: { hasEditor: true }, + hub: { runtime: HubRuntimeState.Idle }, + }); + + button.getByRole('button', { name: 'Run' }).click(); + + expect(dispatch).toHaveBeenCalledWith(downloadAndRun()); +}); diff --git a/src/toolbar/buttons/run/RunButton.tsx b/src/toolbar/buttons/run/RunButton.tsx new file mode 100644 index 00000000..44a5661e --- /dev/null +++ b/src/toolbar/buttons/run/RunButton.tsx @@ -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 ( + dispatch(downloadAndRun())} + /> + ); +}; + +export default RunButton; diff --git a/src/toolbar/buttons/run/i18n.test.ts b/src/toolbar/buttons/run/i18n.test.ts new file mode 100644 index 00000000..b8f901e0 --- /dev/null +++ b/src/toolbar/buttons/run/i18n.test.ts @@ -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(); + }); +}); diff --git a/src/toolbar/buttons/run/i18n.ts b/src/toolbar/buttons/run/i18n.ts new file mode 100644 index 00000000..4b6d51f7 --- /dev/null +++ b/src/toolbar/buttons/run/i18n.ts @@ -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', +} diff --git a/src/hub/run.svg b/src/toolbar/buttons/run/icon.svg similarity index 100% rename from src/hub/run.svg rename to src/toolbar/buttons/run/icon.svg diff --git a/src/toolbar/buttons/run/translations/en.json b/src/toolbar/buttons/run/translations/en.json new file mode 100644 index 00000000..baf7aa07 --- /dev/null +++ b/src/toolbar/buttons/run/translations/en.json @@ -0,0 +1,7 @@ +{ + "label": "Run", + "tooltip": { + "action": "Download and run this program ({key})", + "progress": "Downloading… {percent}" + } +} diff --git a/src/toolbar/buttons/settings/SettingsButton.test.tsx b/src/toolbar/buttons/settings/SettingsButton.test.tsx new file mode 100644 index 00000000..f15eebe9 --- /dev/null +++ b/src/toolbar/buttons/settings/SettingsButton.test.tsx @@ -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(); + + button.getByRole('button', { name: 'Settings' }).click(); + + expect(handleAction).toHaveBeenCalled(); +}); diff --git a/src/toolbar/buttons/settings/SettingsButton.tsx b/src/toolbar/buttons/settings/SettingsButton.tsx new file mode 100644 index 00000000..1796f7c1 --- /dev/null +++ b/src/toolbar/buttons/settings/SettingsButton.tsx @@ -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; + +const SettingsButton: React.VoidFunctionComponent = ({ + onAction, +}) => { + const [i18n] = useI18n(); + + return ( + + ); +}; + +export default SettingsButton; diff --git a/src/toolbar/buttons/settings/i18n.test.ts b/src/toolbar/buttons/settings/i18n.test.ts new file mode 100644 index 00000000..b8f901e0 --- /dev/null +++ b/src/toolbar/buttons/settings/i18n.test.ts @@ -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(); + }); +}); diff --git a/src/toolbar/buttons/settings/i18n.ts b/src/toolbar/buttons/settings/i18n.ts new file mode 100644 index 00000000..8a962ff9 --- /dev/null +++ b/src/toolbar/buttons/settings/i18n.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020-2022 The Pybricks Authors + +export enum I18nId { + Label = 'label', + Tooltip = 'tooltip', +} diff --git a/src/settings/settings.svg b/src/toolbar/buttons/settings/icon.svg similarity index 100% rename from src/settings/settings.svg rename to src/toolbar/buttons/settings/icon.svg diff --git a/src/toolbar/buttons/settings/translations/en.json b/src/toolbar/buttons/settings/translations/en.json new file mode 100644 index 00000000..053a328a --- /dev/null +++ b/src/toolbar/buttons/settings/translations/en.json @@ -0,0 +1,4 @@ +{ + "label": "Settings", + "tooltip": "Open settings and help" +} diff --git a/src/toolbar/buttons/stop/StopButton.test.tsx b/src/toolbar/buttons/stop/StopButton.test.tsx new file mode 100644 index 00000000..fd8af252 --- /dev/null +++ b/src/toolbar/buttons/stop/StopButton.test.tsx @@ -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(, { + hub: { runtime: HubRuntimeState.Running }, + }); + + button.getByRole('button', { name: 'Stop' }).click(); + + expect(dispatch).toHaveBeenCalledWith(stop()); +}); diff --git a/src/toolbar/buttons/stop/StopButton.tsx b/src/toolbar/buttons/stop/StopButton.tsx new file mode 100644 index 00000000..04eb3af0 --- /dev/null +++ b/src/toolbar/buttons/stop/StopButton.tsx @@ -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 ( + dispatch(stop())} + /> + ); +}; + +export default StopButton; diff --git a/src/toolbar/buttons/stop/i18n.test.ts b/src/toolbar/buttons/stop/i18n.test.ts new file mode 100644 index 00000000..b8f901e0 --- /dev/null +++ b/src/toolbar/buttons/stop/i18n.test.ts @@ -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(); + }); +}); diff --git a/src/toolbar/buttons/stop/i18n.ts b/src/toolbar/buttons/stop/i18n.ts new file mode 100644 index 00000000..8a962ff9 --- /dev/null +++ b/src/toolbar/buttons/stop/i18n.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020-2022 The Pybricks Authors + +export enum I18nId { + Label = 'label', + Tooltip = 'tooltip', +} diff --git a/src/hub/stop.svg b/src/toolbar/buttons/stop/icon.svg similarity index 100% rename from src/hub/stop.svg rename to src/toolbar/buttons/stop/icon.svg diff --git a/src/toolbar/buttons/stop/translations/en.json b/src/toolbar/buttons/stop/translations/en.json new file mode 100644 index 00000000..f6b8e82c --- /dev/null +++ b/src/toolbar/buttons/stop/translations/en.json @@ -0,0 +1,4 @@ +{ + "label": "Stop", + "tooltip": "Stop everything ({key})" +} diff --git a/src/toolbar/i18n.ts b/src/toolbar/i18n.ts deleted file mode 100644 index 557b0e31..00000000 --- a/src/toolbar/i18n.ts +++ /dev/null @@ -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', -} diff --git a/src/toolbar/translations/en.json b/src/toolbar/translations/en.json deleted file mode 100644 index 616c7878..00000000 --- a/src/toolbar/translations/en.json +++ /dev/null @@ -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" } -}