Add docs toggle button

This commit is contained in:
David Lechner
2020-06-12 21:46:00 -05:00
committed by David Lechner
parent bda007c17a
commit 1e0ef476ef
12 changed files with 253 additions and 25 deletions
+33
View File
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// File: actions/app.ts
// Actions for the app in general.
import { Action } from 'redux';
/** App action types. */
export enum AppActionType {
/** The app has just ben started. */
Startup = 'app.action.startup',
/** Toggle documentation visibility. */
ToggleDocs = 'app.action.toggleDocs',
}
/** Action that indicates the app has just started. */
export type AppStartupAction = Action<AppActionType.Startup>;
/** Creates an action that indicates the app has just started. */
export function startup(): AppStartupAction {
return { type: AppActionType.Startup };
}
/** Action to toggle documentation visibility. */
export type AppToggleDocsAction = Action<AppActionType.ToggleDocs>;
/** Creates an action to toggle documentation visibility. */
export function toggleDocs(): AppToggleDocsAction {
return { type: AppActionType.ToggleDocs };
}
/** common type for all app actions. */
export type AppAction = AppStartupAction | AppToggleDocsAction;
+2
View File
@@ -2,6 +2,7 @@
// Copyright (c) 2020 The Pybricks Authors
import { Dispatch as ReduxDispatch } from 'redux';
import { AppAction } from './app';
import { BLEAction, BLEConnectAction } from './ble';
import { BleUartAction } from './ble-uart';
import { EditorAction } from './editor';
@@ -22,6 +23,7 @@ import { TerminalDataAction } from './terminal';
* Common type for all actions.
*/
export type Action =
| AppAction
| BLEAction
| BLEConnectAction
| BleUartAction
+4 -22
View File
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import React, { EffectCallback, useEffect, useState } from 'react';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import SplitterLayout from 'react-splitter-layout';
import { RootState } from '../reducers';
import Editor from './Editor';
import StatusBar from './StatusBar';
import Terminal from './Terminal';
@@ -10,28 +12,8 @@ import Toolbar from './Toolbar';
import 'react-splitter-layout/lib/index.css';
function useShowDocs(): boolean {
function getShowDocs(): boolean {
return window.innerWidth >= 1024;
}
const [showDocs, setShowDocs] = useState(getShowDocs);
useEffect((): ReturnType<EffectCallback> => {
function handleResize(): void {
setShowDocs(getShowDocs());
}
window.addEventListener('resize', handleResize);
return (): void => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount and unmount
return showDocs;
}
function App(): JSX.Element {
const showDocs = useShowDocs();
const showDocs = useSelector((s: RootState): boolean => s.app.showDocs);
const [dragging, setDragging] = useState(false);
return (
+33
View File
@@ -0,0 +1,33 @@
// 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';
import docsIcon from './images/run.svg'; // FIXME: need proper icon
type StateProps = {};
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
type OwnProps = Pick<ActionButtonProps, 'id'>;
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onAction: (): Action => dispatch(toggleDocs()),
});
const mergeProps = (
stateProps: StateProps,
dispatchProps: DispatchProps,
ownProps: OwnProps,
): ActionButtonProps => ({
tooltip: TooltipId.Docs,
icon: docsIcon,
...ownProps,
...stateProps,
...dispatchProps,
});
export default connect(undefined, mapDispatchToProps, mergeProps)(ActionButton);
+7 -1
View File
@@ -1,9 +1,10 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { ButtonGroup, Navbar } from '@blueprintjs/core';
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';
@@ -36,6 +37,11 @@ class Toolbar extends React.Component {
<FlashButton id="flash" />
</ButtonGroup>
</Navbar.Group>
<Navbar.Group align={Alignment.RIGHT}>
<ButtonGroup>
<DocsButton id="docs" />
</ButtonGroup>
</Navbar.Group>
</Navbar>
);
}
+2 -1
View File
@@ -8,5 +8,6 @@
"connect": { "tooltip": "Connect using Bluetooth" },
"disconnect": { "tooltip": "Disconnect Bluetooth" }
},
"flash": { "tooltip": "Flash hub firmware" }
"flash": { "tooltip": "Flash hub firmware" },
"docs": { "tooltip": "Show/hide documentation" }
}
+1
View File
@@ -10,4 +10,5 @@ export enum TooltipId {
Flash = 'flash.tooltip',
BluetoothConnect = 'bluetooth.connect.tooltip',
BluetoothDisconnect = 'bluetooth.disconnect.tooltip',
Docs = 'docs.tooltip',
}
+23
View File
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// File: reducers/app.ts
// Manages state the app in general.
import { Reducer, combineReducers } from 'redux';
import { Action } from '../actions';
import { AppActionType } from '../actions/app';
const showDocs: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case AppActionType.ToggleDocs:
return !state;
default:
return state;
}
};
export interface AppState {
readonly showDocs: boolean;
}
export default combineReducers({ showDocs });
+3
View File
@@ -2,6 +2,7 @@
// Copyright (c) 2020 The Pybricks Authors
import { combineReducers } from 'redux';
import app, { AppState } from './app';
import ble, { BleState } from './ble';
import bootloader, { BootloaderState } from './bootloader';
import editor, { EditorState } from './editor';
@@ -14,6 +15,7 @@ import terminal, { TerminalState } from './terminal';
* Root state for redux store.
*/
export interface RootState {
readonly app: AppState;
readonly bootloader: BootloaderState;
readonly ble: BleState;
readonly editor: EditorState;
@@ -24,6 +26,7 @@ export interface RootState {
}
export default combineReducers({
app,
bootloader,
ble,
editor,
+110
View File
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 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';
afterAll(() => {
jest.restoreAllMocks();
});
describe('startup', () => {
test('with large screen', async () => {
const saga = new AsyncSaga(app);
jest.spyOn(
Object.getPrototypeOf(window.localStorage),
'getItem',
).mockReturnValue(null);
innerWidth = 1024;
saga.put(startup());
// toggles documentation to be visible
const toggleDocsAction = await saga.take();
expect(toggleDocsAction.type).toBe(AppActionType.ToggleDocs);
await saga.end();
});
test('with small screen', async () => {
const saga = new AsyncSaga(app);
jest.spyOn(
Object.getPrototypeOf(window.localStorage),
'getItem',
).mockReturnValue(null);
innerWidth = 800;
saga.put(startup());
// does nothing
await saga.end();
});
test('with stored value "true"', async () => {
const saga = new AsyncSaga(app);
jest.spyOn(
Object.getPrototypeOf(window.localStorage),
'getItem',
).mockReturnValue('true');
innerWidth = 800;
saga.put(startup());
// toggles documentation to be visible
const toggleDocsAction = await saga.take();
expect(toggleDocsAction.type).toBe(AppActionType.ToggleDocs);
await saga.end();
});
test('with stored value "false"', async () => {
const saga = new AsyncSaga(app);
jest.spyOn(
Object.getPrototypeOf(window.localStorage),
'getItem',
).mockReturnValue('false');
innerWidth = 1024;
saga.put(startup());
// does nothing
await saga.end();
});
});
describe('storeDocsState', () => {
test('showing', async () => {
const saga = new AsyncSaga(app);
const mockSetItem = jest
.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem')
.mockImplementation((_key, value) => expect(value).toBe('true'));
saga.setState({ app: { showDocs: true } });
saga.put(toggleDocs());
expect(mockSetItem).toHaveBeenCalled();
await saga.end();
});
test('hidden', async () => {
const saga = new AsyncSaga(app);
const mockSetItem = jest
.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem')
.mockImplementation((_key, value) => expect(value).toBe('false'));
saga.setState({ app: { showDocs: false } });
saga.put(toggleDocs());
expect(mockSetItem).toHaveBeenCalled();
await saga.end();
});
});
+30
View File
@@ -0,0 +1,30 @@
// 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);
}
+5 -1
View File
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { all } from 'redux-saga/effects';
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';
@@ -14,6 +16,7 @@ import terminal from './terminal';
/* istanbul ignore next */
export default function* (): Generator {
yield all([
app(),
bleUart(),
bootloader(),
editor(),
@@ -22,5 +25,6 @@ export default function* (): Generator {
hub(),
mpy(),
terminal(),
put(startup()),
]);
}