diff --git a/src/components/tree/index.js b/src/components/tree/index.ts similarity index 93% rename from src/components/tree/index.js rename to src/components/tree/index.ts index ded81bef0..b7931c96b 100644 --- a/src/components/tree/index.js +++ b/src/components/tree/index.ts @@ -1,7 +1,8 @@ import { connect } from "react-redux"; import UnconnectedTree from "./tree"; +import { RootState } from "../../store"; -const Tree = connect((state) => ({ +const Tree = connect((state: RootState) => ({ tree: state.tree, treeToo: state.treeToo, selectedNode: state.controls.selectedNode, diff --git a/src/components/tree/tree.js b/src/components/tree/tree.js index 6589d577c..adee6cf43 100644 --- a/src/components/tree/tree.js +++ b/src/components/tree/tree.js @@ -35,21 +35,24 @@ class Tree extends React.Component { tree: null, treeToo: null }; + /* bind callbacks */ this.clearSelectedNode = callbacks.clearSelectedNode.bind(this); - // this.handleIconClickHOF = callbacks.handleIconClickHOF.bind(this); - this.redrawTree = () => { - this.props.dispatch(updateVisibleTipsAndBranchThicknesses({ - root: [0, 0] - })); - }; - /* pressing the escape key should dismiss an info modal (if one exists) */ - this.handlekeydownEvent = (event) => { - if (event.key==="Escape" && this.props.selectedNode) { - this.clearSelectedNode(this.props.selectedNode); - } - }; } + + redrawTree = () => { + this.props.dispatch(updateVisibleTipsAndBranchThicknesses({ + root: [0, 0] + })); + } + + /* pressing the escape key should dismiss an info modal (if one exists) */ + handlekeydownEvent = (event) => { + if (event.key==="Escape" && this.props.selectedNode) { + this.clearSelectedNode(this.props.selectedNode); + } + } + setUpAndRenderTreeToo(props, newState) { /* this.setState(newState) will be run sometime after this returns */ /* modifies newState in place */ @@ -59,6 +62,7 @@ class Tree extends React.Component { } renderTree(this, false, newState.treeToo, props); } + componentDidMount() { document.addEventListener('keyup', this.handlekeydownEvent); if (this.props.tree.loaded) { @@ -72,6 +76,7 @@ class Tree extends React.Component { this.setState(newState); /* this will trigger an unnecessary CDU :( */ } } + componentDidUpdate(prevProps) { let newState = {}; let rightTreeUpdated = false; @@ -110,16 +115,13 @@ class Tree extends React.Component { } getStyles = () => { - const activeResetTreeButton = this.props.tree.idxOfInViewRootNode !== 0 || - this.props.treeToo.idxOfInViewRootNode !== 0; - const filteredTree = !!this.props.tree.idxOfFilteredRoot && this.props.tree.idxOfInViewRootNode !== this.props.tree.idxOfFilteredRoot; const filteredTreeToo = !!this.props.treeToo.idxOfFilteredRoot && this.props.treeToo.idxOfInViewRootNode !== this.props.treeToo.idxOfFilteredRoot; const activeZoomButton = filteredTree || filteredTreeToo; - const treeIsZoomed = this.props.tree.idxOfInViewRootNode !== 0 || + const anyTreeZoomed = this.props.tree.idxOfInViewRootNode !== 0 || this.props.treeToo.idxOfInViewRootNode !== 0; return { @@ -133,8 +135,8 @@ class Tree extends React.Component { zIndex: 100, display: "inline-block", marginLeft: 4, - cursor: activeResetTreeButton ? "pointer" : "auto", - color: activeResetTreeButton ? darkGrey : lightGrey + cursor: anyTreeZoomed ? "pointer" : "auto", + color: anyTreeZoomed ? darkGrey : lightGrey }, zoomToSelectedButton: { zIndex: 100, @@ -146,9 +148,9 @@ class Tree extends React.Component { zoomOutButton: { zIndex: 100, display: "inline-block", - cursor: treeIsZoomed ? "pointer" : "auto", - color: treeIsZoomed ? darkGrey : lightGrey, - pointerEvents: treeIsZoomed ? "auto" : "none", + cursor: anyTreeZoomed ? "pointer" : "auto", + color: anyTreeZoomed ? darkGrey : lightGrey, + pointerEvents: anyTreeZoomed ? "auto" : "none", marginRight: "4px" } }; diff --git a/src/reducers/controls.ts b/src/reducers/controls.ts index 6682271a0..2c2a5ae2a 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -12,7 +12,24 @@ import { calcBrowserDimensionsInitialState } from "./browserDimensions"; import { doesColorByHaveConfidence } from "../actions/recomputeReduxState"; import { hasMultipleGridPanels } from "../actions/panelDisplay"; +type Layout = "rect" | "radial" | "unrooted" | "clock" | "scatter" + +interface Defaults { + distanceMeasure: string + layout: Layout + geoResolution: string + filters: Record + filtersInFooter: string[] + colorBy: string + selectedBranchLabel: string + tipLabelKey: typeof strainSymbol + showTransmissionLines: boolean + sidebarOpen?: boolean +} + export interface BasicControlsState { + defaults: Defaults + layout: Layout panelsAvailable: string[] panelsToDisplay: string[] showTreeToo: boolean @@ -40,7 +57,7 @@ export interface ControlsState extends BasicControlsState, MeasurementsControlSt at any time, e.g. if we want to revert things (e.g. on dataset change) */ export const getDefaultControlsState = () => { - const defaults: Partial = { + const defaults: Defaults = { distanceMeasure: defaultDistanceMeasure, layout: defaultLayout, geoResolution: defaultGeoResolution, diff --git a/src/reducers/index.js b/src/reducers/index.js deleted file mode 100644 index 70a568a63..000000000 --- a/src/reducers/index.js +++ /dev/null @@ -1,30 +0,0 @@ -import { combineReducers } from "redux"; -import metadata from "./metadata"; -import tree from "./tree"; -import frequencies from "./frequencies"; -import entropy from "./entropy"; -import controls from "./controls"; -import browserDimensions from "./browserDimensions"; -import notifications from "./notifications"; -import narrative from "./narrative"; -import treeToo from "./treeToo"; -import general from "./general"; -import jsonCache from "./jsonCache"; -import measurements from "./measurements"; - -const rootReducer = combineReducers({ - metadata, - tree, - frequencies, - controls, - entropy, - browserDimensions, - notifications, - narrative, - treeToo, - general, - jsonCache, - measurements -}); - -export default rootReducer; diff --git a/src/reducers/index.ts b/src/reducers/index.ts new file mode 100644 index 000000000..c6fcf7c7b --- /dev/null +++ b/src/reducers/index.ts @@ -0,0 +1,45 @@ +import { combineReducers } from "redux"; +import metadata from "./metadata"; +import tree from "./tree"; +import frequencies from "./frequencies"; +import entropy from "./entropy"; +import controls, { ControlsState } from "./controls"; +import browserDimensions from "./browserDimensions"; +import notifications from "./notifications"; +import narrative, { NarrativeState } from "./narrative"; +import treeToo from "./treeToo"; +import general from "./general"; +import jsonCache from "./jsonCache"; +import measurements from "./measurements"; + +interface RootState { + metadata: ReturnType + tree: ReturnType + frequencies: ReturnType + controls: ControlsState + entropy: ReturnType + browserDimensions: ReturnType + notifications: ReturnType + narrative: NarrativeState + treeToo: ReturnType + general: ReturnType + jsonCache: ReturnType + measurements: ReturnType +} + +const rootReducer = combineReducers({ + metadata, + tree, + frequencies, + controls, + entropy, + browserDimensions, + notifications, + narrative, + treeToo, + general, + jsonCache, + measurements +}); + +export default rootReducer; diff --git a/src/reducers/narrative.js b/src/reducers/narrative.ts similarity index 53% rename from src/reducers/narrative.js rename to src/reducers/narrative.ts index 935f81f29..ad0459e89 100644 --- a/src/reducers/narrative.js +++ b/src/reducers/narrative.ts @@ -1,19 +1,47 @@ import * as types from "../actions/types"; +import { AnyAction } from 'redux'; -const narrative = (state = { +export interface NarrativeState { + loaded: boolean + /** + * array of paragraphs (aka blocks) + */ + blocks: { __html: string }[] | null + + /** + * which block is currently "in view" + */ + blockIdx?: number + + /** + * the pathname of the _narrative_ + */ + pathname?: string + + display: boolean + title?: string +} + +const defaultState: NarrativeState = { loaded: false, - blocks: null, /* array of paragraphs (aka blocks) */ - blockIdx: undefined, /* which block is currently "in view" */ - pathname: undefined, /* the pathname of the _narrative_ */ + blocks: null, + blockIdx: undefined, + pathname: undefined, display: false, title: undefined -}, action) => { +}; + +const narrative = ( + state: NarrativeState = defaultState, + action: AnyAction, +): NarrativeState => { switch (action.type) { case types.DATA_INVALID: - return Object.assign({}, state, { + return { + ...state, loaded: false, - display: false - }); + display: false, + }; case types.CLEAN_START: if (action.narrative) { const blocks = action.narrative; @@ -29,12 +57,18 @@ const narrative = (state = { return state; case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE: if (Object.prototype.hasOwnProperty.call(action.query, "n")) { - return Object.assign({}, state, {blockIdx: action.query.n}); + return { + ...state, + blockIdx: action.query.n, + }; } return state; case types.TOGGLE_NARRATIVE: if (state.loaded) { - return Object.assign({}, state, {display: action.narrativeOn}); + return { + ...state, + display: action.narrativeOn, + }; } console.warn("Attempted to toggle narrative that was not loaded"); return state; diff --git a/src/store.ts b/src/store.ts index 829d94d98..4943b23ae 100644 --- a/src/store.ts +++ b/src/store.ts @@ -48,6 +48,7 @@ if (process.env.NODE_ENV !== 'production' && module.hot) { } // Infer types from the store. +// This is more clearly defined in src/reducers/index.ts but exported here. export type RootState = ReturnType export type AppDispatch = typeof store.dispatch