Unverified Commit 73e6006d authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Refactor trackPanel state and update TrackPanelListItem (#158)

parent bf7a5bef
Pipeline #36295 passed with stages
in 5 minutes and 53 seconds
......@@ -61,7 +61,6 @@ describe('<BrowserBar />', () => {
isTrackPanelOpened: false,
isFocusObjectInDefaultPosition: true,
closeDrawer: jest.fn(),
selectTrackPanelTabAndSave: jest.fn(),
toggleTrackPanel: jest.fn(),
changeFocusObject: jest.fn()
};
......
......@@ -32,7 +32,7 @@ import {
getIsTrackPanelOpened
} from '../track-panel/trackPanelSelectors';
import {
selectTrackPanelTabAndSave,
selectTrackPanelTab,
toggleTrackPanel
} from '../track-panel/trackPanelActions';
import { closeDrawer } from '../drawer/drawerActions';
......@@ -67,7 +67,7 @@ type StateProps = {
type DispatchProps = {
closeDrawer: () => void;
selectTrackPanelTabAndSave: (selectedTrackPanelTab: TrackSet) => void;
selectTrackPanelTab: (selectedTrackPanelTab: TrackSet) => void;
toggleBrowserNav: () => void;
toggleGenomeSelector: (genomeSelectorActive: boolean) => void;
toggleTrackPanel: (isTrackPanelOpened: boolean) => void;
......@@ -188,7 +188,7 @@ export const BrowserBar: FunctionComponent<BrowserBarProps> = (
ensObject={props.ensObject}
isDrawerOpened={props.isDrawerOpened}
genomeSelectorActive={props.genomeSelectorActive}
selectTrackPanelTabAndSave={props.selectTrackPanelTabAndSave}
selectTrackPanelTab={props.selectTrackPanelTab}
selectedTrackPanelTab={props.selectedTrackPanelTab}
toggleTrackPanel={props.toggleTrackPanel}
isTrackPanelModalOpened={props.isTrackPanelModalOpened}
......@@ -265,7 +265,7 @@ const mapStateToProps = (state: RootState): StateProps => ({
const mapDispatchToProps: DispatchProps = {
closeDrawer,
selectTrackPanelTabAndSave,
selectTrackPanelTab,
toggleBrowserNav,
toggleGenomeSelector,
toggleTrackPanel,
......
......@@ -87,33 +87,34 @@ describe('BrowserStorageService', () => {
});
});
describe('.getTrackListToggleStates()', () => {
it('gets saved track list toggle states from storage service', () => {
describe('.getTrackPanels()', () => {
it('gets saved track panels configuration from storage service', () => {
const mockTrackPanels = { foo: 'doesnt really matter' };
jest
.spyOn(mockStorageService, 'get')
.mockImplementation(() => trackListToggleStates);
.mockImplementation(() => mockTrackPanels);
const browserStorageService = new BrowserStorageService(
mockStorageService
);
const result = browserStorageService.getTrackListToggleStates();
const result = browserStorageService.getTrackPanels();
expect(mockStorageService.get).toHaveBeenCalledWith(
StorageKeys.TRACK_LIST_TOGGLE_STATES
StorageKeys.TRACK_PANELS
);
expect(result).toEqual(trackListToggleStates);
expect(result).toEqual(mockTrackPanels);
mockStorageService.get.mockRestore();
});
it('returns an empty object if there are no saved track list toggle states', () => {
it('returns an empty object if there are no saved track panel configurations', () => {
jest.spyOn(mockStorageService, 'get').mockImplementation(() => null);
const browserStorageService = new BrowserStorageService(
mockStorageService
);
const result = browserStorageService.getTrackListToggleStates();
const result = browserStorageService.getTrackPanels();
expect(result).toEqual({});
......@@ -121,72 +122,18 @@ describe('BrowserStorageService', () => {
});
});
describe('.updateTrackListToggleStates()', () => {
it('updates track list toggle states via storage service', () => {
describe('.updateTrackPanels()', () => {
it('updates stored track panel configurations', () => {
const mockTrackPanels = { foo: {} };
const browserStorageService = new BrowserStorageService(
mockStorageService
);
const toggledTrackState = { homo_sapiens38: { 'gene-feat': true } };
browserStorageService.updateTrackListToggleStates(toggledTrackState);
expect(mockStorageService.update).toHaveBeenCalledWith(
StorageKeys.TRACK_LIST_TOGGLE_STATES,
toggledTrackState
);
});
});
describe('.getSelectedTrackPanelTab()', () => {
it('gets saved selected browser tab from storage service', () => {
jest
.spyOn(mockStorageService, 'get')
.mockImplementation(() => SELECTED_TRACK_PANEL_TAB);
const browserStorageService = new BrowserStorageService(
mockStorageService
);
const result = browserStorageService.getSelectedTrackPanelTab();
expect(mockStorageService.get).toHaveBeenCalledWith(
StorageKeys.SELECTED_TRACK_PANEL_TAB
);
expect(result).toEqual(SELECTED_TRACK_PANEL_TAB);
mockStorageService.get.mockRestore();
});
it('returns "GENOMIC" if there is no saved selected browser tab', () => {
jest.spyOn(mockStorageService, 'get').mockImplementation(() => null);
const browserStorageService = new BrowserStorageService(
mockStorageService
);
const result = browserStorageService.getSelectedTrackPanelTab();
expect(result).toEqual({});
mockStorageService.get.mockRestore();
});
});
describe('.updateSelectedTrackPanelTab()', () => {
it('updates selected browser tab via storage service', () => {
const browserStorageService = new BrowserStorageService(
mockStorageService
);
browserStorageService.updateSelectedTrackPanelTab({
homo_sapiens38: TrackSet.EXPRESSION
});
browserStorageService.updateTrackPanels(mockTrackPanels);
expect(mockStorageService.update).toHaveBeenCalledWith(
StorageKeys.SELECTED_TRACK_PANEL_TAB,
{
homo_sapiens38: TrackSet.EXPRESSION
}
StorageKeys.TRACK_PANELS,
mockTrackPanels
);
});
});
......
import storageService, {
StorageServiceInterface
} from 'src/services/storage-service';
import {
TrackStates,
TrackSet,
TrackToggleStates
} from './track-panel/trackPanelConfig';
import { TrackStates } from './track-panel/trackPanelConfig';
import { ChrLocations } from './browserState';
import {
TrackPanelState,
TrackPanelStateForGenome
} from 'src/content/app/browser/track-panel/trackPanelState';
export enum StorageKeys {
ACTIVE_GENOME_ID = 'browser.activeGenomeId',
......@@ -14,7 +14,7 @@ export enum StorageKeys {
CHR_LOCATION = 'browser.chrLocation',
DEFAULT_CHR_LOCATION = 'browser.defaultChrLocation',
TRACK_STATES = 'browser.trackStates',
TRACK_LIST_TOGGLE_STATES = 'browser.trackListToggleStates',
TRACK_PANELS = 'browser.trackPanels',
SELECTED_TRACK_PANEL_TAB = 'browser.selectedTrackPanelTab'
}
......@@ -25,7 +25,7 @@ export class BrowserStorageService {
this.storageService = storageService;
}
public getActiveGenomeId(): string {
public getActiveGenomeId(): string | null {
return this.storageService.get(StorageKeys.ACTIVE_GENOME_ID);
}
......@@ -62,30 +62,14 @@ export class BrowserStorageService {
this.storageService.save(StorageKeys.TRACK_STATES, trackStates);
}
public getTrackListToggleStates() {
return this.storageService.get(StorageKeys.TRACK_LIST_TOGGLE_STATES) || {};
}
public updateTrackListToggleStates(toggleState: {
[genomeId: string]: TrackToggleStates;
}) {
this.storageService.update(
StorageKeys.TRACK_LIST_TOGGLE_STATES,
toggleState
);
}
public getSelectedTrackPanelTab(): { [genomeId: string]: TrackSet } {
return this.storageService.get(StorageKeys.SELECTED_TRACK_PANEL_TAB) || {};
public getTrackPanels(): { [genomeId: string]: Partial<TrackPanelState> } {
return this.storageService.get(StorageKeys.TRACK_PANELS) || {};
}
public updateSelectedTrackPanelTab(selectedTrackPanelTabForGenome: {
[genomeId: string]: TrackSet;
}) {
this.storageService.update(
StorageKeys.SELECTED_TRACK_PANEL_TAB,
selectedTrackPanelTabForGenome
);
public updateTrackPanels(trackPanels: {
[genomeId: string]: Partial<TrackPanelStateForGenome>;
}): void {
this.storageService.update(StorageKeys.TRACK_PANELS, trackPanels);
}
}
......
......@@ -17,6 +17,7 @@ import {
BrowserEntityState,
defaultBrowserEntityState
} from './browserState';
import trackPanelReducer from 'src/content/app/browser/track-panel/trackPanelReducer';
export function browserInfo(
state: BrowserState = defaultBrowserState,
......@@ -165,5 +166,6 @@ export default combineReducers({
browserEntity,
browserLocation,
browserNav,
trackConfig
trackConfig,
trackPanel: trackPanelReducer
});
......@@ -8,7 +8,6 @@ import TrackPanelModal from './track-panel-modal/TrackPanelModal';
import Drawer from '../drawer/Drawer';
import { RootState } from 'src/store';
import { toggleTrackPanel } from './trackPanelActions';
import {
getIsTrackPanelOpened,
getIsTrackPanelModalOpened,
......@@ -42,20 +41,11 @@ type TrackPanelProps = {
selectedTrackPanelTab: TrackSet;
genomeTrackCategories: GenomeTrackCategory[];
trackStates: TrackStates;
toggleTrackPanel: (isTrackPanelOpened?: boolean) => void;
};
const TrackPanel = (props: TrackPanelProps) => {
const { isDrawerOpened } = props;
useEffect(() => {
if (props.breakpointWidth !== BreakpointWidth.LARGE) {
props.toggleTrackPanel(false);
} else {
props.toggleTrackPanel(true);
}
}, [props.breakpointWidth, props.toggleTrackPanel]);
const [trackAnimation, setTrackAnimation] = useSpring(() => ({
config: { tension: 280, friction: 45 },
height: '100%',
......@@ -112,11 +102,4 @@ const mapStateToProps = (state: RootState) => {
};
};
const mapDispatchToProps = {
toggleTrackPanel
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(TrackPanel);
export default connect(mapStateToProps)(TrackPanel);
import React, { MouseEvent, ReactNode, useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { RootState } from 'src/store';
import classNames from 'classnames';
import { RootState } from 'src/store';
import analyticsTracking from 'src/services/analytics-service';
import browserMessagingService from 'src/content/app/browser/browser-messaging-service';
import ImageButton, {
ImageButtonStatus
} from 'src/shared/components/image-button/ImageButton';
......@@ -13,14 +16,18 @@ import {
TrackItemColourKey,
TrackId
} from '../trackPanelConfig';
import {
updateTrackStatesAndSave,
UpdateTrackStatesPayload
} from 'src/content/app/browser/browserActions';
import { updateCollapsedTrackIds } from 'src/content/app/browser/track-panel/trackPanelActions';
import { changeDrawerView, toggleDrawer } from '../../drawer/drawerActions';
import browserMessagingService from 'src/content/app/browser/browser-messaging-service';
import { gethighlightedTrackId } from 'src/content/app/browser/track-panel/trackPanelSelectors';
import browserStorageService from '../../browser-storage-service';
import {
getHighlightedTrackId,
isTrackCollapsed
} from 'src/content/app/browser/track-panel/trackPanelSelectors';
import { EnsObjectTrack } from 'src/ens-object/ensObjectTypes';
import { getIsDrawerOpened, getDrawerView } from '../../drawer/drawerSelectors';
import { getBrowserActiveGenomeId } from '../../browserSelectors';
......@@ -32,23 +39,32 @@ import { ReactComponent as Ellipsis } from 'static/img/track-panel/ellipsis.svg'
import styles from './TrackPanelListItem.scss';
type Props = {
changeDrawerView: (drawerView: string) => void;
toggleDrawer: (isDrawerOpened: boolean) => void;
updateTrackStates: (payload: UpdateTrackStatesPayload) => void;
type OwnProps = {
categoryName: string;
children?: ReactNode[];
trackStatus: ImageButtonStatus;
defaultTrackStatus: ImageButtonStatus;
track: EnsObjectTrack;
highlightedTrackId: string;
};
type PropsFromRedux = {
activeGenomeId: string | null;
isDrawerOpened: boolean;
drawerView: string;
highlightedTrackId: string;
isCollapsed: boolean;
changeDrawerView: (drawerView: string) => void;
toggleDrawer: (isDrawerOpened: boolean) => void;
updateTrackStates: (payload: UpdateTrackStatesPayload) => void;
updateCollapsedTrackIds: (payload: {
trackId: string;
isCollapsed: boolean;
}) => void;
};
type Props = OwnProps & PropsFromRedux;
const TrackPanelListItem = (props: Props) => {
const [expanded, setExpanded] = useState(true);
const {
activeGenomeId,
categoryName,
......@@ -65,19 +81,6 @@ const TrackPanelListItem = (props: Props) => {
}
}, []);
useEffect(() => {
const trackToggleStates = browserStorageService.getTrackListToggleStates();
if (
activeGenomeId &&
track.child_tracks &&
trackToggleStates[activeGenomeId] &&
trackToggleStates[activeGenomeId][track.track_id] !== undefined
) {
setExpanded(trackToggleStates[activeGenomeId][track.track_id]);
}
}, []);
const updateDrawerView = (currentTrack: string) => {
const { drawerView, toggleDrawer, changeDrawerView } = props;
......@@ -123,11 +126,9 @@ const TrackPanelListItem = (props: Props) => {
};
const toggleExpand = () => {
setExpanded(!expanded);
browserStorageService.updateTrackListToggleStates({
[activeGenomeId as string]: { [track.track_id]: !expanded }
});
const { track_id: trackId } = props.track;
const { isCollapsed } = props;
props.updateCollapsedTrackIds({ trackId, isCollapsed: !isCollapsed });
};
const toggleTrack = () => {
......@@ -185,8 +186,8 @@ const TrackPanelListItem = (props: Props) => {
{track.child_tracks && (
<button onClick={toggleExpand} className={styles.expandBtn}>
<img
src={expanded ? chevronUpIcon : chevronDownIcon}
alt={expanded ? 'collapse' : 'expand'}
src={props.isCollapsed ? chevronDownIcon : chevronUpIcon}
alt={props.isCollapsed ? 'expand' : 'collapse'}
/>
</button>
)}
......@@ -208,19 +209,21 @@ const TrackPanelListItem = (props: Props) => {
/>
</div>
</dd>
{expanded && props.children}
{!props.isCollapsed && props.children}
</>
);
};
const mapStateToProps = (state: RootState) => ({
const mapStateToProps = (state: RootState, ownProps: OwnProps) => ({
activeGenomeId: getBrowserActiveGenomeId(state),
isDrawerOpened: getIsDrawerOpened(state),
drawerView: getDrawerView(state),
highlightedTrackId: gethighlightedTrackId(state)
highlightedTrackId: getHighlightedTrackId(state),
isCollapsed: isTrackCollapsed(state, ownProps.track.track_id)
});
const mapDispatchToProps = {
updateCollapsedTrackIds,
changeDrawerView,
toggleDrawer,
updateTrackStates: updateTrackStatesAndSave
......
......@@ -11,7 +11,7 @@ type TrackPanelTabsProps = {
ensObject: EnsObject;
isDrawerOpened: boolean;
genomeSelectorActive: boolean;
selectTrackPanelTabAndSave: (selectedTrackPanelTab: TrackSet) => void;
selectTrackPanelTab: (selectedTrackPanelTab: TrackSet) => void;
selectedTrackPanelTab: TrackSet;
toggleTrackPanel: (isTrackPanelOpened: boolean) => void;
isTrackPanelModalOpened: boolean;
......@@ -32,7 +32,7 @@ const TrackPanelTabs = (props: TrackPanelTabsProps) => {
props.closeDrawer();
}
props.selectTrackPanelTabAndSave(value);
props.selectTrackPanelTab(value);
};
const getTrackPanelTabClassNames = (trackSet: TrackSet) => {
......
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import faker from 'faker';
import browserStorageService from 'src/content/app/browser/browser-storage-service';
import * as trackPanelActions from './trackPanelActions';
import {
pickPersistentTrackPanelProperties,
TrackPanelStateForGenome
} from './trackPanelState';
import { TrackSet } from './trackPanelConfig';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const trackPanelProperties: TrackPanelStateForGenome = {
isTrackPanelModalOpened: faker.random.boolean(),
selectedTrackPanelTab: TrackSet.GENOMIC,
trackPanelModalView: faker.lorem.word(),
highlightedTrackId: faker.lorem.word(),
isTrackPanelOpened: faker.random.boolean(),
collapsedTrackIds: [faker.lorem.word()]
};
describe('track panel actions', () => {
beforeEach(() => {
jest.spyOn(browserStorageService, 'updateTrackPanels');
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('updateTrackPanelForGenome', () => {
it('saves a subset of track panel properties every time it‘s called', () => {
const store = mockStore({});
const genomeId = faker.random.word();
store.dispatch(
trackPanelActions.updateTrackPanelForGenome({
activeGenomeId: genomeId,
data: trackPanelProperties
})
);
expect(browserStorageService.updateTrackPanels).toHaveBeenCalledTimes(1);
const storedPayload = (browserStorageService.updateTrackPanels as any)
.mock.calls[0][0];
const storedData = storedPayload[genomeId];
const allowedProperties = pickPersistentTrackPanelProperties(
trackPanelProperties
);
expect(storedData).toEqual(allowedProperties);
});
});
});
import { createStandardAction } from 'typesafe-actions';
import { createAction } from 'typesafe-actions';
import { ThunkAction } from 'redux-thunk';
import { Action, ActionCreator } from 'redux';
import uniq from 'lodash/uniq';
import { RootState } from 'src/store';
import { TrackSet } from './trackPanelConfig';
......@@ -8,11 +9,25 @@ import browserStorageService from '../browser-storage-service';
import { getBrowserActiveGenomeId } from '../browserSelectors';
import analyticsTracking from 'src/services/analytics-service';
import { getActiveTrackPanel } from './trackPanelSelectors';
import { TrackPanelStateForGenome } from './trackPanelState';
export const updateTrackPanelForGenome = createStandardAction(
'track-panel/update-track-panel'
)<{ activeGenomeId: string; data: TrackPanelStateForGenome }>();
import {
pickPersistentTrackPanelProperties,
TrackPanelStateForGenome
} from './trackPanelState';
export const updateTrackPanelForGenome = createAction(
'track-panel/update-track-panel',
(action) => (payload: {
activeGenomeId: string;
data: Partial<TrackPanelStateForGenome>;
}) => {
const { activeGenomeId, data } = payload;
const persistentTrackProperties = pickPersistentTrackPanelProperties(data);
browserStorageService.updateTrackPanels({
[activeGenomeId]: persistentTrackProperties
});
return action({ activeGenomeId, data });
}
);
export const toggleTrackPanel: ActionCreator<
ThunkAction<void, any, null, Action<string>>
......@@ -34,7 +49,7 @@ export const toggleTrackPanel: ActionCreator<
);
};
export const selectTrackPanelTabAndSave: ActionCreator<
export const selectTrackPanelTab: ActionCreator<
ThunkAction<void, any, null, Action<string>>
> = (selectedTrackPanelTab: TrackSet) => (
dispatch,
......@@ -46,10 +61,6 @@ export const selectTrackPanelTabAndSave: ActionCreator<
return;
}
browserStorageService.updateSelectedTrackPanelTab({
[activeGenomeId]: selectedTrackPanelTab
});
analyticsTracking.trackEvent({
category: 'track_panel_tab',