Files
flipper/desktop/app/src/chrome/SettingsSheet.tsx
Michel Weststrate e67ed8030d Make settings dialog scrollable
Summary: Make settings dialog content scrollable, which is more natural than scrolling the entire window, as reported in https://fb.workplace.com/groups/flippersupport/permalink/1063196077494383/

Reviewed By: nikoant

Differential Revision: D26015864

fbshipit-source-id: 8c74e105c290e62313e332ed1b47040eff548a97
2021-01-22 02:06:19 -08:00

431 lines
12 KiB
TypeScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {FlexColumn, Button, styled, Text, FlexRow, Spacer} from '../ui';
import React, {Component, useContext} from 'react';
import {updateSettings, Action} from '../reducers/settings';
import {
Action as LauncherAction,
LauncherSettings,
updateLauncherSettings,
} from '../reducers/launcherSettings';
import {connect} from 'react-redux';
import {State as Store} from '../reducers';
import {Settings, DEFAULT_ANDROID_SDK_PATH} from '../reducers/settings';
import {flush} from '../utils/persistor';
import ToggledSection from './settings/ToggledSection';
import {FilePathConfigField, ConfigText} from './settings/configFields';
import KeyboardShortcutInput from './settings/KeyboardShortcutInput';
import {isEqual, isMatch, isEmpty} from 'lodash';
import restartFlipper from '../utils/restartFlipper';
import LauncherSettingsPanel from '../fb-stubs/LauncherSettingsPanel';
import {reportUsage} from '../utils/metrics';
import {Modal, message} from 'antd';
import {Layout, withTrackingScope, _NuxManagerContext} from 'flipper-plugin';
const Container = styled(FlexColumn)({
padding: 20,
width: 800,
});
const Title = styled(Text)({
marginBottom: 18,
marginRight: 10,
fontWeight: 100,
fontSize: '40px',
});
type OwnProps = {
useSandy?: boolean;
onHide: () => void;
platform: NodeJS.Platform;
};
type StateFromProps = {
settings: Settings;
launcherSettings: LauncherSettings;
isXcodeDetected: boolean;
};
type DispatchFromProps = {
updateSettings: (settings: Settings) => Action;
updateLauncherSettings: (settings: LauncherSettings) => LauncherAction;
};
type State = {
updatedSettings: Settings;
updatedLauncherSettings: LauncherSettings;
forcedRestartSettings: Partial<Settings>;
forcedRestartLauncherSettings: Partial<LauncherSettings>;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
class SettingsSheet extends Component<Props, State> {
state: State = {
updatedSettings: {...this.props.settings},
updatedLauncherSettings: {...this.props.launcherSettings},
forcedRestartSettings: {},
forcedRestartLauncherSettings: {},
};
componentDidMount() {
reportUsage('settings:opened');
}
applyChanges = async () => {
this.props.updateSettings(this.state.updatedSettings);
this.props.updateLauncherSettings(this.state.updatedLauncherSettings);
this.props.onHide();
flush().then(() => {
restartFlipper(true);
});
};
applyChangesWithoutRestart = async () => {
this.props.updateSettings(this.state.updatedSettings);
this.props.updateLauncherSettings(this.state.updatedLauncherSettings);
await flush();
this.props.onHide();
};
renderSandyContainer(
contents: React.ReactElement,
footer: React.ReactElement,
) {
return (
<Modal
visible
onCancel={this.props.onHide}
width={570}
title="Settings"
footer={footer}
bodyStyle={{
overflow: 'scroll',
maxHeight: 'calc(100vh - 250px)',
}}>
{contents}
</Modal>
);
}
renderNativeContainer(
contents: React.ReactElement,
footer: React.ReactElement,
) {
return (
<Container>
<Title>Settings</Title>
{contents}
<br />
<FlexRow>
<Spacer />
{footer}
</FlexRow>
</Container>
);
}
render() {
const {
enableAndroid,
androidHome,
enableIOS,
enablePhysicalIOS,
enablePrefetching,
idbPath,
reactNative,
disableSandy,
darkMode,
} = this.state.updatedSettings;
const {useSandy} = this.props;
const settingsPristine =
isEqual(this.props.settings, this.state.updatedSettings) &&
isEqual(this.props.launcherSettings, this.state.updatedLauncherSettings);
const forcedRestart =
(!isEmpty(this.state.forcedRestartSettings) &&
!isMatch(this.props.settings, this.state.forcedRestartSettings)) ||
(!isEmpty(this.state.forcedRestartLauncherSettings) &&
!isMatch(
this.props.launcherSettings,
this.state.forcedRestartLauncherSettings,
));
const contents = (
<Layout.Container gap>
<ToggledSection
label="Android Developer"
toggled={enableAndroid}
onChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
enableAndroid: v,
},
});
}}>
<FilePathConfigField
label="Android SDK location"
resetValue={DEFAULT_ANDROID_SDK_PATH}
defaultValue={androidHome}
onChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
androidHome: v,
},
});
}}
/>
</ToggledSection>
<ToggledSection
label="iOS Developer"
toggled={enableIOS && this.props.platform === 'darwin'}
onChange={(v) => {
this.setState({
updatedSettings: {...this.state.updatedSettings, enableIOS: v},
});
}}>
{' '}
{this.props.platform === 'darwin' && (
<ConfigText
content={'Use "xcode-select" to switch between Xcode versions'}
/>
)}
{this.props.platform !== 'darwin' && (
<ConfigText
content={
'iOS development has limited functionality on non-MacOS devices'
}
/>
)}
<ToggledSection
label="Enable physical iOS devices"
toggled={enablePhysicalIOS}
frozen={false}
onChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
enablePhysicalIOS: v,
},
});
}}>
<FilePathConfigField
label="IDB binary location"
defaultValue={idbPath}
isRegularFile={true}
onChange={(v) => {
this.setState({
updatedSettings: {...this.state.updatedSettings, idbPath: v},
});
}}
/>
</ToggledSection>
</ToggledSection>
<LauncherSettingsPanel
isPrefetchingEnabled={enablePrefetching}
onEnablePrefetchingChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
enablePrefetching: v,
},
});
}}
isLocalPinIgnored={this.state.updatedLauncherSettings.ignoreLocalPin}
onIgnoreLocalPinChange={(v) => {
this.setState({
updatedLauncherSettings: {
...this.state.updatedLauncherSettings,
ignoreLocalPin: v,
},
});
}}
releaseChannel={this.state.updatedLauncherSettings.releaseChannel}
onReleaseChannelChange={(v) => {
this.setState({
updatedLauncherSettings: {
...this.state.updatedLauncherSettings,
releaseChannel: v,
},
forcedRestartLauncherSettings: {
...this.state.forcedRestartLauncherSettings,
releaseChannel: v,
},
});
}}
/>
<ToggledSection
label="Disable Sandy UI"
toggled={this.state.updatedSettings.disableSandy}
onChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
disableSandy: v,
},
forcedRestartSettings: {
...this.state.forcedRestartSettings,
disableSandy: v,
},
});
}}>
{' '}
<ConfigText
content={
'If disabled, Flipper will fall back to the classic design.'
}
/>
</ToggledSection>
{!disableSandy && (
<ToggledSection
label="Enable dark theme (experimental)"
toggled={darkMode}
onChange={(enabled) => {
this.setState((prevState) => ({
updatedSettings: {
...prevState.updatedSettings,
darkMode: enabled,
},
}));
}}
/>
)}
<ToggledSection
label="React Native keyboard shortcuts"
toggled={reactNative.shortcuts.enabled}
onChange={(enabled) => {
this.setState((prevState) => ({
updatedSettings: {
...prevState.updatedSettings,
reactNative: {
...prevState.updatedSettings.reactNative,
shortcuts: {
...prevState.updatedSettings.reactNative.shortcuts,
enabled,
},
},
},
}));
}}>
<KeyboardShortcutInput
label="Reload application"
value={reactNative.shortcuts.reload}
onChange={(reload) => {
this.setState((prevState) => ({
updatedSettings: {
...prevState.updatedSettings,
reactNative: {
...prevState.updatedSettings.reactNative,
shortcuts: {
...prevState.updatedSettings.reactNative.shortcuts,
reload,
},
},
},
}));
}}
/>
<KeyboardShortcutInput
label="Open developer menu"
value={reactNative.shortcuts.openDevMenu}
onChange={(openDevMenu) => {
this.setState((prevState) => ({
updatedSettings: {
...prevState.updatedSettings,
reactNative: {
...prevState.updatedSettings.reactNative,
shortcuts: {
...prevState.updatedSettings.reactNative.shortcuts,
openDevMenu,
},
},
},
}));
}}
/>
</ToggledSection>
<Layout.Right center>
<span>Reset all new user tooltips</span>
<ResetTooltips />
</Layout.Right>
<Layout.Right center>
<span>Reset all local storage based state</span>
<ResetLocalState />
</Layout.Right>
</Layout.Container>
);
const footer = (
<>
<Button compact padded onClick={this.props.onHide}>
Cancel
</Button>
<Button
disabled={settingsPristine || forcedRestart}
compact
padded
onClick={this.applyChangesWithoutRestart}>
Apply
</Button>
<Button
disabled={settingsPristine}
type="primary"
compact
padded
onClick={this.applyChanges}>
Apply and Restart
</Button>
</>
);
return useSandy
? this.renderSandyContainer(contents, footer)
: this.renderNativeContainer(contents, footer);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({settingsState, launcherSettingsState, application}) => ({
settings: settingsState,
launcherSettings: launcherSettingsState,
isXcodeDetected: application.xcodeCommandLineToolsDetected,
}),
{updateSettings, updateLauncherSettings},
)(withTrackingScope(SettingsSheet));
function ResetTooltips() {
const nuxManager = useContext(_NuxManagerContext);
return (
<Button
onClick={() => {
nuxManager.resetHints();
}}>
Reset hints
</Button>
);
}
function ResetLocalState() {
return (
<Button
type="danger"
onClick={() => {
window.localStorage.clear();
message.success('Local storage state cleared');
}}>
Reset all state
</Button>
);
}