Unverified Commit 2a96301a authored by Manoj Pandian Sakthivel's avatar Manoj Pandian Sakthivel Committed by GitHub
Browse files

Recently viewed bookmarks (#145)

* Save automatic bookmarks

* Save track state along with bookmarks

* Style changes

* SAve bookmarks under genome

* Add test to storage service

* Build bookmark function

* GetExample & bookmark links as its own component

* Separate bookmarks from previously viewed

* Automatically close bookmark tab

* Use previously viewed links instead of bookmarks

* Get previousky viewed links from localstorage onload

* Put back accidentally removed code

* Limit to 20 bookmarks

* Tests for bookmark

* Put back the location

* PR review fixes

* Update wording and names in TrackPanelBookmarks test file

* More updates to names

* PR review changes and fixes

* Add all previously drawer view

* Close drawer on bookmark select

* Minor changes

* Update tests

* fix types

* PR review changes

* Fix first focus object bookmark

* fix bug switching between track panel models

* Restore track states from bookmarks

* Use assign instead of merge

* focus object specific track states

* retain common track states across focus objects

* Close drawer along with modal

* Cleanups and more tests

* Fix restoring common tracks

* rename clear track states to reset

* Turn on all tracks when you reset

* PR review fixes

* Fix an issue with wrong object ID

* Fix types

* Cleanup browser useEffect

* Remove unused import

* Fix tests

* Remove console log

* Rename drawer title

* Fix Drawer links position issue

* PR review fixes

* Incorporate reset tracks feature

* Move reserBrowser function to helpers

* Remove resetBrowserTrackStates helper

* Css cleanups

* Remove dd dt & dl

* Remove unused css

* Drawer links css update
parent e65093ae
Pipeline #43169 passed with stages
in 5 minutes and 22 seconds
......@@ -20,7 +20,8 @@ import {
changeBrowserLocation,
changeFocusObject,
setDataFromUrlAndSave,
ParsedUrlPayload
ParsedUrlPayload,
restoreBrowserTrackStates
} from './browserActions';
import {
getBrowserNavOpened,
......@@ -51,8 +52,7 @@ import {
} from './drawer/drawerActions';
import browserStorageService from './browser-storage-service';
import { TrackStates } from './track-panel/trackPanelConfig';
import { BrowserTrackStates } from './track-panel/trackPanelConfig';
import * as urlFor from 'src/shared/helpers/urlHelper';
import styles from './Browser.scss';
......@@ -77,10 +77,15 @@ type StateProps = {
};
type DispatchProps = {
changeBrowserLocation: (genomeId: string, chrLocation: ChrLocation) => void;
changeBrowserLocation: (locationData: {
genomeId: string;
ensObjectId: string | null;
chrLocation: ChrLocation;
}) => void;
changeFocusObject: (objectId: string) => void;
changeDrawerView: (drawerView: string) => void;
closeDrawer: () => void;
restoreBrowserTrackStates: () => void;
fetchGenomeData: (genomeId: string) => void;
replace: Replace;
toggleDrawer: (isDrawerOpened: boolean) => void;
......@@ -102,7 +107,7 @@ export const Browser: FunctionComponent<BrowserProps> = (
props: BrowserProps
) => {
const [trackStatesFromStorage, setTrackStatesFromStorage] = useState<
TrackStates
BrowserTrackStates
>({});
const { isDrawerOpened, closeDrawer } = props;
......@@ -127,20 +132,29 @@ export const Browser: FunctionComponent<BrowserProps> = (
chrLocation
};
props.setDataFromUrlAndSave(payload);
if (chrLocation) {
dispatchBrowserLocation(genomeId, chrLocation);
} else if (focus) {
if (focus && !chrLocation) {
/*
changeFocusObject needs to be called before setDataFromUrlAndSave
in order to prevent creating an previouslyViewedObject entry
for the focus object that is viewed first.
*/
props.changeFocusObject(focus);
} else if (focus && chrLocation) {
props.changeFocusObject(focus);
props.changeBrowserLocation({
genomeId,
ensObjectId: focus,
chrLocation
});
} else if (chrLocation) {
props.changeBrowserLocation({
genomeId,
ensObjectId: focus,
chrLocation
});
}
};
const dispatchBrowserLocation = (
genomeId: string,
chrLocation: ChrLocation
) => {
props.changeBrowserLocation(genomeId, chrLocation);
props.setDataFromUrlAndSave(payload);
};
const changeSelectedSpecies = (genomeId: string) => {
......@@ -194,6 +208,7 @@ export const Browser: FunctionComponent<BrowserProps> = (
useEffect(() => {
setTrackStatesFromStorage(browserStorageService.getTrackStates());
props.restoreBrowserTrackStates();
}, [props.activeGenomeId, props.activeEnsObjectId]);
useEffect(() => {
......@@ -206,7 +221,7 @@ export const Browser: FunctionComponent<BrowserProps> = (
const chrLocation = location ? getChrLocationFromStr(location) : null;
if (props.browserActivated && genomeId && chrLocation) {
dispatchBrowserLocation(genomeId, chrLocation);
props.changeBrowserLocation({ genomeId, chrLocation, ensObjectId: null });
}
}, [props.browserActivated]);
......@@ -243,9 +258,7 @@ export const Browser: FunctionComponent<BrowserProps> = (
return launchbarExpanded ? styles.shorter : styles.taller;
};
const browserBar = (
<BrowserBar dispatchBrowserLocation={dispatchBrowserLocation} />
);
const browserBar = <BrowserBar />;
const shouldShowNavBar =
props.browserActivated && props.browserNavOpened && !isDrawerOpened;
......@@ -335,7 +348,8 @@ const mapDispatchToProps: DispatchProps = {
fetchGenomeData,
replace,
toggleDrawer,
setDataFromUrlAndSave
setDataFromUrlAndSave,
restoreBrowserTrackStates
};
export default withRouter(
......
......@@ -31,7 +31,6 @@ jest.mock('../track-panel/track-panel-tabs/TrackPanelTabs', () => () => (
));
describe('<BrowserBar />', () => {
const dispatchBrowserLocation: any = jest.fn();
const selectTrackPanelTab: any = jest.fn();
const toggleBrowserNav: any = jest.fn();
const toggleDrawer: any = jest.fn();
......@@ -51,7 +50,6 @@ describe('<BrowserBar />', () => {
selectedTrackPanelTab: TrackSet.GENOMIC,
trackPanelModalOpened: false,
trackPanelOpened: false,
dispatchBrowserLocation,
selectTrackPanelTab,
toggleBrowserNav,
toggleDrawer,
......
......@@ -48,7 +48,7 @@ import { BreakpointWidth } from 'src/global/globalConfig';
import styles from './BrowserBar.scss';
type StateProps = {
export type BrowserBarProps = {
activeGenomeId: string | null;
breakpointWidth: BreakpointWidth;
browserActivated: boolean;
......@@ -63,9 +63,6 @@ type StateProps = {
ensObject: EnsObject | null;
selectedTrackPanelTab: TrackSet;
isFocusObjectInDefaultPosition: boolean;
};
type DispatchProps = {
closeDrawer: () => void;
selectTrackPanelTab: (selectedTrackPanelTab: TrackSet) => void;
toggleBrowserNav: () => void;
......@@ -74,12 +71,6 @@ type DispatchProps = {
changeFocusObject: (objectId: string) => void;
};
type OwnProps = {
dispatchBrowserLocation: (genomeId: string, chrLocation: ChrLocation) => void;
};
export type BrowserBarProps = StateProps & DispatchProps & OwnProps;
type BrowserInfoProps = {
ensObject: EnsObject;
};
......@@ -167,7 +158,6 @@ export const BrowserBar: FunctionComponent<BrowserBarProps> = (
<BrowserGenomeSelector
activeGenomeId={props.activeGenomeId}
browserActivated={props.browserActivated}
dispatchBrowserLocation={props.dispatchBrowserLocation}
chrLocation={props.actualChrLocation}
isDrawerOpened={isDrawerOpened}
genomeSelectorActive={props.genomeSelectorActive}
......@@ -246,7 +236,7 @@ export const BrowserNavigatorButton = (props: BrowserNavigatorButtonProps) => (
</dd>
);
const mapStateToProps = (state: RootState): StateProps => ({
const mapStateToProps = (state: RootState) => ({
activeGenomeId: getBrowserActiveGenomeId(state),
breakpointWidth: getBreakpointWidth(state),
browserActivated: getBrowserActivated(state),
......@@ -263,7 +253,7 @@ const mapStateToProps = (state: RootState): StateProps => ({
isFocusObjectInDefaultPosition: isFocusObjectPositionDefault(state)
});
const mapDispatchToProps: DispatchProps = {
const mapDispatchToProps = {
closeDrawer,
selectTrackPanelTab,
toggleBrowserNav,
......
import React, {
FunctionComponent,
useState,
ChangeEvent,
FormEvent,
useEffect
} from 'react';
import React, { useState, ChangeEvent, FormEvent, useEffect } from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { ChrLocation } from '../browserState';
import { getChrLocationStr } from '../browserHelper';
import { getCommaSeparatedNumber } from 'src/shared/helpers/numberFormatter';
import { changeBrowserLocation } from 'src/content/app/browser/browserActions';
import applyIcon from 'static/img/shared/apply.svg';
import clearIcon from 'static/img/shared/clear.svg';
......@@ -20,15 +16,17 @@ type BrowserGenomeSelectorProps = {
activeGenomeId: string | null;
browserActivated: boolean;
chrLocation: ChrLocation;
dispatchBrowserLocation: (genomeId: string, chrLocation: ChrLocation) => void;
changeBrowserLocation: (locationData: {
genomeId: string;
ensObjectId: string | null;
chrLocation: ChrLocation;
}) => void;
isDrawerOpened: boolean;
genomeSelectorActive: boolean;
toggleGenomeSelector: (genomeSelectorActive: boolean) => void;
};
const BrowserGenomeSelector: FunctionComponent<BrowserGenomeSelectorProps> = (
props: BrowserGenomeSelectorProps
) => {
const BrowserGenomeSelector = (props: BrowserGenomeSelectorProps) => {
const { activeGenomeId, chrLocation, isDrawerOpened } = props;
const chrLocationStr = getChrLocationStr(chrLocation);
......@@ -72,7 +70,11 @@ const BrowserGenomeSelector: FunctionComponent<BrowserGenomeSelectorProps> = (
) {
closeForm();
props.dispatchBrowserLocation(activeGenomeId, [chrLocationInput, 0, 0]);
props.changeBrowserLocation({
genomeId: activeGenomeId,
ensObjectId: null,
chrLocation: [chrLocationInput, 0, 0]
});
} else {
const [chrCodeInput, chrRegionInput] = chrLocationInput.split(':');
const [chrStartInput, chrEndInput] = chrRegionInput.split('-');
......@@ -86,7 +88,11 @@ const BrowserGenomeSelector: FunctionComponent<BrowserGenomeSelectorProps> = (
closeForm();
props.dispatchBrowserLocation(activeGenomeId, currChrLocation);
props.changeBrowserLocation({
genomeId: activeGenomeId,
chrLocation: currChrLocation,
ensObjectId: null
});
}
}
};
......@@ -129,4 +135,11 @@ const BrowserGenomeSelector: FunctionComponent<BrowserGenomeSelectorProps> = (
) : null;
};
export default BrowserGenomeSelector;
const mapDispatchToProps = {
changeBrowserLocation
};
export default connect(
null,
mapDispatchToProps
)(BrowserGenomeSelector);
......@@ -35,7 +35,7 @@ import { ChrLocation } from '../browserState';
import { CircleLoader } from 'src/shared/components/loader/Loader';
import { RootState } from 'src/store';
import { TrackStates } from '../track-panel/trackPanelConfig';
import { BrowserTrackStates } from '../track-panel/trackPanelConfig';
import { BROWSER_CONTAINER_ID } from '../browser-constants';
type StateProps = {
......@@ -57,7 +57,7 @@ type DispatchProps = {
};
type OwnProps = {
trackStates: TrackStates;
trackStates: BrowserTrackStates;
};
type BrowserImageProps = StateProps & DispatchProps & OwnProps;
......
......@@ -66,8 +66,12 @@ describe('BrowserStorageService', () => {
const toggledTrack = {
homo_sapiens38: {
'Genes & transcripts': {
'gene-pc-fwd': Status.INACTIVE as TrackActivityStatus
objectTracks: {
homo_sapiens38_brca2: {
'Genes & transcripts': {
'gene-pc-fwd': Status.INACTIVE as TrackActivityStatus
}
}
}
}
};
......
import storageService, {
StorageServiceInterface
} from 'src/services/storage-service';
import { TrackStates } from './track-panel/trackPanelConfig';
import { BrowserTrackStates } from './track-panel/trackPanelConfig';
import { ChrLocations } from './browserState';
import {
TrackPanelState,
......@@ -54,11 +54,11 @@ export class BrowserStorageService {
this.storageService.update(StorageKeys.CHR_LOCATION, chrLocation);
}
public getTrackStates(): TrackStates {
public getTrackStates(): BrowserTrackStates {
return this.storageService.get(StorageKeys.TRACK_STATES) || {};
}
public saveTrackStates(trackStates: TrackStates) {
public saveTrackStates(trackStates: BrowserTrackStates) {
this.storageService.save(StorageKeys.TRACK_STATES, trackStates);
}
......
......@@ -3,6 +3,7 @@ import { Dispatch, ActionCreator, Action } from 'redux';
import { replace } from 'connected-react-router';
import { ThunkAction } from 'redux-thunk';
import isEqual from 'lodash/isEqual';
import get from 'lodash/get';
import config from 'config';
import * as urlFor from 'src/shared/helpers/urlHelper';
......@@ -16,11 +17,19 @@ import { fetchEnsObject } from 'src/ens-object/ensObjectActions';
import {
getBrowserActiveGenomeId,
getBrowserActiveEnsObjectId,
getBrowserActiveEnsObjectIds,
getBrowserTrackStates,
getChrLocation,
getBrowserMessageCount
getBrowserMessageCount,
getBrowserActiveEnsObjectIds
} from './browserSelectors';
import { updatePreviouslyViewedObjectsAndSave } from 'src/content/app/browser/track-panel/trackPanelActions';
import { RootState } from 'src/store';
import {
BrowserTrackStates,
TrackStates
} from './track-panel/trackPanelConfig';
import { BROWSER_CONTAINER_ID } from './browser-constants';
import {
......@@ -29,9 +38,8 @@ import {
CogList,
ChrLocations
} from './browserState';
import { TrackStates } from './track-panel/trackPanelConfig';
import { RootState } from 'src/store';
import { TrackActivityStatus } from 'src/content/app/browser/track-panel/trackPanelConfig';
import { Status } from 'src/shared/types/status';
export type UpdateTrackStatesPayload = {
genomeId: string;
......@@ -80,10 +88,12 @@ export const setDataFromUrlAndSave: ActionCreator<
browserStorageService.saveActiveGenomeId(payload.activeGenomeId);
chrLocation &&
browserStorageService.updateChrLocation({ [activeGenomeId]: chrLocation });
activeEnsObjectId &&
if (activeEnsObjectId) {
browserStorageService.updateActiveEnsObjectIds({
[activeGenomeId]: activeEnsObjectId
});
}
};
export const fetchDataForLastVisitedObjects: ActionCreator<
......@@ -109,15 +119,15 @@ export const updateBrowserActiveEnsObjectIdsAndSave: ActionCreator<
return;
}
const currentActiveEnsObjectIds = getBrowserActiveEnsObjectIds(state);
const updatedActiveEnsObjectId = {
const updatedActiveEnsObjectIds = {
...currentActiveEnsObjectIds,
[activeGenomeId]: activeEnsObjectId
};
dispatch(updateBrowserActiveEnsObjectIds(updatedActiveEnsObjectId));
dispatch(updateBrowserActiveEnsObjectIds(updatedActiveEnsObjectIds));
dispatch(fetchEnsObject(activeEnsObjectId));
browserStorageService.updateActiveEnsObjectIds(updatedActiveEnsObjectId);
browserStorageService.updateActiveEnsObjectIds(updatedActiveEnsObjectIds);
};
};
......@@ -127,27 +137,53 @@ export const updateDefaultPositionFlag = createStandardAction(
export const updateTrackStates = createStandardAction(
'browser/update-tracks-state'
)<TrackStates>();
)<BrowserTrackStates>();
export const updateTrackStatesAndSave: ActionCreator<
ThunkAction<void, any, null, Action<string>>
> = (payload: UpdateTrackStatesPayload) => (
dispatch: Dispatch,
getState: () => RootState
) => {
const stateFragment = {
[payload.genomeId]: {
[payload.categoryName]: {
[payload.trackId]: payload.status
}
}
};
dispatch(updateTrackStates(stateFragment));
> = (payload: BrowserTrackStates) => (dispatch, getState: () => RootState) => {
dispatch(updateTrackStates(payload));
const trackStates = getBrowserTrackStates(getState());
browserStorageService.saveTrackStates(trackStates);
};
export const restoreBrowserTrackStates: ActionCreator<
ThunkAction<void, any, null, Action<string>>
> = () => (dispatch, getState: () => RootState) => {
const state = getState();
const activeGenomeId = getBrowserActiveGenomeId(state);
const activeEnsObjectId = getBrowserActiveEnsObjectId(state);
if (!activeGenomeId || !activeEnsObjectId) {
return;
}
const trackStatesFromStorage = browserStorageService.getTrackStates();
const mergedTrackStates = {
...get(
trackStatesFromStorage,
`${activeGenomeId}.objectTracks.${activeEnsObjectId}`
),
...get(trackStatesFromStorage, `${activeGenomeId}.commonTracks`)
} as TrackStates;
const tracksToTurnOff: string[] = [];
const tracksToTurnOn: string[] = [];
Object.values(mergedTrackStates).forEach((trackStates) => {
Object.keys(trackStates).forEach((trackId) => {
trackStates[trackId] === Status.ACTIVE
? tracksToTurnOn.push(trackId)
: tracksToTurnOff.push(trackId);
});
});
browserMessagingService.send('bpane', {
off: tracksToTurnOff,
on: tracksToTurnOn
});
};
export const toggleBrowserNav = createStandardAction(
'browser/toggle-browser-navigation'
)();
......@@ -216,11 +252,18 @@ export const updateMessageCounter = createStandardAction(
export const changeBrowserLocation: ActionCreator<
ThunkAction<any, any, null, Action<string>>
> = (genomeId: string, chrLocation: ChrLocation) => {
> = (locationData: {
genomeId: string;
ensObjectId: string | null;
chrLocation: ChrLocation;
}) => {
return (dispatch, getState: () => RootState) => {
const state = getState();
const [chrCode, startBp, endBp] = chrLocation;
const activeEnsObjectId = getBrowserActiveEnsObjectId(state);
const [chrCode, startBp, endBp] = locationData.chrLocation;
const activeEnsObjectId =
locationData.ensObjectId || getBrowserActiveEnsObjectId(state);
const messageCount = getBrowserMessageCount(state);
const focusInstruction = activeEnsObjectId
? {
......@@ -229,7 +272,7 @@ export const changeBrowserLocation: ActionCreator<
: {};
browserMessagingService.send('bpane', {
stick: `${genomeId}:${chrCode}`,
stick: `${locationData.genomeId}:${chrCode}`,
goto: `${startBp}-${endBp}`,
'message-counter': messageCount,
...focusInstruction
......@@ -244,6 +287,8 @@ export const changeFocusObject: ActionCreator<
const state = getState();
const messageCount = getBrowserMessageCount(state);
dispatch(updatePreviouslyViewedObjectsAndSave());
browserMessagingService.send('bpane', {
focus: objectId,
'message-counter': messageCount
......
......@@ -37,6 +37,14 @@ export const getBrowserActiveEnsObject = (state: RootState) => {
export const getBrowserTrackStates = (state: RootState) =>
state.browser.browserEntity.trackStates;
export const getBrowserActiveGenomeTrackStates = (state: RootState) => {
const activeGenomeId = getBrowserActiveGenomeId(state);
return activeGenomeId
? state.browser.browserEntity.trackStates[activeGenomeId]
: null;
};
export const getBrowserQueryParams = (
state: RootState
): { [key: string]: string } => getQueryParamsMap(state.router.location.search);
......
import browserStorageService from './browser-storage-service';
import { TrackStates } from './track-panel/trackPanelConfig';
import { BrowserTrackStates } from './track-panel/trackPanelConfig';
const activeGenomeId = browserStorageService.getActiveGenomeId();
const activeEnsObjectIds = browserStorageService.getActiveEnsObjectIds();
......@@ -36,7 +36,7 @@ export const defaultBrowserState: BrowserState = {
export type BrowserEntityState = Readonly<{
activeGenomeId: string | null;
activeEnsObjectIds: { [genomeId: string]: string };
trackStates: TrackStates;
trackStates: BrowserTrackStates;
messageCounter: number;
}>;
......
......@@ -12,6 +12,7 @@ import ProteinCodingGenes from './drawer-views/ProteinCodingGenes';
import OtherGenes from './drawer-views/OtherGenes';
import DrawerContigs from './drawer-views/DrawerContigs';
import DrawerGC from './drawer-views/DrawerGC';
import DrawerBookmarks from './drawer-views/DrawerBookmarks';
import closeIcon from 'static/img/shared/close.svg';
......@@ -53,6 +54,8 @@ const Drawer = (props: DrawerProps) => {
return <DrawerGC />;
case 'snps-and-indels':
return <SnpIndels />;
case 'bookmarks':
return <DrawerBookmarks />;
}
};
......
@import 'src/styles/common';
.drawerTitle {
margin: 20px 0 0 30px;
}
.contentWrapper {
height: 80%;
}
.linksWrapper {
margin: 20px 0 0 40px;
height: 100%;
width: calc(100% - 110px);
display: flex;
flex-direction: column;
flex-wrap: wrap;
overflow-x: auto;
.linkHolder {
margin: 5px 0 0 15px;
width: 20%;
padding-right: 10px;
text-overflow: ellipsis;
overflow: hidden;
}