Unverified Commit 94cd4c76 authored by Ridwan Amode's avatar Ridwan Amode Committed by GitHub
Browse files

Apply transcript filtering to protein view and reposition filter & sort box (#501)

* ENSWBSITES-1038: Applying transcript filters to protein view and showing message when there are no transcripts after applying filters on protein view

* repositioning filter and sort box
* readapting grid layout for geneview content to fit in filters
* Update styles to adjust the height of a grid row to the height of its content
* add box-shadow to filter box
* make red dot bigger when filters are active, filter label white when active
* fix background when scrolling for filter label
* using one icon (chevron down) for both chevron up and down and applying relevant transformation
* fix selected tabs when filterbox is open
* Set a minimum height for the panel so that it doesnt look squash when there is one line.
* set min-height in each child body style
* body style is passed as props to panel component
parent 26bd2feb
Pipeline #167565 passed with stages
in 7 minutes and 8 seconds
......@@ -16,7 +16,7 @@ $backgroundColor: $black;
display: grid;
background-color: $backgroundColor;
grid-template-columns: 178px 902px 1fr;
grid-template-rows: min-content 75px 1fr;
grid-template-rows: min-content minmax(76px, auto) minmax(0, 1fr);
padding: 60px 20px 10px;
height: 100%; // may need to change this later
width: 100%;
......@@ -37,14 +37,95 @@ $backgroundColor: $black;
.geneViewTabs {
grid-area: 2 / 1 / 3 / 4;
display: grid;
grid-template-areas:
"filter-toggle tabs"
"filters filters";
grid-template-columns: 170px auto;
grid-template-rows: auto auto;
z-index: 2;
position: sticky;
top: -80px;
height: 100%;
height: 100%;
}
.tabWrapper {
grid-area: tabs;
height: 76px;
display: flex;
align-items: flex-end;
padding: 4px;
background-color: $backgroundColor;
padding-left: 180px;
}
.filtersWrapper {
grid-area: filters;
}
.geneViewTabContent {
grid-area: 3 / 1 / 4 / 4;
}
.filterLabelContainer {
background-color: $backgroundColor;
grid-area: filter-toggle;
display: flex;
flex-direction: column;
justify-content: flex-end;
position: sticky;
top: 0;
}
.filterLabelWrapper {
padding-right: 20px;
padding-bottom: 9px;
padding-top: 6px;
}
.openFilterLabelContainer {
background-color: $backgroundColor;
top: -80px;
.filterLabelWrapper {
background-color: $soft-black;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
}
.filterLabel {
text-align: right;
padding-right: 2px;
cursor: pointer;
color: $blue;
}
.openFilterLabel {
color: $white;
}
.labelWithActivityIndicator {
position: relative;
&::after {
content: '';
position: absolute;
border-radius: 100%;
height: 8px;
width: 8px;
background: $red;
right: -10px;
top: -2px;
}
}
.chevron {
margin-left: 10px;
margin-bottom: -3px;
height: 12px;
width: 12px;
}
.chevronUp {
transform: rotate(180deg);
}
......@@ -16,6 +16,7 @@
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import classNames from 'classnames';
import { replace } from 'connected-react-router';
import { useQuery, gql } from '@apollo/client';
import { useParams, useLocation } from 'react-router-dom';
......@@ -34,6 +35,10 @@ import {
import { updatePreviouslyViewedEntities } from 'src/content/app/entity-viewer/state/bookmarks/entityViewerBookmarksSlice';
import { closeSidebarModal } from 'src/content/app/entity-viewer/state/sidebar/entityViewerSidebarActions';
import { isEntityViewerSidebarOpen } from 'src/content/app/entity-viewer/state/sidebar/entityViewerSidebarSelectors';
import {
getFilters,
getSortingRule
} from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSelectors';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { buildFocusIdForUrl } from 'src/shared/state/ens-object/ensObjectHelpers';
......@@ -46,6 +51,7 @@ import DefaultTranscriptsList, {
Props as DefaultTranscriptsListProps
} from './components/default-transcripts-list/DefaultTranscriptsList';
import GeneViewTabs from './components/gene-view-tabs/GeneViewTabs';
import TranscriptsFilter from 'src/content/app/entity-viewer/gene-view/components/transcripts-filter/TranscriptsFilter';
import GeneFunction, {
Props as GeneFunctionProps
} from 'src/content/app/entity-viewer/gene-view/components/gene-function/GeneFunction';
......@@ -55,6 +61,9 @@ import { CircleLoader } from 'src/shared/components/loader/Loader';
import { TicksAndScale } from 'src/content/app/entity-viewer/gene-view/components/base-pairs-ruler/BasePairsRuler';
import { FullGene } from 'src/shared/types/thoas/gene';
import { SortingRule } from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSlice';
import { ReactComponent as ChevronDown } from 'static/img/shared/chevron-down.svg';
import styles from './GeneView.scss';
......@@ -184,6 +193,10 @@ const GeneViewWithData = (props: GeneViewWithDataProps) => {
setBasePairsRulerTicks
] = useState<TicksAndScale | null>(null);
const [isFilterOpen, setFilterOpen] = useState(false);
const sortingRule = useSelector(getSortingRule);
const filters = useSelector(getFilters);
const dispatch = useDispatch();
const { search } = useLocation();
const view = new URLSearchParams(search).get('view');
......@@ -200,6 +213,23 @@ const GeneViewWithData = (props: GeneViewWithDataProps) => {
const isSidebarOpen = useSelector(isEntityViewerSidebarOpen);
const shouldShowFilterIndicator =
sortingRule !== SortingRule.DEFAULT || Object.values(filters).some(Boolean);
const filterLabel = (
<span
className={classNames({
[styles.labelWithActivityIndicator]: shouldShowFilterIndicator
})}
>
Filter & sort
</span>
);
const toggleFilter = () => {
setFilterOpen(!isFilterOpen);
};
useEffect(() => {
if (!genomeId || !props.gene) {
return;
......@@ -228,10 +258,43 @@ const GeneViewWithData = (props: GeneViewWithDataProps) => {
<div className={styles.viewInLinks}>
<ViewInApp links={{ genomeBrowser: { url: gbUrl } }} />
</div>
<div className={styles.geneViewTabs}>
<GeneViewTabs />
<div
className={classNames([styles.filterLabelContainer], {
[styles.openFilterLabelContainer]: isFilterOpen
})}
>
{props.gene.transcripts.length > 5 && (
<div className={styles.filterLabelWrapper}>
<div
className={classNames([styles.filterLabel], {
[styles.openFilterLabel]: isFilterOpen
})}
onClick={toggleFilter}
>
{filterLabel}
<ChevronDown
className={classNames([styles.chevron], {
[styles.chevronUp]: isFilterOpen
})}
/>
</div>
</div>
)}
</div>
<div className={styles.tabWrapper}>
<GeneViewTabs isFilterOpen={isFilterOpen} />
</div>
{isFilterOpen && (
<div className={styles.filtersWrapper}>
<TranscriptsFilter
toggleFilter={toggleFilter}
transcripts={props.gene.transcripts}
/>
</div>
)}
</div>
<div className={styles.geneViewTabContent}>
{selectedTabs.primaryTab === GeneViewTabName.TRANSCRIPTS &&
basePairsRulerTicks && (
......
......@@ -12,15 +12,6 @@
font-weight: 300;
}
.headerChevron {
width: 16px;
height: 16px;
cursor: pointer;
margin-left: 0.5em;
top: 5px;
position: relative;
}
.content {
position: relative;
padding-top: 36px;
......@@ -64,36 +55,3 @@
.right {
grid-column: 5/6;
}
.filterLabel {
grid-column: 1/2;
text-align: right;
padding-right: 2px;
cursor: pointer;
}
.labelWithActivityIndicator {
position: relative;
&::after {
content: '';
position: absolute;
border-radius: 100%;
height: 5px;
width: 5px;
background: $red;
right: -6px;
top: -2px;
}
}
.chevron {
margin-left: 10px;
margin-bottom: -3px;
height: 12px;
width: 12px;
}
.hidden {
display: none;
}
......@@ -14,9 +14,8 @@
* limitations under the License.
*/
import React, { useState, useEffect, useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import classNames from 'classnames';
import { Pick2, Pick3 } from 'ts-multipick';
import { getFeatureCoordinates } from 'src/content/app/entity-viewer/shared/helpers/entity-helpers';
......@@ -33,15 +32,11 @@ import {
getFilters,
getSortingRule
} from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSelectors';
import {
toggleTranscriptInfo,
SortingRule
} from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSlice';
import { toggleTranscriptInfo } from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSlice';
import DefaultTranscriptsListItem, {
DefaultTranscriptListItemProps
} from './default-transcripts-list-item/DefaultTranscriptListItem';
import TranscriptsFilter from 'src/content/app/entity-viewer/gene-view/components/transcripts-filter/TranscriptsFilter';
import { TicksAndScale } from 'src/content/app/entity-viewer/gene-view/components/base-pairs-ruler/BasePairsRuler';
import { FullGene } from 'src/shared/types/thoas/gene';
......@@ -51,8 +46,6 @@ import { FullCDS } from 'src/shared/types/thoas/cds';
import { SplicedExon } from 'src/shared/types/thoas/exon';
import { Slice } from 'src/shared/types/thoas/slice';
import { ReactComponent as ChevronDown } from 'static/img/shared/chevron-down.svg';
import styles from './DefaultTranscriptsList.scss';
type ProductGeneratingContext = {
......@@ -99,8 +92,6 @@ const DefaultTranscriptslist = (props: Props) => {
? (filterTranscriptsBySOTerm(sortedTranscripts, filters) as Transcript[])
: sortedTranscripts;
const [isFilterOpen, setFilterOpen] = useState(false);
useEffect(() => {
const hasExpandedTranscripts = !!expandedTranscriptIds.length;
......@@ -110,40 +101,10 @@ const DefaultTranscriptslist = (props: Props) => {
}
}, []);
const shouldShowFilterIndicator =
sortingRule !== SortingRule.DEFAULT || Object.values(filters).some(Boolean);
const filterLabel = (
<span
className={classNames({
[styles.labelWithActivityIndicator]: shouldShowFilterIndicator
})}
>
Filter & sort
</span>
);
const toggleFilter = () => {
setFilterOpen(!isFilterOpen);
};
return (
<div>
<div className={styles.header}>
{isFilterOpen && (
<TranscriptsFilter
label={filterLabel}
toggleFilter={toggleFilter}
transcripts={filteredTranscripts}
/>
)}
<div className={styles.row}>
{gene.transcripts.length > 5 && !isFilterOpen && (
<div className={styles.filterLabel} onClick={toggleFilter}>
{filterLabel}
<ChevronDown className={styles.chevron} />
</div>
)}
<div className={styles.right}>Transcript ID</div>
</div>
</div>
......
......@@ -7,6 +7,7 @@
.panelBody {
color: $black;
min-height: 300px;
}
.header {
......
......@@ -7,6 +7,7 @@
.panelBody {
color: $black;
min-height: 300px;
}
.selectedTabName {
......
......@@ -15,8 +15,10 @@
}
.geneViewTabs {
position: absolute;
bottom: -9px;
padding: 0;
height: auto;
z-index: 2;
overflow-y: visible;
}
.disabledGeneTab {
......@@ -42,6 +44,12 @@
}
}
.selectedGeneTab.withOpenFilter {
&::after {
display: none;
}
}
.geneTab:first-of-type {
&::after {
display: none;
......
......@@ -15,6 +15,7 @@
*/
import React from 'react';
import classNames from 'classnames';
import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { push, Push } from 'connected-react-router';
......@@ -48,15 +49,18 @@ type Props = {
selectedTab: string;
selectedTabViews?: SelectedTabViews;
push: Push;
isFilterOpen: boolean;
};
const GeneViewTabs = (props: Props) => {
const { genomeId, entityId } = useParams() as { [key: string]: string };
const tabClassNames = {
default: styles.geneTab,
selected: styles.selectedGeneTab,
selected: classNames(styles.selectedGeneTab, {
[styles.withOpenFilter]: props.isFilterOpen
}),
disabled: styles.disabledGeneTab,
tabsContainer: styles.geneViewTabs
tabsContainer: styles.geneViewTabs //FIXME: Pass this as a props so that it can be styled from the parent
};
const onTabChange = (selectedTabName: string) => {
......
......@@ -31,10 +31,14 @@ import {
transcriptSortingFunctions,
defaultSort
} from 'src/content/app/entity-viewer/shared/helpers/transcripts-sorter';
import { filterTranscriptsBySOTerm } from 'src/content/app/entity-viewer/shared/helpers/transcripts-filter';
import { toggleExpandedProtein } from 'src/content/app/entity-viewer/state/gene-view/proteins/geneViewProteinsSlice';
import { getExpandedTranscriptIds } from 'src/content/app/entity-viewer/state/gene-view/proteins/geneViewProteinsSelectors';
import { getSortingRule } from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSelectors';
import {
getFilters,
getSortingRule
} from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSelectors';
import { FullGene } from 'src/shared/types/thoas/gene';
import { FullTranscript } from 'src/shared/types/thoas/transcript';
......@@ -72,11 +76,20 @@ const ProteinsList = (props: ProteinsListProps) => {
const sortingFunction = transcriptSortingFunctions[sortingRule];
const sortedTranscripts = sortingFunction(props.gene.transcripts);
const proteinCodingTranscripts = sortedTranscripts.filter(
const filters = useSelector(getFilters);
const filteredTranscripts = Object.values(filters).some(Boolean)
? (filterTranscriptsBySOTerm(sortedTranscripts, filters) as Transcript[])
: sortedTranscripts;
const proteinCodingTranscripts = filteredTranscripts.filter(
isProteinCodingTranscript
) as Transcript[];
useEffect(() => {
if (!proteinCodingTranscripts.length) {
return;
}
const hasExpandedTranscripts = !!expandedTranscriptIds.length;
const firstProteinId =
proteinCodingTranscripts[0].product_generating_contexts[0].product
......@@ -89,7 +102,9 @@ const ProteinsList = (props: ProteinsListProps) => {
const longestProteinLength = getLongestProteinLength(props.gene);
return (
return !proteinCodingTranscripts.length ? (
<div>No transcripts to show with the filters selected</div>
) : (
<div className={styles.proteinsList}>
{proteinCodingTranscripts.map((transcript) => (
<ProteinsListItem
......
......@@ -2,44 +2,29 @@
@import 'src/content/app/entity-viewer/gene-view/styles/constants';
.container {
display: grid;
grid-template-columns: $left_column $middle_column_left_gap auto;
padding: 9px 0;
}
.filterLabel {
grid-column: 1/2;
text-align: right;
padding-right: 2px;
cursor: pointer;
}
.chevron {
margin-left: 10px;
height: 12px;
width: 12px;
margin-bottom: -3px;
grid-area: 3/1/3/4;
position: sticky;
height: 100%;
top: -5px;
z-index: 1;
padding-bottom: 9px;
}
.filterBox {
display: grid;
grid-column: 3;
grid-template-columns: 270px minmax(210px, auto) 20px;
grid-template-columns: 430px minmax(210px, auto) 20px;
background-color: $soft-black;
color: $white;
text-align: left;
padding: 15px 15px 45px 20px;
margin-top: 2px;
margin-left: 2px;
font-weight: 300;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
box-shadow: -1px 16px 18px 0px rgba(0, 0, 0, 0.25);
}
@media screen and (min-width: 1600px) {
width: calc(100% - 320px);
&FullWidth {
width: 100%;
}
}
.sortContainer {
padding-left: 160px;
}
.filterContent {
......
......@@ -82,12 +82,10 @@ const wrapInRedux = (
state: typeof mockState = mockState,
transcripts = defaultTranscripts
) => {
const filterLabel = <span>Filter & sort</span>;
store = mockStore(state);
return render(
<Provider store={store}>
<TranscriptsFilter
label={filterLabel}
transcripts={transcripts}
toggleFilter={mockToggleFilter}
/>
......
......@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { ReactNode, useEffect } from 'react';
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import classNames from 'classnames';
......@@ -36,7 +36,6 @@ import RadioGroup, {
import Checkbox from 'src/shared/components/checkbox/Checkbox';