Unverified Commit 6d2a2f93 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Support view query parameter in EntityViewer url (#309)

parent bf644d11
Pipeline #83162 passed with stages
in 9 minutes and 18 seconds
......@@ -14,18 +14,25 @@
* limitations under the License.
*/
import React, { useState } from 'react';
import { connect } from 'react-redux';
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { replace } from 'connected-react-router';
import { useQuery } from '@apollo/react-hooks';
import { gql } from 'apollo-boost';
import { useParams } from 'react-router-dom';
import { useParams, useLocation } from 'react-router-dom';
import usePrevious from 'src/shared/hooks/usePrevious';
import {
getSelectedGeneViewTabs,
getGeneViewName
} from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewSelectors';
import { setGeneViewName } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewActions';
import { GeneViewTabName } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewState';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { buildFocusIdForUrl } from 'src/shared/state/ens-object/ensObjectHelpers';
import { parseFocusIdFromUrl } from 'src/shared/state/ens-object/ensObjectHelpers';
import { getEntityViewerActiveEnsObject } from 'src/content/app/entity-viewer/state/general/entityViewerGeneralSelectors';
import { getEntityViewerActiveGeneTab } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewSelectors';
import GeneOverviewImage from './components/gene-overview-image/GeneOverviewImage';
import DefaultTranscriptslist from './components/default-transcripts-list/DefaultTranscriptsList';
import GeneViewTabs from './components/gene-view-tabs/GeneViewTabs';
......@@ -34,21 +41,13 @@ import GeneRelationships from 'src/content/app/entity-viewer/gene-view/component
import ViewInApp from 'src/shared/components/view-in-app/ViewInApp';
import { CircleLoader } from 'src/shared/components/loader/Loader';
import { GeneViewTabName } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewState.ts';
import { Gene } from 'src/content/app/entity-viewer/types/gene';
import { TicksAndScale } from 'src/content/app/entity-viewer/gene-view/components/base-pairs-ruler/BasePairsRuler';
import { RootState } from 'src/store';
import styles from './GeneView.scss';
type GeneViewProps = {
geneId: string | null;
selectedGeneTabName: GeneViewTabName;
};
type GeneViewWithDataProps = {
gene: Gene;
selectedGeneTabName: GeneViewTabName;
};
const QUERY = gql`
......@@ -99,7 +98,7 @@ const QUERY = gql`
}
`;
const GeneView = (props: GeneViewProps) => {
const GeneView = () => {
const params: { [key: string]: string } = useParams();
const { entityId } = params;
const { objectId: geneId } = parseFocusIdFromUrl(entityId);
......@@ -119,12 +118,7 @@ const GeneView = (props: GeneViewProps) => {
return null;
}
return (
<GeneViewWithData
gene={data.gene}
selectedGeneTabName={props.selectedGeneTabName}
/>
);
return <GeneViewWithData gene={data.gene} />;
};
const GeneViewWithData = (props: GeneViewWithDataProps) => {
......@@ -133,9 +127,9 @@ const GeneViewWithData = (props: GeneViewWithDataProps) => {
setBasePairsRulerTicks
] = useState<TicksAndScale | null>(null);
const params: { [key: string]: string } = useParams();
const { genomeId, entityId } = params;
const gbUrl = urlFor.browser({ genomeId, focus: entityId });
const { genomeId, geneId, selectedTabs } = useGeneViewRouting();
const focusId = buildFocusIdForUrl({ type: 'gene', objectId: geneId });
const gbUrl = urlFor.browser({ genomeId, focus: focusId });
return (
<div className={styles.geneView}>
......@@ -153,7 +147,7 @@ const GeneViewWithData = (props: GeneViewWithDataProps) => {
<GeneViewTabs />
</div>
<div className={styles.geneViewTabContent}>
{props.selectedGeneTabName === GeneViewTabName.TRANSCRIPTS &&
{selectedTabs.primaryTab === GeneViewTabName.TRANSCRIPTS &&
basePairsRulerTicks && (
<DefaultTranscriptslist
gene={props.gene}
......@@ -161,11 +155,11 @@ const GeneViewWithData = (props: GeneViewWithDataProps) => {
/>
)}
{props.selectedGeneTabName === GeneViewTabName.GENE_FUNCTION && (
{selectedTabs.primaryTab === GeneViewTabName.GENE_FUNCTION && (
<GeneFunction gene={props.gene} />
)}
{props.selectedGeneTabName === GeneViewTabName.GENE_RELATIONSHIPS && (
{selectedTabs.primaryTab === GeneViewTabName.GENE_RELATIONSHIPS && (
<GeneRelationships />
)}
</div>
......@@ -173,10 +167,38 @@ const GeneViewWithData = (props: GeneViewWithDataProps) => {
);
};
const mapStateToProps = (state: RootState) => ({
// FIXME: this will have to be superseded with a proper way we get ids
geneId: getEntityViewerActiveEnsObject(state)?.stable_id || null,
selectedGeneTabName: getEntityViewerActiveGeneTab(state)
});
const useGeneViewRouting = () => {
const dispatch = useDispatch();
const params: { [key: string]: string } = useParams();
const { genomeId, entityId } = params;
const { objectId: geneId } = parseFocusIdFromUrl(entityId);
const { search } = useLocation();
// TODO: discuss – is using URLSearchParams better than using the querystring package?
const view = new URLSearchParams(search).get('view');
const viewInRedux = useSelector(getGeneViewName);
const previousGenomeId = usePrevious(genomeId); // genomeId during previous render
const selectedTabs = useSelector(getSelectedGeneViewTabs);
useEffect(() => {
if (previousGenomeId !== genomeId) {
if (viewInRedux && viewInRedux !== view) {
const url = urlFor.entityViewer({
genomeId,
entityId,
view: viewInRedux
});
dispatch(replace(url));
}
} else if (viewInRedux !== view) {
dispatch(setGeneViewName(view));
}
}, [view, viewInRedux, genomeId, previousGenomeId]);
return {
genomeId,
geneId,
selectedTabs
};
};
export default connect(mapStateToProps)(GeneView);
export default GeneView;
......@@ -16,10 +16,14 @@
import React from 'react';
import { connect } from 'react-redux';
import { useParams } from 'react-router-dom';
import { push, Push } from 'connected-react-router';
import { isEntityViewerSidebarOpen } from 'src/content/app/entity-viewer/state/sidebar/entityViewerSidebarSelectors';
import { getEntityViewerActiveGeneFunction } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewSelectors';
import { setActiveGeneFunctionTab } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewActions';
import { getSelectedGeneViewTabs } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewSelectors';
import { setGeneViewName } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewActions';
import * as urlFor from 'src/shared/helpers/urlHelper';
import Tabs, { Tab } from 'src/shared/components/tabs/Tabs';
import Panel from 'src/shared/components/panel/Panel';
......@@ -27,20 +31,22 @@ import ProteinsList from '../proteins-list/ProteinsList';
import { RootState } from 'src/store';
import { Gene } from 'src/content/app/entity-viewer/types/gene';
import { GeneFunctionTabName } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewState.ts';
import {
GeneViewTabMap,
GeneViewTabName,
GeneFunctionTabName
} from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewState.ts';
import styles from './GeneFunction.scss';
// TODO: the isDisabled flags are hardcoded here since we do not have any data available.
// We need to update this logic once we have the data available
const tabsData: Tab[] = [
{ title: GeneFunctionTabName.PROTEINS },
{ title: GeneFunctionTabName.VARIANTS, isDisabled: true },
{ title: GeneFunctionTabName.PHENOTYPES, isDisabled: true },
{ title: GeneFunctionTabName.GENE_EXPRESSION, isDisabled: true },
{ title: GeneFunctionTabName.GENE_ONTOLOGY, isDisabled: true },
{ title: GeneFunctionTabName.GENE_PATHWAYS, isDisabled: true }
];
const tabsData = [...GeneViewTabMap.values()]
.filter(({ primaryTab }) => primaryTab === GeneViewTabName.GENE_FUNCTION)
.map((item) => ({
title: item.secondaryTab,
isDisabled: false
})) as Tab[];
const tabClassNames = {
default: styles.defaultTabName,
......@@ -51,23 +57,41 @@ type Props = {
gene: Gene;
isNarrow: boolean;
selectedTabName: GeneFunctionTabName | null;
setActiveGeneFunctionTab: (tab: string) => void;
push: Push;
};
const GeneFunction = (props: Props) => {
const { genomeId, entityId } = useParams() as { [key: string]: string };
const {
gene: { transcripts }
} = props;
let { selectedTabName } = props;
const { selectedTabName } = props;
const changeTab = (tab: string) => {
const match = [...GeneViewTabMap.entries()].find(
([, { secondaryTab }]) => secondaryTab === tab
);
if (!match) {
return;
}
const [view] = match;
const url = urlFor.entityViewer({
genomeId,
entityId,
view
});
props.push(url);
};
// Check if we have at least one protein coding transcript
const proteinCodingTranscriptIndex = transcripts.findIndex(
// TODO: use a more reliable indicator than the biotype field
const isProteinCodingTranscript = transcripts.some(
(transcript) => transcript.biotype === 'protein_coding'
);
// Disable the Proteins tab if there are no transcripts data
// TODO: We need a better logic to disable tabs once we have the data available for other tabs
if (proteinCodingTranscriptIndex === -1) {
if (!isProteinCodingTranscript) {
const proteinTabIndex = tabsData.findIndex(
(tab) => tab.title === GeneFunctionTabName.PROTEINS
);
......@@ -75,28 +99,13 @@ const GeneFunction = (props: Props) => {
tabsData[proteinTabIndex].isDisabled = true;
}
// If the selectedTab is disabled or if there is no selectedtab, pick the first available tab
const selectedTabIndex = tabsData.findIndex(
(tab) => tab.title === selectedTabName
);
if (selectedTabIndex === -1 || tabsData[selectedTabIndex].isDisabled) {
const nextAvailableTab = tabsData.find((tab) => !tab.isDisabled);
selectedTabName = (nextAvailableTab?.title as GeneFunctionTabName) || null;
}
const TabWrapper = () => {
const onTabChange = (tab: string) => {
props.setActiveGeneFunctionTab(tab);
};
return (
<Tabs
tabs={tabsData}
selectedTab={selectedTabName}
classNames={tabClassNames}
onTabChange={onTabChange}
onTabChange={changeTab}
/>
);
};
......@@ -125,11 +134,13 @@ const GeneFunction = (props: Props) => {
const mapStateToProps = (state: RootState) => ({
isNarrow: isEntityViewerSidebarOpen(state),
selectedTabName: getEntityViewerActiveGeneFunction(state).selectedTabName
selectedTabName: getSelectedGeneViewTabs(state)
.secondaryTab as GeneFunctionTabName
});
const mapDispatchToProps = {
setActiveGeneFunctionTab
push,
setGeneViewName
};
export default connect(mapStateToProps, mapDispatchToProps)(GeneFunction);
......@@ -16,30 +16,34 @@
import React from 'react';
import { connect } from 'react-redux';
import { useParams } from 'react-router-dom';
import { push, Push } from 'connected-react-router';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { GeneRelationshipsTabName } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewState.ts';
import { isEntityViewerSidebarOpen } from 'src/content/app/entity-viewer/state/sidebar/entityViewerSidebarSelectors';
import { getEntityViewerActiveGeneRelationships } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewSelectors';
import { setActiveGeneRelationshipsTab } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewActions';
import { getSelectedGeneViewTabs } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewSelectors';
import Tabs, { Tab } from 'src/shared/components/tabs/Tabs';
import Panel from 'src/shared/components/panel/Panel';
import { RootState } from 'src/store';
import {
GeneViewTabMap,
GeneViewTabName,
GeneRelationshipsTabName
} from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewState.ts';
import styles from './GeneRelationships.scss';
// TODO: the isDisabled flags are hardcoded here since we do not have any data available.
// We need to update this logic once we have the data available
const tabsData: Tab[] = [
{ title: GeneRelationshipsTabName.ORTHOLOGUES, isDisabled: true },
{ title: GeneRelationshipsTabName.PARALOGUES, isDisabled: true },
{ title: GeneRelationshipsTabName.GENE_FAMILIES, isDisabled: true },
{ title: GeneRelationshipsTabName.GENE_CLUSTERS, isDisabled: true },
{ title: GeneRelationshipsTabName.GENE_PANELS, isDisabled: true },
{ title: GeneRelationshipsTabName.GENE_NEIGHBOUTHOOD, isDisabled: true },
{ title: GeneRelationshipsTabName.GENE_SIMILARITY, isDisabled: true }
];
const tabsData = [...GeneViewTabMap.values()]
.filter(({ primaryTab }) => primaryTab === GeneViewTabName.GENE_RELATIONSHIPS)
.map((item) => ({
title: item.secondaryTab,
isDisabled: false
})) as Tab[];
const tabClassNames = {
selected: styles.selectedTabName
......@@ -48,17 +52,35 @@ const tabClassNames = {
type Props = {
isSidebarOpen: boolean;
selectedTabName: GeneRelationshipsTabName | null;
setActiveGeneRelationshipsTab: (tab: string) => void;
push: Push;
};
const GeneRelationships = (props: Props) => {
const { genomeId, entityId } = useParams() as { [key: string]: string };
let { selectedTabName } = props;
const changeTab = (tab: string) => {
const match = [...GeneViewTabMap.entries()].find(
([, { secondaryTab }]) => secondaryTab === tab
);
if (!match) {
return;
}
const [view] = match;
const url = urlFor.entityViewer({
genomeId,
entityId,
view
});
props.push(url);
};
// If the selectedTab is disabled or if there is no selectedtab, pick the first available tab
const selectedTabIndex = tabsData.findIndex(
(tab) => tab.title === selectedTabName
);
if (!selectedTabIndex || tabsData[selectedTabIndex].isDisabled) {
if (selectedTabIndex === -1 || tabsData[selectedTabIndex].isDisabled) {
const nextAvailableTab = tabsData.find((tab) => !tab.isDisabled);
selectedTabName =
......@@ -66,16 +88,12 @@ const GeneRelationships = (props: Props) => {
}
const TabWrapper = () => {
const onTabChange = (tab: string) => {
props.setActiveGeneRelationshipsTab(tab);
};
return (
<Tabs
tabs={tabsData}
selectedTab={selectedTabName}
classNames={tabClassNames}
onTabChange={onTabChange}
onTabChange={changeTab}
/>
);
};
......@@ -104,11 +122,12 @@ const GeneRelationships = (props: Props) => {
const mapStateToProps = (state: RootState) => ({
isSidebarOpen: isEntityViewerSidebarOpen(state),
selectedTabName: getEntityViewerActiveGeneRelationships(state).selectedTabName
selectedTabName: getSelectedGeneViewTabs(state)
.secondaryTab as GeneRelationshipsTabName
});
const mapDispatchToProps = {
setActiveGeneRelationshipsTab
push
};
export default connect(mapStateToProps, mapDispatchToProps)(GeneRelationships);
......@@ -15,14 +15,25 @@
*/
import React from 'react';
import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { push, Push } from 'connected-react-router';
import { RootState } from 'src/store';
import { getEntityViewerActiveGeneTab } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewSelectors';
import { setActiveGeneTab } from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewActions';
import * as urlFor from 'src/shared/helpers/urlHelper';
import {
getSelectedGeneViewTabs,
getSelectedTabViews
} from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewSelectors';
import Tabs, { Tab } from 'src/shared/components/tabs/Tabs';
import { RootState } from 'src/store';
import {
GeneViewTabName,
View,
SelectedTabViews
} from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewState.ts';
import styles from './GeneViewTabs.scss';
const tabsData: Tab[] = [
......@@ -34,11 +45,13 @@ const tabsData: Tab[] = [
const DEFAULT_TAB = tabsData[0].title;
type Props = {
selectedGeneTabName: string | null;
setActiveGeneTab: (selectedTabName: string) => void;
selectedTab: string;
selectedTabViews: SelectedTabViews;
push: Push;
};
const GeneViewTabs = (props: Props) => {
const { genomeId, entityId } = useParams() as { [key: string]: string };
const tabClassNames = {
default: styles.geneTab,
selected: styles.selectedGeneTab,
......@@ -47,29 +60,37 @@ const GeneViewTabs = (props: Props) => {
};
const onTabChange = (selectedTabName: string) => {
if (selectedTabName === props.selectedGeneTabName) {
props.setActiveGeneTab(DEFAULT_TAB);
} else {
props.setActiveGeneTab(selectedTabName);
let view;
if (selectedTabName === GeneViewTabName.GENE_FUNCTION) {
view = props.selectedTabViews.geneFunctionTab || View.PROTEIN;
} else if (selectedTabName === GeneViewTabName.GENE_RELATIONSHIPS) {
view = props.selectedTabViews.geneRelationshipsTab || View.ORTHOLOGUES;
}
const url = urlFor.entityViewer({
genomeId,
entityId,
view
});
props.push(url);
};
return (
<Tabs
classNames={tabClassNames}
tabs={tabsData}
selectedTab={props.selectedGeneTabName || DEFAULT_TAB}
selectedTab={props.selectedTab || DEFAULT_TAB}
onTabChange={onTabChange}
/>
);
};
const mapStateToProps = (state: RootState) => ({
selectedGeneTabName: getEntityViewerActiveGeneTab(state)
selectedTab: getSelectedGeneViewTabs(state).primaryTab,
selectedTabViews: getSelectedTabViews(state)
});
const mapDispatchToProps = {
setActiveGeneTab
push
};
export default connect(mapStateToProps, mapDispatchToProps)(GeneViewTabs);
......@@ -22,13 +22,13 @@ import {
getEntityViewerActiveGenomeId,
getEntityViewerActiveEnsObjectId
} from 'src/content/app/entity-viewer/state/general/entityViewerGeneralSelectors';
import {
EntityViewerGeneViewUIState,
View,
GeneViewTabMap,
GeneViewTabName,
GeneFunctionTabName,
GeneRelationshipsTabName
EntityViewerGeneViewUIState
} from 'src/content/app/entity-viewer/state/gene-view/entityViewerGeneViewState';
import { RootState } from 'src/store';
export const updateActiveGeneViewUIState = createAction(
......@@ -39,83 +39,44 @@ export const updateActiveGeneViewUIState = createAction(
fragment: Partial<EntityViewerGeneViewUIState>;
}>();
export const setActiveGeneTab: ActionCreator<ThunkAction<
export const setGeneViewName: ActionCreator<ThunkAction<
void,
any,
null,
Action<string>
>> = (selectedTabName: GeneViewTabName) => (
dispatch,
getState: () => RootState
) => {
>> = (view: View | null) => (dispatch, getState: () => RootState) => {
const activeGenomeId = getEntityViewerActiveGenomeId(getState());
const activeObjectId = getEntityViewerActiveEnsObjectId(getState());
if (!activeGenomeId || !activeObjectId) {
return;
}
const primaryTabName = view ? GeneViewTabMap.get(view)?.primaryTab : null;
const primaryTab =
primaryTabName === GeneViewTabName.GENE_FUNCTION
? 'geneFunctionTab'
: primaryTabName === GeneViewTabName.GENE_RELATIONSHIPS
? 'geneRelationshipsTab'
: null;
const tabView: {
selectedTabViews?: Record<
'geneFunctionTab' | 'geneRelationshipsTab',
View | null
>;
} = {};
if (primaryTab) {
tabView.selectedTabViews = { [primaryTab]: view } as Record<
'geneFunctionTab' | 'geneRelationshipsTab',
View | null
>;
}
dispatch(
updateActiveGeneViewUIState({
activeGenomeId,
activeObjectId,
fragment: {