mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
src: use prop unpacking pattern
This makes the code a bit shorter by not having to use `props.` all of the time. Also make everything VoidFunctionComponent while we are touching this.
This commit is contained in:
@@ -24,7 +24,10 @@ import './about.scss';
|
||||
|
||||
type AboutDialogProps = { isOpen: boolean; onClose: () => void };
|
||||
|
||||
const AboutDialog: React.FunctionComponent<AboutDialogProps> = (props) => {
|
||||
const AboutDialog: React.VoidFunctionComponent<AboutDialogProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const [isLicenseDialogOpen, setIsLicenseDialogOpen] = useState(false);
|
||||
|
||||
const [i18n] = useI18n({ id: 'about', translations: { en }, fallback: en });
|
||||
@@ -32,8 +35,8 @@ const AboutDialog: React.FunctionComponent<AboutDialogProps> = (props) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={`Pybricks v${firmwareVersion} (${appName} v${appVersion})`}
|
||||
isOpen={props.isOpen}
|
||||
onClose={props.onClose}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<div className="pb-about-icon">
|
||||
|
||||
+3
-3
@@ -127,7 +127,7 @@ type AppProps = {
|
||||
onEditorChanged?: (editor: EditorType) => void;
|
||||
};
|
||||
|
||||
const App: React.VoidFunctionComponent<AppProps> = (props) => {
|
||||
const App: React.VoidFunctionComponent<AppProps> = ({ onEditorChanged }) => {
|
||||
const darkMode = useSelector((s): boolean => s.settings.darkMode);
|
||||
const showDocs = useSelector((s): boolean => s.settings.showDocs);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
@@ -139,8 +139,8 @@ const App: React.VoidFunctionComponent<AppProps> = (props) => {
|
||||
setEditor: (editor) => {
|
||||
setEditor(editor);
|
||||
|
||||
if (props.onEditorChanged) {
|
||||
props.onEditorChanged(editor);
|
||||
if (onEditorChanged) {
|
||||
onEditorChanged(editor);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
+20
-14
@@ -46,16 +46,22 @@ type ActionButtonProps = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = (props) => {
|
||||
const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
|
||||
icon,
|
||||
toolTipId,
|
||||
toolTipReplacements,
|
||||
disabled,
|
||||
onClick,
|
||||
}) => {
|
||||
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={props.icon}
|
||||
title={i18n.translate(props.toolTipId, props.toolTipReplacements)}
|
||||
disabled={props.disabled}
|
||||
icon={icon}
|
||||
title={i18n.translate(toolTipId, toolTipReplacements)}
|
||||
disabled={disabled}
|
||||
onMouseDown={preventFocusOnClick}
|
||||
onClick={props.onClick}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -73,7 +79,7 @@ type ActionButtonGroupProps = {
|
||||
const FileActionButtonGroup = forwardRef<
|
||||
FileActionButtonGroupRef,
|
||||
ActionButtonGroupProps
|
||||
>((props, ref) => {
|
||||
>(({ fileName }, ref) => {
|
||||
const dispatch = useDispatch();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||
@@ -85,10 +91,10 @@ const FileActionButtonGroup = forwardRef<
|
||||
// received.
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
useEffect(() => {
|
||||
if (!fileNames.includes(props.fileName)) {
|
||||
if (!fileNames.includes(fileName)) {
|
||||
setVisible(false);
|
||||
}
|
||||
}, [fileNames, props.fileName, setVisible]);
|
||||
}, [fileNames, fileName, setVisible]);
|
||||
|
||||
useImperativeHandle(ref, () => ({ setVisible }), [setVisible]);
|
||||
|
||||
@@ -97,11 +103,11 @@ const FileActionButtonGroup = forwardRef<
|
||||
<ActionButton
|
||||
icon="edit"
|
||||
toolTipId={ExplorerStringId.TreeItemRenameTooltip}
|
||||
toolTipReplacements={{ fileName: props.fileName }}
|
||||
toolTipReplacements={{ fileName: fileName }}
|
||||
onClick={() => setIsRenameDialogOpen(true)}
|
||||
/>
|
||||
<RenameFileDialog
|
||||
oldName={props.fileName}
|
||||
oldName={fileName}
|
||||
isOpen={isRenameDialogOpen}
|
||||
onClose={() => setIsRenameDialogOpen(false)}
|
||||
/>
|
||||
@@ -113,14 +119,14 @@ const FileActionButtonGroup = forwardRef<
|
||||
// download operation
|
||||
icon="import"
|
||||
toolTipId={ExplorerStringId.TreeItemExportTooltip}
|
||||
toolTipReplacements={{ fileName: props.fileName }}
|
||||
onClick={() => dispatch(fileStorageExportFile(props.fileName))}
|
||||
toolTipReplacements={{ fileName: fileName }}
|
||||
onClick={() => dispatch(fileStorageExportFile(fileName))}
|
||||
/>
|
||||
<ActionButton
|
||||
icon="trash"
|
||||
toolTipId={ExplorerStringId.TreeItemDeleteTooltip}
|
||||
toolTipReplacements={{ fileName: props.fileName }}
|
||||
onClick={() => dispatch(explorerDeleteFile(props.fileName))}
|
||||
toolTipReplacements={{ fileName: fileName }}
|
||||
onClick={() => dispatch(explorerDeleteFile(fileName))}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
|
||||
@@ -17,12 +17,12 @@ type FileNameHelpTextProps = {
|
||||
/**
|
||||
* Component that maps FileNameValidationResult to help message to display to user.
|
||||
*/
|
||||
const FileNameHelpText: React.VoidFunctionComponent<FileNameHelpTextProps> = (
|
||||
props,
|
||||
) => {
|
||||
const FileNameHelpText: React.VoidFunctionComponent<FileNameHelpTextProps> = ({
|
||||
validation,
|
||||
}) => {
|
||||
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
|
||||
|
||||
switch (props.validation) {
|
||||
switch (validation) {
|
||||
case FileNameValidationResult.IsOk:
|
||||
return <>{i18n.translate(NewFileWizardStringId.FileNameHelpTextIsOk)}</>;
|
||||
case FileNameValidationResult.IsEmpty:
|
||||
@@ -92,23 +92,27 @@ type FileNameFormGroupProps = {
|
||||
/**
|
||||
* Component used to get a valid new file name.
|
||||
*/
|
||||
const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = (
|
||||
props,
|
||||
) => {
|
||||
const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = ({
|
||||
fileName,
|
||||
fileExtension,
|
||||
inputRef,
|
||||
onChange,
|
||||
onValidation,
|
||||
}) => {
|
||||
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
|
||||
const fileNames = useSelector((s) => s.fileStorage.fileNames);
|
||||
|
||||
const [fileNameValidation, fileNameIntent] = useMemo(() => {
|
||||
const result = validateFileName(props.fileName, props.fileExtension, fileNames);
|
||||
const result = validateFileName(fileName, fileExtension, fileNames);
|
||||
|
||||
// can't call callback now because it would break react, so defer it
|
||||
setTimeout(() => props.onValidation(result), 0);
|
||||
setTimeout(() => onValidation(result), 0);
|
||||
|
||||
return [
|
||||
result,
|
||||
result === FileNameValidationResult.IsOk ? Intent.NONE : Intent.DANGER,
|
||||
];
|
||||
}, [props.fileName, props.fileExtension, fileNames]);
|
||||
}, [fileName, fileExtension, fileNames]);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
@@ -118,11 +122,11 @@ const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = (
|
||||
>
|
||||
<InputGroup
|
||||
aria-label="File name"
|
||||
value={props.fileName}
|
||||
inputRef={props.inputRef}
|
||||
value={fileName}
|
||||
inputRef={inputRef}
|
||||
intent={fileNameIntent}
|
||||
rightElement={<Tag>{props.fileExtension}</Tag>}
|
||||
onChange={(e) => props.onChange(e.target.value)}
|
||||
rightElement={<Tag>{fileExtension}</Tag>}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,10 @@ type NewFileWizardProps = {
|
||||
readonly onClose: () => void;
|
||||
};
|
||||
|
||||
const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) => {
|
||||
const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -48,10 +51,10 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
|
||||
<Dialog
|
||||
icon="plus"
|
||||
title={i18n.translate(NewFileWizardStringId.Title)}
|
||||
isOpen={props.isOpen}
|
||||
isOpen={isOpen}
|
||||
onOpening={() => setFileName('')}
|
||||
onOpened={() => fileNameInputRef.current?.focus()}
|
||||
onClose={props.onClose}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<FileNameFormGroup
|
||||
@@ -83,7 +86,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
|
||||
disabled={fileNameValidation !== FileNameValidationResult.IsOk}
|
||||
onMouseDown={preventFocusOnClick}
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
onClose();
|
||||
dispatch(
|
||||
explorerCreateNewFile(
|
||||
fileName,
|
||||
|
||||
@@ -21,13 +21,15 @@ type RenameFileDialogProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = (
|
||||
props,
|
||||
) => {
|
||||
const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = ({
|
||||
oldName,
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [baseName, extension] = props.oldName.split(/(\.\w+)$/);
|
||||
const [baseName, extension] = oldName.split(/(\.\w+)$/);
|
||||
|
||||
const [newName, setNewName] = useState(baseName);
|
||||
const [result, setResult] = useState(FileNameValidationResult.Unknown);
|
||||
@@ -37,15 +39,15 @@ const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = (
|
||||
return (
|
||||
<Dialog
|
||||
title={i18n.translate(RenameFileStringId.Title, {
|
||||
fileName: props.oldName,
|
||||
fileName: oldName,
|
||||
})}
|
||||
isOpen={props.isOpen}
|
||||
isOpen={isOpen}
|
||||
onOpening={() => setNewName(baseName)}
|
||||
onOpened={() => {
|
||||
inputRef.current?.select();
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
onClose={() => props.onClose()}
|
||||
onClose={() => onClose()}
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<FileNameFormGroup
|
||||
@@ -64,10 +66,10 @@ const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = (
|
||||
disabled={result !== FileNameValidationResult.IsOk}
|
||||
onMouseDown={preventFocusOnClick}
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
onClose();
|
||||
dispatch(
|
||||
fileStorageRenameFile(
|
||||
props.oldName,
|
||||
oldName,
|
||||
`${newName}${extension}`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -14,7 +14,7 @@ import firmwareIcon from './firmware.svg';
|
||||
|
||||
type FlashButtonProps = Pick<OpenFileButtonProps, 'id'>;
|
||||
|
||||
const FlashButton: React.FunctionComponent<FlashButtonProps> = (props) => {
|
||||
const FlashButton: React.VoidFunctionComponent<FlashButtonProps> = ({ id }) => {
|
||||
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
|
||||
const bleConnection = useSelector((s) => s.ble.connection);
|
||||
const flashing = useSelector((s) => s.firmware.flashing);
|
||||
@@ -24,6 +24,7 @@ const FlashButton: React.FunctionComponent<FlashButtonProps> = (props) => {
|
||||
|
||||
return (
|
||||
<OpenFileButton
|
||||
id={id}
|
||||
fileExtension=".zip"
|
||||
icon={firmwareIcon}
|
||||
tooltip={flashing ? TooltipId.FlashProgress : TooltipId.Flash}
|
||||
@@ -43,7 +44,6 @@ const FlashButton: React.FunctionComponent<FlashButtonProps> = (props) => {
|
||||
)
|
||||
}
|
||||
onClick={() => dispatch(flashFirmware(null))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -14,7 +14,7 @@ import btDisconnectedIcon from './bt-disconnected.svg';
|
||||
|
||||
type BluetoothButtonProps = Pick<ActionButtonProps, 'id'>;
|
||||
|
||||
const BluetoothButton: React.FunctionComponent<BluetoothButtonProps> = (props) => {
|
||||
const BluetoothButton: React.VoidFunctionComponent<BluetoothButtonProps> = ({ id }) => {
|
||||
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
|
||||
const bleConnection = useSelector((s) => s.ble.connection);
|
||||
|
||||
@@ -26,6 +26,7 @@ const BluetoothButton: React.FunctionComponent<BluetoothButtonProps> = (props) =
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
id={id}
|
||||
tooltip={
|
||||
isDisconnected
|
||||
? TooltipId.BluetoothConnect
|
||||
@@ -35,7 +36,6 @@ const BluetoothButton: React.FunctionComponent<BluetoothButtonProps> = (props) =
|
||||
enabled={isDisconnected || bleConnection === BleConnectionState.Connected}
|
||||
showProgress={bleConnection === BleConnectionState.Connecting}
|
||||
onAction={() => dispatch(toggleBluetooth())}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,10 +11,12 @@ import { downloadAndRun } from './actions';
|
||||
import { HubRuntimeState } from './reducers';
|
||||
import runIcon from './run.svg';
|
||||
|
||||
type RunButtonProps = Pick<ActionButtonProps, 'id'> &
|
||||
Pick<ActionButtonProps, 'keyboardShortcut'>;
|
||||
type RunButtonProps = Pick<ActionButtonProps, 'id' | 'keyboardShortcut'>;
|
||||
|
||||
const RunButton: React.FunctionComponent<RunButtonProps> = (props) => {
|
||||
const RunButton: React.VoidFunctionComponent<RunButtonProps> = ({
|
||||
id,
|
||||
keyboardShortcut,
|
||||
}) => {
|
||||
const { editor } = useContext(EditorContext);
|
||||
const downloadProgress = useSelector((s) => s.hub.downloadProgress);
|
||||
const runtime = useSelector((s) => s.hub.runtime);
|
||||
@@ -23,6 +25,8 @@ const RunButton: React.FunctionComponent<RunButtonProps> = (props) => {
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
id={id}
|
||||
keyboardShortcut={keyboardShortcut}
|
||||
tooltip={TooltipId.Run}
|
||||
progressTooltip={TooltipId.RunProgress}
|
||||
icon={runIcon}
|
||||
@@ -30,7 +34,6 @@ const RunButton: React.FunctionComponent<RunButtonProps> = (props) => {
|
||||
showProgress={runtime === HubRuntimeState.Loading}
|
||||
progress={downloadProgress === null ? undefined : downloadProgress}
|
||||
onAction={() => dispatch(downloadAndRun())}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -10,21 +10,24 @@ import { stop } from './actions';
|
||||
import { HubRuntimeState } from './reducers';
|
||||
import stopIcon from './stop.svg';
|
||||
|
||||
type StopButtonProps = Pick<ActionButtonProps, 'id'> &
|
||||
Pick<ActionButtonProps, 'keyboardShortcut'>;
|
||||
type StopButtonProps = Pick<ActionButtonProps, 'id' | 'keyboardShortcut'>;
|
||||
|
||||
const StopButton: React.FunctionComponent<StopButtonProps> = (props) => {
|
||||
const StopButton: React.VoidFunctionComponent<StopButtonProps> = ({
|
||||
id,
|
||||
keyboardShortcut,
|
||||
}) => {
|
||||
const runtime = useSelector((s) => s.hub.runtime);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
id={id}
|
||||
keyboardShortcut={keyboardShortcut}
|
||||
tooltip={TooltipId.Stop}
|
||||
icon={stopIcon}
|
||||
enabled={runtime === HubRuntimeState.Running}
|
||||
onAction={() => dispatch(stop())}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
// The license dialog
|
||||
|
||||
@@ -28,9 +28,9 @@ type LicenseListPanelProps = {
|
||||
onItemClick(info: LicenseInfo): void;
|
||||
};
|
||||
|
||||
const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = (
|
||||
props,
|
||||
) => {
|
||||
const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = ({
|
||||
onItemClick,
|
||||
}) => {
|
||||
const licenseList = useSelector((s) => s.licenses.list);
|
||||
|
||||
return (
|
||||
@@ -43,7 +43,7 @@ const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = (
|
||||
) : (
|
||||
<ButtonGroup minimal={true} vertical={true} alignText="left">
|
||||
{licenseList.map((info, i) => (
|
||||
<Button key={i} onClick={() => props.onItemClick(info)}>
|
||||
<Button key={i} onClick={() => onItemClick(info)}>
|
||||
{info.name}
|
||||
</Button>
|
||||
))}
|
||||
@@ -107,7 +107,10 @@ type LicenseDialogProps = {
|
||||
onClose(): void;
|
||||
};
|
||||
|
||||
const LicenseDialog: React.VoidFunctionComponent<LicenseDialogProps> = (props) => {
|
||||
const LicenseDialog: React.VoidFunctionComponent<LicenseDialogProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const infoDiv = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
@@ -116,10 +119,11 @@ const LicenseDialog: React.VoidFunctionComponent<LicenseDialogProps> = (props) =
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={i18n.translate(LicenseStringId.Title)}
|
||||
onOpening={() => dispatch(fetchList())}
|
||||
className="pb-license-dialog"
|
||||
{...props}
|
||||
title={i18n.translate(LicenseStringId.Title)}
|
||||
isOpen={isOpen}
|
||||
onOpening={() => dispatch(fetchList())}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<Callout className={Classes.INTENT_PRIMARY} icon="info-sign">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
// provides translation for notification text
|
||||
|
||||
@@ -13,14 +13,17 @@ type NotificationActionProps = {
|
||||
replacements?: Replacements;
|
||||
};
|
||||
|
||||
const NotificationAction: React.FC<NotificationActionProps> = (props) => {
|
||||
const NotificationAction: React.VoidFunctionComponent<NotificationActionProps> = ({
|
||||
messageId,
|
||||
replacements,
|
||||
}) => {
|
||||
const [i18n] = useI18n({
|
||||
id: 'notification',
|
||||
translations: { en },
|
||||
fallback: en,
|
||||
});
|
||||
|
||||
return <>{i18n.translate(props.messageId, props.replacements)}</>;
|
||||
return <>{i18n.translate(messageId, replacements)}</>;
|
||||
};
|
||||
|
||||
export default NotificationAction;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
// provides translation for notification text
|
||||
|
||||
@@ -13,14 +13,17 @@ type NotificationMessageProps = {
|
||||
replacements?: Replacements;
|
||||
};
|
||||
|
||||
const NotificationMessage: React.FC<NotificationMessageProps> = (props) => {
|
||||
const NotificationMessage: React.VoidFunctionComponent<NotificationMessageProps> = ({
|
||||
messageId,
|
||||
replacements,
|
||||
}) => {
|
||||
const [i18n] = useI18n({
|
||||
id: 'notification',
|
||||
translations: { en },
|
||||
fallback: en,
|
||||
});
|
||||
|
||||
let message = i18n.translate(props.messageId, props.replacements) as
|
||||
let message = i18n.translate(messageId, replacements) as
|
||||
| React.ReactElement
|
||||
| string;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
// Provides special notification contents for unexpected errors.
|
||||
|
||||
@@ -14,9 +14,9 @@ type UnexpectedErrorNotificationProps = {
|
||||
err: Error;
|
||||
};
|
||||
|
||||
const UnexpectedErrorNotification: React.FC<UnexpectedErrorNotificationProps> = (
|
||||
props,
|
||||
) => {
|
||||
const UnexpectedErrorNotification: React.VoidFunctionComponent<
|
||||
UnexpectedErrorNotificationProps
|
||||
> = ({ messageId, err }) => {
|
||||
const [i18n] = useI18n({
|
||||
id: 'notification',
|
||||
translations: { en },
|
||||
@@ -25,9 +25,7 @@ const UnexpectedErrorNotification: React.FC<UnexpectedErrorNotificationProps> =
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{i18n.translate(props.messageId, { errorMessage: props.err.message })}
|
||||
</p>
|
||||
<p>{i18n.translate(messageId, { errorMessage: err.message })}</p>
|
||||
<div>
|
||||
<ButtonGroup minimal={true} fill={true}>
|
||||
<Button
|
||||
@@ -35,9 +33,7 @@ const UnexpectedErrorNotification: React.FC<UnexpectedErrorNotificationProps> =
|
||||
icon="duplicate"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
`\`\`\`\n${
|
||||
props.err.stack || props.err.message
|
||||
}\n\`\`\``,
|
||||
`\`\`\`\n${err.stack || err.message}\n\`\`\``,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -48,7 +44,7 @@ const UnexpectedErrorNotification: React.FC<UnexpectedErrorNotificationProps> =
|
||||
icon="virus"
|
||||
href={`https://github.com/pybricks/support/issues?q=${encodeURIComponent(
|
||||
'is:issue',
|
||||
)}+${encodeURIComponent(props.err.message)}`}
|
||||
)}+${encodeURIComponent(err.message)}`}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(MessageId.ReportBug)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
import React from 'react';
|
||||
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
|
||||
@@ -8,8 +8,18 @@ import settingsIcon from './settings.svg';
|
||||
|
||||
type SettingsButtonProps = Pick<ActionButtonProps, 'id' | 'onAction'>;
|
||||
|
||||
const SettingsButton: React.FunctionComponent<SettingsButtonProps> = (props) => {
|
||||
return <ActionButton tooltip={TooltipId.Settings} icon={settingsIcon} {...props} />;
|
||||
const SettingsButton: React.VoidFunctionComponent<SettingsButtonProps> = ({
|
||||
id,
|
||||
onAction,
|
||||
}) => {
|
||||
return (
|
||||
<ActionButton
|
||||
id={id}
|
||||
tooltip={TooltipId.Settings}
|
||||
icon={settingsIcon}
|
||||
onAction={onAction}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsButton;
|
||||
|
||||
@@ -45,7 +45,10 @@ type SettingsProps = {
|
||||
onClose(): void;
|
||||
};
|
||||
|
||||
const SettingsDrawer: React.FunctionComponent<SettingsProps> = (props) => {
|
||||
const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
|
||||
|
||||
const showDocs = useSelector((s) => s.settings.showDocs);
|
||||
@@ -89,11 +92,11 @@ const SettingsDrawer: React.FunctionComponent<SettingsProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={props.isOpen}
|
||||
isOpen={isOpen}
|
||||
icon="cog"
|
||||
size={DrawerSize.SMALL}
|
||||
title={i18n.translate(SettingsStringId.Title)}
|
||||
onClose={props.onClose}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={Classes.DRAWER_BODY}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
|
||||
@@ -40,7 +40,17 @@ export interface ActionButtonProps {
|
||||
readonly onAction: () => void;
|
||||
}
|
||||
|
||||
const ActionButton: React.FC<ActionButtonProps> = (props) => {
|
||||
const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
|
||||
id,
|
||||
keyboardShortcut,
|
||||
tooltip,
|
||||
progressTooltip,
|
||||
icon,
|
||||
enabled,
|
||||
showProgress,
|
||||
progress,
|
||||
onAction,
|
||||
}) => {
|
||||
const [i18n] = useI18n({ id: 'actionButton', translations: { en }, fallback: en });
|
||||
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(
|
||||
@@ -58,18 +68,16 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
|
||||
const buttonSize = isSmallScreen ? SpinnerSize.SMALL : SpinnerSize.STANDARD;
|
||||
|
||||
const tooltipText =
|
||||
props.showProgress && props.progressTooltip
|
||||
? i18n.translate(props.progressTooltip, {
|
||||
showProgress && progressTooltip
|
||||
? i18n.translate(progressTooltip, {
|
||||
percent:
|
||||
props.progress === undefined
|
||||
? ''
|
||||
: i18n.formatPercentage(props.progress),
|
||||
progress === undefined ? '' : i18n.formatPercentage(progress),
|
||||
})
|
||||
: i18n.translate(props.tooltip) +
|
||||
(props.keyboardShortcut ? ` (${props.keyboardShortcut})` : '');
|
||||
: i18n.translate(tooltip) +
|
||||
(keyboardShortcut ? ` (${keyboardShortcut})` : '');
|
||||
|
||||
const hotkeys = useMemo(() => {
|
||||
if (!props.keyboardShortcut) {
|
||||
if (!keyboardShortcut) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -78,16 +86,16 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
|
||||
global: true,
|
||||
allowInInput: true,
|
||||
preventDefault: true,
|
||||
combo: props.keyboardShortcut.replaceAll('-', '+'),
|
||||
label: i18n.translate(props.tooltip),
|
||||
combo: keyboardShortcut.replaceAll('-', '+'),
|
||||
label: i18n.translate(tooltip),
|
||||
onKeyDown: () => {
|
||||
if (props.enabled) {
|
||||
props.onAction();
|
||||
if (enabled) {
|
||||
onAction();
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [props.keyboardShortcut, props.tooltip, props.enabled, props.onAction, i18n]);
|
||||
}, [keyboardShortcut, tooltip, enabled, onAction, i18n]);
|
||||
|
||||
useHotkeys(hotkeys);
|
||||
|
||||
@@ -106,13 +114,13 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
|
||||
{...tooltipTargetProps}
|
||||
intent={Intent.PRIMARY}
|
||||
onMouseDown={preventFocusOnClick}
|
||||
onClick={props.onAction}
|
||||
disabled={props.enabled === false}
|
||||
style={props.enabled === false ? pointerEventsNone : undefined}
|
||||
onClick={onAction}
|
||||
disabled={enabled === false}
|
||||
style={enabled === false ? pointerEventsNone : undefined}
|
||||
>
|
||||
{props.showProgress ? (
|
||||
{showProgress ? (
|
||||
<Spinner
|
||||
value={props.progress}
|
||||
value={progress}
|
||||
intent={Intent.PRIMARY}
|
||||
size={buttonSize}
|
||||
/>
|
||||
@@ -120,8 +128,8 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
|
||||
<img
|
||||
width={`${buttonSize}px`}
|
||||
height={`${buttonSize}px`}
|
||||
src={props.icon}
|
||||
alt={props.id}
|
||||
src={icon}
|
||||
alt={id}
|
||||
style={pointerEventsNone}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,18 @@ export interface OpenFileButtonProps {
|
||||
/**
|
||||
* Button that opens a file chooser dialog or accepts files dropped on it.
|
||||
*/
|
||||
const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
|
||||
const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
|
||||
id,
|
||||
fileExtension,
|
||||
tooltip,
|
||||
icon,
|
||||
enabled,
|
||||
showProgress,
|
||||
progress,
|
||||
onFile,
|
||||
onReject,
|
||||
onClick,
|
||||
}) => {
|
||||
const [i18n] = useI18n({
|
||||
id: 'openFileButton',
|
||||
translations: { en },
|
||||
@@ -60,11 +71,11 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
|
||||
const buttonSize = isSmallScreen ? SpinnerSize.SMALL : SpinnerSize.STANDARD;
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: props.fileExtension,
|
||||
accept: fileExtension,
|
||||
// using File System Access API is blocked by https://github.com/react-dropzone/react-dropzone/issues/1141
|
||||
useFsAccessApi: false,
|
||||
multiple: false,
|
||||
noClick: props.onClick !== undefined,
|
||||
noClick: onClick !== undefined,
|
||||
onDropAccepted: (acceptedFiles) => {
|
||||
// should only be one file since multiple={false}
|
||||
acceptedFiles.forEach((f) => {
|
||||
@@ -80,7 +91,7 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
|
||||
if (typeof binaryStr === 'string') {
|
||||
throw Error('Unexpected string binaryStr');
|
||||
}
|
||||
props.onFile(binaryStr);
|
||||
onFile(binaryStr);
|
||||
};
|
||||
reader.readAsArrayBuffer(f);
|
||||
});
|
||||
@@ -88,7 +99,7 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
|
||||
onDropRejected: (fileRejections) => {
|
||||
// should only be one file since multiple={false}
|
||||
fileRejections.forEach((r) => {
|
||||
props.onReject(r.file);
|
||||
onReject(r.file);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -96,13 +107,13 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
|
||||
return (
|
||||
<Tooltip2
|
||||
content={i18n.translate(
|
||||
props.tooltip,
|
||||
props.tooltip === TooltipId.FlashProgress
|
||||
tooltip,
|
||||
tooltip === TooltipId.FlashProgress
|
||||
? {
|
||||
percent:
|
||||
props.progress === undefined
|
||||
progress === undefined
|
||||
? ''
|
||||
: i18n.formatPercentage(props.progress),
|
||||
: i18n.formatPercentage(progress),
|
||||
}
|
||||
: undefined,
|
||||
)}
|
||||
@@ -119,16 +130,16 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
|
||||
elementRef: tooltipTargetRef as IRef<HTMLButtonElement>,
|
||||
...tooltipTargetProps,
|
||||
intent: Intent.PRIMARY,
|
||||
disabled: props.enabled === false,
|
||||
style: props.enabled === false ? pointerEventsNone : undefined,
|
||||
disabled: enabled === false,
|
||||
style: enabled === false ? pointerEventsNone : undefined,
|
||||
onMouseDown: preventFocusOnClick,
|
||||
onClick: props.onClick,
|
||||
onClick: onClick,
|
||||
})}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{props.showProgress ? (
|
||||
{showProgress ? (
|
||||
<Spinner
|
||||
value={props.progress}
|
||||
value={progress}
|
||||
intent={Intent.PRIMARY}
|
||||
size={buttonSize}
|
||||
/>
|
||||
@@ -136,8 +147,8 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
|
||||
<img
|
||||
width={`${buttonSize}px`}
|
||||
height={`${buttonSize}px`}
|
||||
src={props.icon}
|
||||
alt={props.id}
|
||||
src={icon}
|
||||
alt={id}
|
||||
style={pointerEventsNone}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user