Unverified Commit ae08cae1 authored by Jyothish's avatar Jyothish Committed by GitHub
Browse files

Maintain track label state on browser refresh (#747)

parent e1bad885
Pipeline #286578 passed with stages
in 5 minutes and 16 seconds
......@@ -19,7 +19,6 @@ import configureMockStore from 'redux-mock-store';
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import set from 'lodash/fp/set';
import { IncomingActionType } from '@ensembl/ensembl-genome-browser';
import MockGenomeBrowser from 'tests/mocks/mockGenomeBrowser';
......@@ -27,7 +26,8 @@ import MockGenomeBrowser from 'tests/mocks/mockGenomeBrowser';
import { BrowserCogList } from './BrowserCogList';
import { createMockBrowserState } from 'tests/fixtures/browser';
import { updateCogTrackList } from 'src/content/app/genome-browser/state/track-config/trackConfigSlice';
import { updateCogList } from 'src/content/app/genome-browser/state/track-config/trackConfigSlice';
const mockGenomeBrowser = jest.fn(() => new MockGenomeBrowser() as any);
......@@ -40,17 +40,7 @@ jest.mock(
jest.mock('./BrowserCog', () => () => <div id="browserCog" />);
let mockState = createMockBrowserState();
mockState = set('browser.trackConfig.trackConfigNames', {}, mockState);
mockState = set('browser.trackConfig.trackConfigLabel', {}, mockState);
mockState = set(
'browser.trackConfig.browserCogTrackList',
{
'track:gc': 100
},
mockState
);
const mockState = createMockBrowserState();
const mockStore = configureMockStore([thunk]);
......@@ -83,7 +73,7 @@ describe('<BrowserCogList />', () => {
expect(container.querySelector('#browserCog')).toBeFalsy();
});
it('calls updateCogTrackList when genome browser sends track summary updates', () => {
it('calls updateCogList when genome browser sends track summary updates', () => {
mockGenomeBrowser.mockReturnValue(new MockGenomeBrowser());
renderComponent();
......@@ -92,11 +82,11 @@ describe('<BrowserCogList />', () => {
type: IncomingActionType.TRACK_SUMMARY,
payload: [
{
'switch-id': 'track-1',
'switch-id': 'gene-focus',
offset: 100
},
{
'switch-id': 'track-2',
'switch-id': 'contig',
offset: 200
}
]
......@@ -104,13 +94,13 @@ describe('<BrowserCogList />', () => {
const dispatchedActions = store.getActions();
const updateCogTrackListAction = dispatchedActions.find(
(action) => action.type === updateCogTrackList.type
const updateCogListAction = dispatchedActions.find(
(action) => action.type === updateCogList.type
);
expect(updateCogTrackListAction.payload).toEqual({
'track-1': 100,
'track-2': 200
expect(updateCogListAction.payload).toEqual({
'gene-focus': 100,
contig: 200
});
});
});
......
......@@ -15,41 +15,64 @@
*/
import React, { useEffect, memo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
UpdateTrackSummaryAction,
IncomingActionType,
TrackSummaryList,
TrackSummary
type UpdateTrackSummaryAction,
type TrackSummaryList,
type TrackSummary
} from '@ensembl/ensembl-genome-browser';
import useGenomeBrowser from 'src/content/app/genome-browser/hooks/useGenomeBrowser';
import BrowserCog from './BrowserCog';
import { useAppDispatch, useAppSelector } from 'src/store';
import useBrowserCogList from './useBrowserCogList';
import {
getBrowserCogList,
getBrowserCogTrackList,
getBrowserSelectedCog
} from 'src/content/app/genome-browser/state/track-config/trackConfigSelectors';
import {
CogList,
updateCogTrackList,
type CogList,
updateCogList,
updateSelectedCog
} from 'src/content/app/genome-browser/state/track-config/trackConfigSlice';
import {
getBrowserActiveFocusObjectId,
getBrowserActiveGenomeId
} from 'src/content/app/genome-browser/state/browser-general/browserGeneralSelectors';
import { TrackId } from 'src/content/app/genome-browser/components/track-panel/trackPanelConfig';
import styles from './BrowserCogList.scss';
export const BrowserCogList = () => {
const browserCogList = useSelector(getBrowserCogList);
const browserCogTrackList = useSelector(getBrowserCogTrackList);
const selectedCog = useSelector(getBrowserSelectedCog);
const dispatch = useDispatch();
const browserCogList = useAppSelector(getBrowserCogList);
const selectedCog = useAppSelector(getBrowserSelectedCog);
const genomeId = useAppSelector(getBrowserActiveGenomeId) as string;
const objectId = useAppSelector(getBrowserActiveFocusObjectId);
const dispatch = useAppDispatch();
const { genomeBrowser } = useGenomeBrowser();
useBrowserCogList();
useEffect(() => {
const subscription = genomeBrowser?.subscribe(
IncomingActionType.TRACK_SUMMARY,
(action: UpdateTrackSummaryAction) => {
updateTrackSummary(action.payload);
}
);
return () => subscription?.unsubscribe();
}, [genomeBrowser, genomeId, objectId]);
const updateTrackSummary = (trackSummaryList: TrackSummaryList) => {
if (!objectId) {
return;
}
const cogList: CogList = {};
trackSummaryList.forEach((trackSummary: TrackSummary) => {
......@@ -58,49 +81,46 @@ export const BrowserCogList = () => {
trackSummary['switch-id'] &&
!cogList[trackSummary['switch-id']]
) {
cogList[trackSummary['switch-id']] = Number(trackSummary.offset);
const trackId =
trackSummary['switch-id'] === 'focus'
? TrackId.GENE
: trackSummary['switch-id'];
cogList[trackId] = Number(trackSummary.offset);
}
});
if (cogList) {
dispatch(updateCogTrackList(cogList));
}
dispatch(updateCogList(cogList));
};
useEffect(() => {
const subscription = genomeBrowser?.subscribe(
IncomingActionType.TRACK_SUMMARY,
(action: UpdateTrackSummaryAction) => updateTrackSummary(action.payload)
);
return () => subscription?.unsubscribe();
}, [genomeBrowser]);
const cogs = Object.entries(browserCogTrackList).map(([name, pos]) => {
const posStyle = { top: pos + 'px' };
return (
<div key={name} className={styles.browserCogOuter} style={posStyle}>
<BrowserCog
cogActivated={selectedCog === name}
trackId={name}
updateSelectedCog={(trackId: string | null) =>
dispatch(updateSelectedCog(trackId))
}
/>
</div>
);
});
// make sure to close the floating track config panel if the user switches to a different species
dispatch(updateSelectedCog(null));
}, [genomeId]);
const transformStyle = {
transform: 'translate(0,' + browserCogList + 'px)'
const handleCogSelect = (trackId: string | null) => {
dispatch(updateSelectedCog(trackId));
};
const cogs =
browserCogList &&
Object.entries(browserCogList).map(([name, pos]) => {
const posStyle = { top: `${pos}px` };
return (
<div key={name} className={styles.browserCogOuter} style={posStyle}>
<BrowserCog
cogActivated={selectedCog === name}
trackId={name}
updateSelectedCog={handleCogSelect}
/>
</div>
);
});
return genomeBrowser ? (
<div className={styles.browserTrackConfigOuter}>
<div className={styles.browserCogListOuter}>
<div className={styles.browserCogListInner} style={transformStyle}>
{cogs}
</div>
<div className={styles.browserCogListInner}>{cogs}</div>
</div>
</div>
) : null;
......
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from 'src/store';
import browserTrackConfigStorageService from 'src/content/app/genome-browser/components/browser-track-config/services/browserTrackConfigStorageService';
import { getGenomeTrackCategories } from 'src/shared/state/genome/genomeSelectors';
import {
getBrowserActiveGenomeId,
getBrowserActiveFocusObject
} from 'src/content/app/genome-browser/state/browser-general/browserGeneralSelectors';
import {
setInitialTrackConfigsForGenome,
getDefaultGeneTrackConfig,
getDefaultRegularTrackConfig,
getTrackType,
TrackType,
type TrackConfigs,
type TrackConfigsForGenome
} from 'src/content/app/genome-browser/state/track-config/trackConfigSlice';
import { TrackId } from 'src/content/app/genome-browser/components/track-panel/trackPanelConfig';
import type { GenomeTrackCategory } from 'src/shared/state/genome/genomeTypes';
import type { FocusObject } from 'src/shared/types/focus-object/focusObjectTypes';
const useBrowserCogList = () => {
const genomeId = useAppSelector(getBrowserActiveGenomeId) as string;
const trackCategories = useAppSelector(getGenomeTrackCategories);
const focusObject = useAppSelector(getBrowserActiveFocusObject); // should we think about what to do if there is no focus object
const dispatch = useAppDispatch();
const trackCategoriesForGenome = trackCategories[genomeId];
// TODO: think about what should happen when we switch types of focus objects
// eg gene -> variant -> region
useEffect(() => {
if (!trackCategoriesForGenome) {
return;
}
const defaultTracksForGenome = prepareTrackConfigs({
trackCategories: trackCategoriesForGenome,
focusObject
});
const savedTrackConfigsForGenome =
getPersistentTrackConfigsForGenome(genomeId);
const trackConfigs = {
tracks: defaultTracksForGenome,
...savedTrackConfigsForGenome
};
dispatch(setInitialTrackConfigsForGenome({ genomeId, trackConfigs }));
}, [trackCategories, focusObject, genomeId]);
};
export const getPersistentTrackConfigsForGenome = (
genomeId: string
): Partial<TrackConfigsForGenome> => {
const trackConfigs = browserTrackConfigStorageService.getTrackConfigs();
return trackConfigs[genomeId] ?? {};
};
export const prepareTrackConfigs = ({
trackCategories,
focusObject
}: {
trackCategories: GenomeTrackCategory[];
focusObject: FocusObject | null;
}) => {
const defaultTrackConfigs: TrackConfigs = {};
if (focusObject?.type === TrackType.GENE) {
defaultTrackConfigs[TrackId.GENE] = getDefaultGeneTrackConfig();
}
trackCategories.forEach((category) => {
category.track_list.forEach((track) => {
const trackId = track.track_id.replace('track:', '');
const trackType = getTrackType(trackId);
if (trackType === TrackType.GENE) {
defaultTrackConfigs[trackId] = getDefaultGeneTrackConfig();
} else {
defaultTrackConfigs[trackId] = getDefaultRegularTrackConfig();
}
});
});
return defaultTrackConfigs;
};
export default useBrowserCogList;
......@@ -15,32 +15,77 @@
*/
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { createMockBrowserState } from 'tests/fixtures/browser';
import MockGenomeBrowser from 'tests/mocks/mockGenomeBrowser';
import * as trackConfigActions from 'src/content/app/genome-browser/state/track-config/trackConfigSlice';
import * as trackConfigSlice from 'src/content/app/genome-browser/state/track-config/trackConfigSlice';
import * as browserGeneralSlice from 'src/content/app/genome-browser/state/browser-general/browserGeneralSlice';
import { BrowserTrackConfig } from './BrowserTrackConfig';
import BrowserTrackConfig from './BrowserTrackConfig';
const mockState = createMockBrowserState();
const mockStore = configureMockStore([thunk]);
const genomeId = 'fake_genome_id_1';
const selectedTrackId = 'gene-focus';
const renderComponent = () => {
const rootReducer = combineReducers({
browser: combineReducers({
browserGeneral: browserGeneralSlice.default,
trackConfig: trackConfigSlice.default
})
});
let store: ReturnType<typeof mockStore>;
const fragment = {
tracks: {
[selectedTrackId]: {
showSeveralTranscripts: false,
showTranscriptIds: false,
showTrackName: false,
showFeatureLabel: false,
trackType: trackConfigSlice.TrackType.GENE
}
}
};
const initialState = {
browser: {
browserGeneral: Object.assign(
{},
browserGeneralSlice.defaultBrowserGeneralState,
{ activeGenomeId: genomeId }
),
trackConfig: {
browserTrackCogs: {
cogList: {},
selectedCog: selectedTrackId
},
configs: {
[genomeId]: Object.assign(
{},
trackConfigSlice.defaultTrackConfigsForGenome,
fragment
)
}
}
}
};
const store = configureStore({
reducer: rootReducer,
preloadedState: initialState
});
const renderComponent = () => {
store = mockStore(mockState);
return render(
const renderResult = render(
<Provider store={store}>
<BrowserTrackConfig />
</Provider>
);
return {
...renderResult,
store
};
};
const mockGenomeBrowser = new MockGenomeBrowser();
......@@ -48,7 +93,9 @@ const mockGenomeBrowser = new MockGenomeBrowser();
jest.mock(
'src/content/app/genome-browser/hooks/useGenomeBrowser',
() => () => ({
genomeBrowser: mockGenomeBrowser
genomeBrowser: mockGenomeBrowser,
toggleTrackName: jest.fn(),
toggleTrackLabel: jest.fn()
})
);
......@@ -58,55 +105,70 @@ describe('<BrowserTrackConfig />', () => {
});
describe('behaviour', () => {
it('can update all tracks', async () => {
const { container } = renderComponent();
it('updates state when clicking All Tracks option', async () => {
const { container, store } = renderComponent();
const allTracksLabel = [...container.querySelectorAll('label')].find(
(el) => el.textContent === 'All tracks'
);
const allTracksInputElement = allTracksLabel?.querySelector(
'input'
) as HTMLElement;
jest.spyOn(trackConfigActions, 'updateApplyToAll');
await userEvent.click(allTracksInputElement);
jest.spyOn(trackConfigSlice, 'updateApplyToAll');
expect(trackConfigActions.updateApplyToAll).toHaveBeenCalledTimes(1);
await userEvent.click(allTracksInputElement);
const updatedState = store.getState();
expect(trackConfigSlice.updateApplyToAll).toHaveBeenCalledWith({
genomeId,
isSelected: true
});
expect(
updatedState.browser.trackConfig.configs[genomeId].shouldApplyToAll
).toBeTruthy();
});
it('toggles track name', async () => {
const { container } = renderComponent();
const { container, store } = renderComponent();
const toggle = [...container.querySelectorAll('label')]
.find((element) => element.textContent === 'Track name')
?.parentElement?.querySelector('svg') as SVGElement;
jest.spyOn(trackConfigActions, 'updateTrackConfigNames');
jest.spyOn(trackConfigSlice, 'updateTrackName');
await userEvent.click(toggle);
expect(trackConfigActions.updateTrackConfigNames).toHaveBeenCalledTimes(
1
);
expect(trackConfigActions.updateTrackConfigNames).toHaveBeenCalledWith({
isTrackNameShown: false,
selectedCog: mockState.browser.trackConfig.selectedCog
const updatedState = store.getState();
expect(trackConfigSlice.updateTrackName).toHaveBeenCalledWith({
genomeId,
isTrackNameShown: true,
trackId: updatedState.browser.trackConfig.browserTrackCogs.selectedCog
});
expect(
updatedState.browser.trackConfig.configs[genomeId].tracks[
selectedTrackId
].showTrackName
).toBeTruthy();
});
it('toggles feature labels on the track', async () => {
const { container } = renderComponent();
const { container, store } = renderComponent();
const toggle = [...container.querySelectorAll('label')]
.find((element) => element.textContent === 'Feature labels')
?.parentElement?.querySelector('svg') as SVGElement;
jest.spyOn(trackConfigActions, 'updateTrackConfigLabel');
jest.spyOn(trackConfigSlice, 'updateFeatureLabel');
await userEvent.click(toggle);
expect(trackConfigActions.updateTrackConfigLabel).toHaveBeenCalledTimes(
1
);
expect(trackConfigActions.updateTrackConfigLabel).toHaveBeenCalledWith({
isTrackLabelShown: false,
selectedCog: mockState.browser.trackConfig.selectedCog
const updatedState = store.getState();
expect(trackConfigSlice.updateFeatureLabel).toHaveBeenCalledWith({
genomeId,
isTrackLabelShown: true,
trackId: updatedState.browser.trackConfig.browserTrackCogs.selectedCog
});
const trackInfo =
updatedState.browser.trackConfig.configs[genomeId].tracks[
selectedTrackId
];
if (trackInfo.trackType === trackConfigSlice.TrackType.GENE) {
expect(trackInfo.showFeatureLabel).toBeTruthy();
}
});
});
});
......@@ -15,163 +15,42 @@
*/
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { OutgoingActionType } from '@ensembl/ensembl-genome-browser';
import RadioGroup, {
type RadioOptions
} from 'src/shared/components/radio-group/RadioGroup';
import GeneTrackConfig from './track-config-views/GeneTrackConfig';
import RegularTrackConfig from './track-config-views/RegularTrackConfig';
import { useAppSelector } from 'src/store';
import {
updateTrackConfigNames,
updateTrackConfigLabel,
updateApplyToAll,
updateApplyToAllTrackLabels,
updateApplyToAllTrackNames
getTrackType,
TrackType
} from 'src/content/app/genome-browser/state/track-config/trackConfigSlice';
import {
getTrackConfigNames,