move docs toggle to settings

This commit is contained in:
David Lechner
2021-01-14 10:55:00 -06:00
parent b6bda26484
commit b15b2e1dea
16 changed files with 93 additions and 257 deletions
+10 -1
View File
@@ -5,9 +5,18 @@ import { Action } from 'redux';
/** Actions related to settings. */
export enum SettingsActionType {
ToggleDocs = 'settings.action.toggleDocs',
ToggleDarkMode = 'settings.action.toggleDarkMode',
}
/** Action to toggle show docs setting. */
export type SettingsToggleDocsAction = Action<SettingsActionType.ToggleDocs>;
/** Toggles show docs setting on or off. */
export function toggleDocs(): SettingsToggleDocsAction {
return { type: SettingsActionType.ToggleDocs };
}
/** Action to toggle dark mode setting. */
export type SettingsToggleDarkModeAction = Action<SettingsActionType.ToggleDarkMode>;
@@ -17,4 +26,4 @@ export function toggleDarkMode(): SettingsToggleDarkModeAction {
}
/** common type for all settings actions. */
export type SettingsAction = SettingsToggleDarkModeAction;
export type SettingsAction = SettingsToggleDocsAction | SettingsToggleDarkModeAction;
+1 -1
View File
@@ -14,7 +14,7 @@ import Toolbar from './Toolbar';
import 'react-splitter-layout/lib/index.css';
function App(): JSX.Element {
const showDocs = useSelector((s: RootState): boolean => s.app.showDocs);
const showDocs = useSelector((s: RootState): boolean => s.settings.showDocs);
const [dragging, setDragging] = useState(false);
return (
-33
View File
@@ -1,33 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// File: components/DocsButton.ts
// Toolbar button for toggling documentation.
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { toggleDocs } from '../actions/app';
import ActionButton, { ActionButtonProps } from './ActionButton';
import { TooltipId } from './button-i18n';
import docsIcon from './images/question-mark.svg';
type StateProps = undefined;
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
type OwnProps = Pick<ActionButtonProps, 'id'> &
Pick<ActionButtonProps, 'keyboardShortcut'>;
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onAction: (): Action => dispatch(toggleDocs()),
});
const mergeProps = (
_stateProps: StateProps,
dispatchProps: DispatchProps,
ownProps: OwnProps,
): ActionButtonProps => ({
tooltip: TooltipId.Docs,
icon: docsIcon,
...ownProps,
...dispatchProps,
});
export default connect(undefined, mapDispatchToProps, mergeProps)(ActionButton);
+23 -2
View File
@@ -7,7 +7,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { closeSettings } from '../actions/app';
import { toggleDarkMode } from '../actions/settings';
import { toggleDarkMode, toggleDocs } from '../actions/settings';
import { RootState } from '../reducers';
import { SettingsStringId } from './settings-i18n';
import en from './settings-i18n.en.json';
@@ -16,11 +16,13 @@ import './settings.scss';
type StateProps = {
open: boolean;
showDocs: boolean;
darkMode: boolean;
};
type DispatchProps = {
onClose: () => void;
onShowDocsChanged: () => void;
onDarkModeChanged: () => void;
};
@@ -28,16 +30,33 @@ type SettingsProps = StateProps & DispatchProps & WithI18nProps;
class SettingsDrawer extends React.PureComponent<SettingsProps> {
render(): JSX.Element {
const { i18n, open, onClose, darkMode, onDarkModeChanged } = this.props;
const {
i18n,
open,
onClose,
showDocs,
onShowDocsChanged,
darkMode,
onDarkModeChanged,
} = this.props;
return (
<Drawer
isOpen={open}
icon="cog"
size={Drawer.SIZE_SMALL}
title={i18n.translate(SettingsStringId.Title)}
onClose={() => onClose()}
>
<div className="pb-settings">
<FormGroup label={i18n.translate(SettingsStringId.AppearanceTitle)}>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDocumentationLabel,
)}
large={true}
checked={showDocs}
onChange={() => onShowDocsChanged()}
/>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDarkModeLabel,
@@ -55,11 +74,13 @@ class SettingsDrawer extends React.PureComponent<SettingsProps> {
const mapStateToProps = (state: RootState): StateProps => ({
open: state.app.showSettings,
showDocs: state.settings.showDocs,
darkMode: state.settings.darkMode,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onClose: (): Action => dispatch(closeSettings()),
onShowDocsChanged: (): Action => dispatch(toggleDocs()),
onDarkModeChanged: (): Action => dispatch(toggleDarkMode()),
});
-2
View File
@@ -4,7 +4,6 @@
import { Alignment, ButtonGroup, Navbar } from '@blueprintjs/core';
import React from 'react';
import BluetoothButton from './BluetoothButton';
import DocsButton from './DocsButton';
import FlashButton from './FlashButton';
import OpenButton from './OpenButton';
import ReplButton from './ReplButton';
@@ -40,7 +39,6 @@ class Toolbar extends React.Component {
<Navbar.Group align={Alignment.RIGHT}>
<ButtonGroup>
<SettingsButton id="settings" />
<DocsButton id="docs" />
</ButtonGroup>
</Navbar.Group>
</Navbar>
+1 -2
View File
@@ -9,6 +9,5 @@
"disconnect": { "tooltip": "Disconnect Bluetooth" }
},
"flash": { "tooltip": "Flash hub firmware" },
"docs": { "tooltip": "Show/hide documentation" },
"settings": { "tooltip": "Open setting" }
"settings": { "tooltip": "Settings" }
}
-1
View File
@@ -12,6 +12,5 @@ export enum TooltipId {
Flash = 'flash.tooltip',
BluetoothConnect = 'bluetooth.connect.tooltip',
BluetoothDisconnect = 'bluetooth.disconnect.tooltip',
Docs = 'docs.tooltip',
Settings = 'settings.tooltip',
}
-145
View File
@@ -1,145 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="50"
height="50"
viewBox="0 0 50.000001 50.000001"
xml:space="preserve"
sodipodi:docname="question-mark.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata
id="metadata43"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title><cc:license
rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" /><dc:source>https://www.svgrepo.com/svg/131030/question-mark</dc:source></cc:Work><cc:License
rdf:about="http://creativecommons.org/publicdomain/zero/1.0/"><cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /></cc:License></rdf:RDF></metadata><defs
id="defs41" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1376"
id="namedview39"
showgrid="false"
inkscape:pagecheckerboard="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="3.8803824"
inkscape:cx="21.870149"
inkscape:cy="16.167941"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<g
id="g6"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,4.9999645,5.0000021)">
<path
d="m 502.29,788.199 h -47 c -33.1,0 -60,26.9 -60,60 v 64.9 c 0,33.1 26.9,60 60,60 h 47 c 33.101,0 60,-26.9 60,-60 v -64.9 c 0,-33.199 -26.899,-60 -60,-60 z"
id="path2"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1" />
<path
d="m 170.89,285.8 86.7,10.8 c 27.5,3.4 53.6,-12.4 63.5,-38.3 12.5,-32.7 29.9,-58.5 52.2,-77.3 31.601,-26.6 70.9,-40 117.9,-40 48.7,0 87.5,12.8 116.3,38.3 28.8,25.6 43.1,56.2 43.1,92.1 0,25.8 -8.1,49.4 -24.3,70.8 -10.5,13.6 -42.8,42.2 -96.7,85.9 -54,43.7 -89.899,83.099 -107.899,118.099 -18.4,35.801 -24.8,75.5 -26.4,115.301 -1.399,34.1 25.8,62.5 60,62.5 h 49 c 31.2,0 57,-23.9 59.8,-54.9 2,-22.299 5.7,-39.199 11.301,-50.699 9.399,-19.701 33.699,-45.701 72.699,-78.1 C 723.59,477.8 772.79,428.4 795.891,392 c 23,-36.3 34.6,-74.8 34.6,-115.5 0,-73.5 -31.3,-138 -94,-193.4 -62.6,-55.4 -147,-83.1 -253,-83.1 -100.8,0 -182.1,27.3 -244.1,82 -52.8,46.6 -84.9,101.8 -96.2,165.5 -3.501,18.6 9.199,36 27.699,38.3 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1" />
</g>
<g
id="g8"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g10"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g12"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g14"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g16"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g18"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g20"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g22"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g24"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g26"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g28"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g30"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g32"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g34"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
<g
id="g36"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.04110579,0,0,0.04110579,-0.86208638,5.0000021)">
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.2 KiB

+3
View File
@@ -3,6 +3,9 @@
"title": "Settings",
"appearance": {
"title": "Appearance",
"documentation": {
"label": "Documentation"
},
"dark-mode": {
"label": "Dark mode"
}
+1
View File
@@ -6,5 +6,6 @@
export enum SettingsStringId {
Title = 'settings.title',
AppearanceTitle = 'settings.appearance.title',
AppearanceDocumentationLabel = 'settings.appearance.documentation.label',
AppearanceDarkModeLabel = 'settings.appearance.dark-mode.label',
}
+1 -11
View File
@@ -18,18 +18,8 @@ const showSettings: Reducer<boolean, Action> = (state = false, action) => {
}
};
const showDocs: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case AppActionType.ToggleDocs:
return !state;
default:
return state;
}
};
export interface AppState {
readonly showSettings: boolean;
readonly showDocs: boolean;
}
export default combineReducers({ showSettings, showDocs });
export default combineReducers({ showSettings });
+11 -1
View File
@@ -7,6 +7,7 @@ import { SettingsActionType } from '../actions/settings';
export interface SettingsState {
readonly darkMode: boolean;
readonly showDocs: boolean;
}
const darkMode: Reducer<boolean, Action> = (state = false, action) => {
@@ -18,4 +19,13 @@ const darkMode: Reducer<boolean, Action> = (state = false, action) => {
}
};
export default combineReducers({ darkMode });
const showDocs: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case SettingsActionType.ToggleDocs:
return !state;
default:
return state;
}
};
export default combineReducers({ darkMode, showDocs });
-30
View File
@@ -1,30 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// File: sagas/app.ts
// Manages the application lifecycle.
import { put, select, takeEvery } from 'redux-saga/effects';
import {
AppActionType,
AppStartupAction,
AppToggleDocsAction,
toggleDocs,
} from '../actions/app';
import { RootState } from '../reducers';
function* handleStartup(_action: AppStartupAction): Generator {
const showDocs = localStorage.getItem('showDocs');
if (showDocs === null ? window.innerWidth >= 1024 : showDocs === 'true') {
yield put(toggleDocs());
}
}
function* storeDocsState(_action: AppToggleDocsAction): Generator {
const showDocs = (yield select((s: RootState) => s.app.showDocs)) as boolean;
localStorage.setItem('showDocs', String(showDocs));
}
export default function* (): Generator {
yield takeEvery(AppActionType.Startup, handleStartup);
yield takeEvery(AppActionType.ToggleDocs, storeDocsState);
}
-2
View File
@@ -3,7 +3,6 @@
import { all, put } from 'redux-saga/effects';
import { startup } from '../actions/app';
import app from './app';
import bleUart from './ble-uart';
import editor from './editor';
import errorLog from './error-log';
@@ -18,7 +17,6 @@ import terminal from './terminal';
/* istanbul ignore next */
export default function* (): Generator {
yield all([
app(),
bleUart(),
lwp3BootloaderBle(),
lwp3BootloaderProtocol(),
@@ -1,11 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
// File: sagas/app.test.ts
// Tests for app sagas.
import { AsyncSaga } from '../../test';
import { AppActionType, startup, toggleDocs } from '../actions/app';
import app from './app';
import { startup } from '../actions/app';
import { SettingsActionType, toggleDarkMode, toggleDocs } from '../actions/settings';
import settings from './settings';
afterAll(() => {
jest.restoreAllMocks();
@@ -13,7 +14,7 @@ afterAll(() => {
describe('startup', () => {
test('with large screen', async () => {
const saga = new AsyncSaga(app);
const saga = new AsyncSaga(settings);
jest.spyOn(
Object.getPrototypeOf(window.localStorage),
@@ -25,13 +26,13 @@ describe('startup', () => {
// toggles documentation to be visible
const toggleDocsAction = await saga.take();
expect(toggleDocsAction.type).toBe(AppActionType.ToggleDocs);
expect(toggleDocsAction.type).toBe(SettingsActionType.ToggleDocs);
await saga.end();
});
test('with small screen', async () => {
const saga = new AsyncSaga(app);
const saga = new AsyncSaga(settings);
jest.spyOn(
Object.getPrototypeOf(window.localStorage),
@@ -47,30 +48,30 @@ describe('startup', () => {
});
test('with stored value "true"', async () => {
const saga = new AsyncSaga(app);
const saga = new AsyncSaga(settings);
jest.spyOn(
Object.getPrototypeOf(window.localStorage),
'getItem',
).mockReturnValue('true');
).mockReturnValue('{"showDocs":true}');
innerWidth = 800;
saga.put(startup());
// toggles documentation to be visible
const toggleDocsAction = await saga.take();
expect(toggleDocsAction.type).toBe(AppActionType.ToggleDocs);
expect(toggleDocsAction.type).toBe(SettingsActionType.ToggleDocs);
await saga.end();
});
test('with stored value "false"', async () => {
const saga = new AsyncSaga(app);
const saga = new AsyncSaga(settings);
jest.spyOn(
Object.getPrototypeOf(window.localStorage),
'getItem',
).mockReturnValue('false');
).mockReturnValue('{"showDocs":false}');
innerWidth = 1024;
saga.put(startup());
@@ -81,28 +82,36 @@ describe('startup', () => {
});
});
describe('storeDocsState', () => {
test('showing', async () => {
const saga = new AsyncSaga(app);
describe('store settings to local storage', () => {
test('showDocs', async () => {
const saga = new AsyncSaga(settings);
// NOTE: we aren't testing reducers here, so value doesn't change
// even though we call the toggle function
const mockSetItem = jest
.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem')
.mockImplementation((_key, value) => expect(value).toBe('true'));
saga.setState({ app: { showSettings: false, showDocs: true } });
.mockImplementation((_key, value) =>
expect(value).toBe('{"darkMode":false,"showDocs":true}'),
);
saga.setState({ settings: { darkMode: false, showDocs: true } });
saga.put(toggleDocs());
expect(mockSetItem).toHaveBeenCalled();
await saga.end();
});
test('hidden', async () => {
const saga = new AsyncSaga(app);
test('darkMode', async () => {
const saga = new AsyncSaga(settings);
// NOTE: we aren't testing reducers here, so value doesn't change
// even though we call the toggle function
const mockSetItem = jest
.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem')
.mockImplementation((_key, value) => expect(value).toBe('false'));
saga.setState({ app: { showSettings: false, showDocs: false } });
saga.put(toggleDocs());
.mockImplementation((_key, value) =>
expect(value).toBe('{"darkMode":true,"showDocs":false}'),
);
saga.setState({ settings: { darkMode: true, showDocs: false } });
saga.put(toggleDarkMode());
expect(mockSetItem).toHaveBeenCalled();
await saga.end();
+12 -5
View File
@@ -1,17 +1,23 @@
import { put, select, takeEvery } from 'redux-saga/effects';
import { AppActionType } from '../actions/app';
import { SettingsActionType, toggleDarkMode } from '../actions/settings';
import { SettingsActionType, toggleDarkMode, toggleDocs } from '../actions/settings';
import { RootState } from '../reducers';
import { SettingsState } from '../reducers/settings';
function* loadSettings(): Generator {
const settingsString = localStorage.getItem('settings');
if (!settingsString) {
return;
}
const settingsString = localStorage.getItem('settings') || '{}';
const settings = JSON.parse(settingsString) as SettingsState;
// TODO: there has to be a better way to initialize app state from settings
if (
settings.showDocs === undefined
? window.innerWidth >= 1024
: settings.showDocs === true
) {
yield put(toggleDocs());
}
if (settings.darkMode) {
yield put(toggleDarkMode());
}
@@ -22,6 +28,7 @@ function* saveSettings(): Generator {
localStorage.setItem('settings', JSON.stringify(settings));
}
// TODO: this should really be part of component, not saga
function* updateDarkModeClass(): Generator {
const darkMode = (yield select((s: RootState) => s.settings.darkMode)) as boolean;
if (darkMode) {