settings: convert SettingsDrawer to function

This reduces boilerplate code.
This commit is contained in:
David Lechner
2021-12-27 12:36:38 -06:00
parent f55bc919f6
commit a1db455a66
+307 -370
View File
@@ -9,9 +9,6 @@ import {
ControlGroup,
Drawer,
FormGroup,
Hotkey,
Hotkeys,
HotkeysTarget,
Icon,
InputGroup,
Intent,
@@ -19,10 +16,11 @@ import {
Position,
Switch,
Tooltip,
useHotkeys,
} from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import { connect } from 'react-redux';
import { useI18n } from '@shopify/react-i18n';
import React, { useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AboutDialog from '../about/AboutDialog';
import { checkForUpdate, installPrompt, reload } from '../app/actions';
import {
@@ -35,7 +33,6 @@ import {
import { pseudolocalize } from '../i18n';
import { RootState } from '../reducers';
import ExternalLinkIcon from '../utils/ExternalLinkIcon';
import { BeforeInstallPromptEvent } from '../utils/dom';
import { isMacOS } from '../utils/os';
import { setBoolean, setString, toggleBoolean } from './actions';
import { BooleanSettingId, StringSettingId } from './defaults';
@@ -43,376 +40,316 @@ import { SettingsStringId } from './i18n';
import en from './i18n.en.json';
import './settings.scss';
type StateProps = {
showDocs: boolean;
darkMode: boolean;
flashCurrentProgram: boolean;
serviceWorker: ServiceWorkerRegistration | null;
checkingForUpdate: boolean;
updateAvailable: boolean;
beforeInstallPrompt: BeforeInstallPromptEvent | null;
promptingInstall: boolean;
readyForOfflineUse: boolean;
hubName: string;
isHubNameValid: boolean;
};
type DispatchProps = {
onShowDocsChanged: (checked: boolean) => void;
onDarkModeChanged: (checked: boolean) => void;
onFlashCurrentProgramChanged: (checked: boolean) => void;
onToggleDocs: () => void;
onCheckForUpdate: (registration: ServiceWorkerRegistration) => void;
onReload: (registration: ServiceWorkerRegistration) => void;
onInstallPrompt: (event: BeforeInstallPromptEvent) => void;
onHubNameChange: React.FormEventHandler<HTMLInputElement>;
};
type OwnProps = {
type SettingsProps = {
isOpen: boolean;
onClose(): void;
};
type SettingsProps = StateProps & DispatchProps & OwnProps & WithI18nProps;
const SettingsDrawer: React.FunctionComponent<SettingsProps> = (props) => {
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
@HotkeysTarget
class SettingsDrawer extends React.PureComponent<SettingsProps> {
public state = {
aboutDialogIsOpen: false,
};
const showDocs = useSelector((state: RootState) => state.settings.showDocs);
const darkMode = useSelector((state: RootState) => state.settings.darkMode);
const flashCurrentProgram = useSelector(
(state: RootState) => state.settings.flashCurrentProgram,
);
const serviceWorker = useSelector((state: RootState) => state.app.serviceWorker);
const checkingForUpdate = useSelector(
(state: RootState) => state.app.checkingForUpdate,
);
const updateAvailable = useSelector(
(state: RootState) => state.app.updateAvailable,
);
const beforeInstallPrompt = useSelector(
(state: RootState) => state.app.beforeInstallPrompt,
);
const promptingInstall = useSelector(
(state: RootState) => state.app.promptingInstall,
);
const readyForOfflineUse = useSelector(
(state: RootState) => state.app.readyForOfflineUse,
);
const hubName = useSelector((state: RootState) => state.settings.hubName);
const isHubNameValid = useSelector(
(state: RootState) => state.settings.isHubNameValid,
);
render(): JSX.Element {
const {
showDocs,
darkMode,
serviceWorker,
flashCurrentProgram,
checkingForUpdate,
updateAvailable,
beforeInstallPrompt,
promptingInstall,
readyForOfflineUse,
onShowDocsChanged,
onDarkModeChanged,
onFlashCurrentProgramChanged,
onCheckForUpdate,
onReload,
onInstallPrompt,
isOpen,
onClose,
i18n,
hubName,
isHubNameValid,
onHubNameChange,
} = this.props;
return (
<Drawer
isOpen={isOpen}
icon="cog"
size={Drawer.SIZE_SMALL}
title={i18n.translate(SettingsStringId.Title)}
onClose={() => onClose()}
>
<div className={Classes.DRAWER_BODY}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={i18n.translate(SettingsStringId.AppearanceTitle)}
helperText={i18n.translate(
SettingsStringId.AppearanceZoomHelp,
{
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
},
)}
>
<Tooltip
content={i18n.translate(
SettingsStringId.AppearanceDocumentationTooltip,
)}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDocumentationLabel,
)}
checked={showDocs}
onChange={(e) =>
onShowDocsChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip>
<Tooltip
content={i18n.translate(
SettingsStringId.AppearanceDarkModeTooltip,
)}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDarkModeLabel,
)}
checked={darkMode}
onChange={(e) =>
onDarkModeChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip>
</FormGroup>
<FormGroup
label={i18n.translate(SettingsStringId.FirmwareTitle)}
>
<Tooltip
content={i18n.translate(
SettingsStringId.FirmwareCurrentProgramTooltip,
)}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.FirmwareCurrentProgramLabel,
)}
checked={flashCurrentProgram}
onChange={(e) =>
onFlashCurrentProgramChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip>
<ControlGroup>
<Tooltip
content={i18n.translate(
SettingsStringId.FirmwareHubNameTooltip,
)}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Label
className={Classes.INLINE}
htmlFor="hub-name-input"
>
{i18n.translate(
SettingsStringId.FirmwareHubNameLabel,
)}
</Label>
</Tooltip>
<InputGroup
id="hub-name-input"
value={hubName}
onChange={onHubNameChange}
onMouseOver={(e) => e.preventDefault()}
className="pb-hub-name-input"
intent={
isHubNameValid ? Intent.NONE : Intent.DANGER
}
placeholder="Pybricks Hub"
rightElement={
isHubNameValid ? undefined : (
<Tooltip
content={i18n.translate(
SettingsStringId.FirmwareHubNameErrorTooltip,
)}
boundary="window"
position={Position.BOTTOM}
targetTagName="div"
>
<Icon
icon="error"
intent={Intent.DANGER}
itemType="div"
/>
</Tooltip>
)
}
/>
</ControlGroup>
</FormGroup>
<FormGroup label={i18n.translate(SettingsStringId.HelpTitle)}>
<ButtonGroup
minimal={true}
vertical={true}
alignText="left"
>
<AnchorButton
icon="lightbulb"
href={pybricksProjectsUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpProjectsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="help"
href={pybricksSupportUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpSupportLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="chat"
href={pybricksGitterUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpChatLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="virus"
href={pybricksBugReportsUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpBugsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AboutDialog
isOpen={this.state.aboutDialogIsOpen}
onClose={() =>
this.setState({ aboutDialogIsOpen: false })
}
/>
</ButtonGroup>
</FormGroup>
<FormGroup
label={i18n.translate(SettingsStringId.AppTitle)}
helperText={
readyForOfflineUse &&
i18n.translate(SettingsStringId.AppOfflineUseHelp)
}
>
<ButtonGroup
minimal={true}
vertical={true}
alignText="left"
>
{beforeInstallPrompt && (
<Button
icon="add"
onClick={() =>
onInstallPrompt(beforeInstallPrompt)
}
loading={promptingInstall}
>
{i18n.translate(
SettingsStringId.AppInstallLabel,
)}
</Button>
)}
{serviceWorker && !updateAvailable && (
<Button
icon="refresh"
onClick={() => onCheckForUpdate(serviceWorker)}
loading={checkingForUpdate}
>
{i18n.translate(
SettingsStringId.AppCheckForUpdateLabel,
)}
</Button>
)}
{serviceWorker && updateAvailable && (
<Button
icon="refresh"
onClick={() => onReload(serviceWorker)}
>
{i18n.translate(
SettingsStringId.AppRestartLabel,
)}
</Button>
)}
<Button
icon="info-sign"
onClick={() => {
this.setState({ aboutDialogIsOpen: true });
return true;
}}
>
{i18n.translate(SettingsStringId.AppAboutLabel)}
</Button>
</ButtonGroup>
</FormGroup>
{process.env.NODE_ENV === 'development' && (
<FormGroup label="Developer">
<Switch
checked={i18n.pseudolocalize !== false}
onChange={() =>
pseudolocalize(!i18n.pseudolocalize)
}
label="Pseudolocalize"
/>
</FormGroup>
const dispatch = useDispatch();
const [i18n] = useI18n({
id: 'settings',
translations: { en },
fallback: en,
});
const hotkeys = useMemo(
() => [
{
combo: 'mod+d',
label: i18n.translate(SettingsStringId.AppearanceDocumentationTooltip),
global: true,
preventDefault: true,
onKeyDown: () => dispatch(toggleBoolean(BooleanSettingId.ShowDocs)),
},
],
[],
);
useHotkeys(hotkeys);
return (
<Drawer
isOpen={props.isOpen}
icon="cog"
size={Drawer.SIZE_SMALL}
title={i18n.translate(SettingsStringId.Title)}
onClose={() => props.onClose()}
>
<div className={Classes.DRAWER_BODY}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={i18n.translate(SettingsStringId.AppearanceTitle)}
helperText={i18n.translate(
SettingsStringId.AppearanceZoomHelp,
{
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
},
)}
</div>
</div>
</Drawer>
);
}
renderHotkeys(): JSX.Element {
return (
<Hotkeys>
<Hotkey
combo="mod+d"
label={this.props.i18n.translate(
SettingsStringId.AppearanceDocumentationTooltip,
>
<Tooltip
content={i18n.translate(
SettingsStringId.AppearanceDocumentationTooltip,
)}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDocumentationLabel,
)}
checked={showDocs}
onChange={(e) =>
dispatch(
setBoolean(
BooleanSettingId.ShowDocs,
(e.target as HTMLInputElement).checked,
),
)
}
/>
</Tooltip>
<Tooltip
content={i18n.translate(
SettingsStringId.AppearanceDarkModeTooltip,
)}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDarkModeLabel,
)}
checked={darkMode}
onChange={(e) =>
dispatch(
setBoolean(
BooleanSettingId.DarkMode,
(e.target as HTMLInputElement).checked,
),
)
}
/>
</Tooltip>
</FormGroup>
<FormGroup label={i18n.translate(SettingsStringId.FirmwareTitle)}>
<Tooltip
content={i18n.translate(
SettingsStringId.FirmwareCurrentProgramTooltip,
)}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.FirmwareCurrentProgramLabel,
)}
checked={flashCurrentProgram}
onChange={(e) =>
dispatch(
setBoolean(
BooleanSettingId.FlashCurrentProgram,
(e.target as HTMLInputElement).checked,
),
)
}
/>
</Tooltip>
<ControlGroup>
<Tooltip
content={i18n.translate(
SettingsStringId.FirmwareHubNameTooltip,
)}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Label
className={Classes.INLINE}
htmlFor="hub-name-input"
>
{i18n.translate(
SettingsStringId.FirmwareHubNameLabel,
)}
</Label>
</Tooltip>
<InputGroup
id="hub-name-input"
value={hubName}
onChange={(e) =>
dispatch(
setString(
StringSettingId.HubName,
e.currentTarget.value,
),
)
}
onMouseOver={(e) => e.preventDefault()}
className="pb-hub-name-input"
intent={isHubNameValid ? Intent.NONE : Intent.DANGER}
placeholder="Pybricks Hub"
rightElement={
isHubNameValid ? undefined : (
<Tooltip
content={i18n.translate(
SettingsStringId.FirmwareHubNameErrorTooltip,
)}
boundary="window"
position={Position.BOTTOM}
targetTagName="div"
>
<Icon
icon="error"
intent={Intent.DANGER}
itemType="div"
/>
</Tooltip>
)
}
/>
</ControlGroup>
</FormGroup>
<FormGroup label={i18n.translate(SettingsStringId.HelpTitle)}>
<ButtonGroup minimal={true} vertical={true} alignText="left">
<AnchorButton
icon="lightbulb"
href={pybricksProjectsUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpProjectsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="help"
href={pybricksSupportUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpSupportLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="chat"
href={pybricksGitterUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpChatLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="virus"
href={pybricksBugReportsUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpBugsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AboutDialog
isOpen={isAboutDialogOpen}
onClose={() => setIsAboutDialogOpen(false)}
/>
</ButtonGroup>
</FormGroup>
<FormGroup
label={i18n.translate(SettingsStringId.AppTitle)}
helperText={
readyForOfflineUse &&
i18n.translate(SettingsStringId.AppOfflineUseHelp)
}
>
<ButtonGroup minimal={true} vertical={true} alignText="left">
{beforeInstallPrompt && (
<Button
icon="add"
onClick={() =>
dispatch(installPrompt(beforeInstallPrompt))
}
loading={promptingInstall}
>
{i18n.translate(SettingsStringId.AppInstallLabel)}
</Button>
)}
{serviceWorker && !updateAvailable && (
<Button
icon="refresh"
onClick={() =>
dispatch(checkForUpdate(serviceWorker))
}
loading={checkingForUpdate}
>
{i18n.translate(
SettingsStringId.AppCheckForUpdateLabel,
)}
</Button>
)}
{serviceWorker && updateAvailable && (
<Button
icon="refresh"
onClick={() => dispatch(reload(serviceWorker))}
>
{i18n.translate(SettingsStringId.AppRestartLabel)}
</Button>
)}
<Button
icon="info-sign"
onClick={() => {
setIsAboutDialogOpen(true);
return true;
}}
>
{i18n.translate(SettingsStringId.AppAboutLabel)}
</Button>
</ButtonGroup>
</FormGroup>
{process.env.NODE_ENV === 'development' && (
<FormGroup label="Developer">
<Switch
checked={i18n.pseudolocalize !== false}
onChange={() => pseudolocalize(!i18n.pseudolocalize)}
label="Pseudolocalize"
/>
</FormGroup>
)}
global={true}
preventDefault={true}
onKeyDown={() => this.props.onToggleDocs()}
/>
</Hotkeys>
);
}
}
const mapStateToProps = (state: RootState): StateProps => ({
showDocs: state.settings.showDocs,
darkMode: state.settings.darkMode,
flashCurrentProgram: state.settings.flashCurrentProgram,
serviceWorker: state.app.serviceWorker,
checkingForUpdate: state.app.checkingForUpdate,
updateAvailable: state.app.updateAvailable,
beforeInstallPrompt: state.app.beforeInstallPrompt,
promptingInstall: state.app.promptingInstall,
readyForOfflineUse: state.app.readyForOfflineUse,
hubName: state.settings.hubName,
isHubNameValid: state.settings.isHubNameValid,
});
const mapDispatchToProps: DispatchProps = {
onShowDocsChanged: (checked) => setBoolean(BooleanSettingId.ShowDocs, checked),
onDarkModeChanged: (checked) => setBoolean(BooleanSettingId.DarkMode, checked),
onFlashCurrentProgramChanged: (checked) =>
setBoolean(BooleanSettingId.FlashCurrentProgram, checked),
onToggleDocs: () => toggleBoolean(BooleanSettingId.ShowDocs),
onCheckForUpdate: checkForUpdate,
onReload: reload,
onInstallPrompt: installPrompt,
onHubNameChange: (event) =>
setString(StringSettingId.HubName, event.currentTarget.value),
</div>
</div>
</Drawer>
);
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(
withI18n({
id: 'settings',
fallback: en,
translations: { en },
})(SettingsDrawer),
);
export default SettingsDrawer;