From b15b2e1deaa0ffdccabd6f6112b42ec61761c8a7 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 14 Jan 2021 10:55:00 -0600 Subject: [PATCH] move docs toggle to settings --- src/actions/settings.ts | 11 +- src/components/App.tsx | 2 +- src/components/DocsButton.tsx | 33 ----- src/components/SettingsDrawer.tsx | 25 +++- src/components/Toolbar.tsx | 2 - src/components/button-i18n.en.json | 3 +- src/components/button-i18n.ts | 1 - src/components/images/question-mark.svg | 145 -------------------- src/components/settings-i18n.en.json | 3 + src/components/settings-i18n.ts | 1 + src/reducers/app.ts | 12 +- src/reducers/settings.ts | 12 +- src/sagas/app.ts | 30 ---- src/sagas/index.ts | 2 - src/sagas/{app.test.ts => settings.test.ts} | 51 ++++--- src/sagas/settings.ts | 17 ++- 16 files changed, 93 insertions(+), 257 deletions(-) delete mode 100644 src/components/DocsButton.tsx delete mode 100644 src/components/images/question-mark.svg delete mode 100644 src/sagas/app.ts rename src/sagas/{app.test.ts => settings.test.ts} (55%) diff --git a/src/actions/settings.ts b/src/actions/settings.ts index 4b2f6556..d3731c6e 100644 --- a/src/actions/settings.ts +++ b/src/actions/settings.ts @@ -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; + +/** 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; @@ -17,4 +26,4 @@ export function toggleDarkMode(): SettingsToggleDarkModeAction { } /** common type for all settings actions. */ -export type SettingsAction = SettingsToggleDarkModeAction; +export type SettingsAction = SettingsToggleDocsAction | SettingsToggleDarkModeAction; diff --git a/src/components/App.tsx b/src/components/App.tsx index f4490d82..61d7417d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -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 ( diff --git a/src/components/DocsButton.tsx b/src/components/DocsButton.tsx deleted file mode 100644 index debf79cf..00000000 --- a/src/components/DocsButton.tsx +++ /dev/null @@ -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; -type OwnProps = Pick & - Pick; - -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); diff --git a/src/components/SettingsDrawer.tsx b/src/components/SettingsDrawer.tsx index 71ee2b77..a66cf5b8 100644 --- a/src/components/SettingsDrawer.tsx +++ b/src/components/SettingsDrawer.tsx @@ -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 { render(): JSX.Element { - const { i18n, open, onClose, darkMode, onDarkModeChanged } = this.props; + const { + i18n, + open, + onClose, + showDocs, + onShowDocsChanged, + darkMode, + onDarkModeChanged, + } = this.props; return ( onClose()} >
+ onShowDocsChanged()} + /> { 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()), }); diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index d4bfb6e9..b8ed8c90 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -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 { - diff --git a/src/components/button-i18n.en.json b/src/components/button-i18n.en.json index 78d2547b..e178f5f1 100644 --- a/src/components/button-i18n.en.json +++ b/src/components/button-i18n.en.json @@ -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" } } diff --git a/src/components/button-i18n.ts b/src/components/button-i18n.ts index fbad5705..b34b8aac 100644 --- a/src/components/button-i18n.ts +++ b/src/components/button-i18n.ts @@ -12,6 +12,5 @@ export enum TooltipId { Flash = 'flash.tooltip', BluetoothConnect = 'bluetooth.connect.tooltip', BluetoothDisconnect = 'bluetooth.disconnect.tooltip', - Docs = 'docs.tooltip', Settings = 'settings.tooltip', } diff --git a/src/components/images/question-mark.svg b/src/components/images/question-mark.svg deleted file mode 100644 index 218ad12d..00000000 --- a/src/components/images/question-mark.svg +++ /dev/null @@ -1,145 +0,0 @@ - - - -image/svg+xmlhttps://www.svgrepo.com/svg/131030/question-mark - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/components/settings-i18n.en.json b/src/components/settings-i18n.en.json index 49c8212d..94a6a92a 100644 --- a/src/components/settings-i18n.en.json +++ b/src/components/settings-i18n.en.json @@ -3,6 +3,9 @@ "title": "Settings", "appearance": { "title": "Appearance", + "documentation": { + "label": "Documentation" + }, "dark-mode": { "label": "Dark mode" } diff --git a/src/components/settings-i18n.ts b/src/components/settings-i18n.ts index 06464b31..0e7062bb 100644 --- a/src/components/settings-i18n.ts +++ b/src/components/settings-i18n.ts @@ -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', } diff --git a/src/reducers/app.ts b/src/reducers/app.ts index 2b2171ee..ca68133a 100644 --- a/src/reducers/app.ts +++ b/src/reducers/app.ts @@ -18,18 +18,8 @@ const showSettings: Reducer = (state = false, action) => { } }; -const showDocs: Reducer = (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 }); diff --git a/src/reducers/settings.ts b/src/reducers/settings.ts index a4c6e6e6..f5365542 100644 --- a/src/reducers/settings.ts +++ b/src/reducers/settings.ts @@ -7,6 +7,7 @@ import { SettingsActionType } from '../actions/settings'; export interface SettingsState { readonly darkMode: boolean; + readonly showDocs: boolean; } const darkMode: Reducer = (state = false, action) => { @@ -18,4 +19,13 @@ const darkMode: Reducer = (state = false, action) => { } }; -export default combineReducers({ darkMode }); +const showDocs: Reducer = (state = false, action) => { + switch (action.type) { + case SettingsActionType.ToggleDocs: + return !state; + default: + return state; + } +}; + +export default combineReducers({ darkMode, showDocs }); diff --git a/src/sagas/app.ts b/src/sagas/app.ts deleted file mode 100644 index 6b2e3e3c..00000000 --- a/src/sagas/app.ts +++ /dev/null @@ -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); -} diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 2c5f370a..24a2cc0c 100644 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -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(), diff --git a/src/sagas/app.test.ts b/src/sagas/settings.test.ts similarity index 55% rename from src/sagas/app.test.ts rename to src/sagas/settings.test.ts index 77ef1431..63204417 100644 --- a/src/sagas/app.test.ts +++ b/src/sagas/settings.test.ts @@ -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(); diff --git a/src/sagas/settings.ts b/src/sagas/settings.ts index 1d888233..933d1f94 100644 --- a/src/sagas/settings.ts +++ b/src/sagas/settings.ts @@ -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) {