GeneView.tsx 10.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/**
 * 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.
 */

17 18
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
19
import classNames from 'classnames';
20
import { replace } from 'connected-react-router';
Andrey Azov's avatar
Andrey Azov committed
21
import { useQuery, gql } from '@apollo/client';
22 23
import { useParams, useLocation } from 'react-router-dom';

24
import { useRestoreScrollPosition } from 'src/shared/hooks/useRestoreScrollPosition';
25 26 27
import usePrevious from 'src/shared/hooks/usePrevious';
import {
  getSelectedGeneViewTabs,
28 29
  getCurrentView
} from 'src/content/app/entity-viewer/state/gene-view/view/geneViewViewSelectors';
30
import {
31 32 33 34
  updateView,
  View,
  GeneViewTabName
} from 'src/content/app/entity-viewer/state/gene-view/view/geneViewViewSlice';
Jyothish's avatar
Jyothish committed
35
import { updatePreviouslyViewedEntities } from 'src/content/app/entity-viewer/state/bookmarks/entityViewerBookmarksSlice';
36 37 38 39
import {
  getFilters,
  getSortingRule
} from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSelectors';
40

41
import * as urlFor from 'src/shared/helpers/urlHelper';
42
import { buildFocusIdForUrl } from 'src/shared/state/ens-object/ensObjectHelpers';
43 44
import { parseFocusIdFromUrl } from 'src/shared/state/ens-object/ensObjectHelpers';

45 46 47 48 49 50
import GeneOverviewImage, {
  GeneOverviewImageProps
} from './components/gene-overview-image/GeneOverviewImage';
import DefaultTranscriptsList, {
  Props as DefaultTranscriptsListProps
} from './components/default-transcripts-list/DefaultTranscriptsList';
51
import GeneViewTabs from './components/gene-view-tabs/GeneViewTabs';
52
import TranscriptsFilter from 'src/content/app/entity-viewer/gene-view/components/transcripts-filter/TranscriptsFilter';
53 54 55
import GeneFunction, {
  Props as GeneFunctionProps
} from 'src/content/app/entity-viewer/gene-view/components/gene-function/GeneFunction';
56
import GeneRelationships from 'src/content/app/entity-viewer/gene-view/components/gene-relationships/GeneRelationships';
57
import ViewInApp from 'src/shared/components/view-in-app/ViewInApp';
58
import { CircleLoader } from 'src/shared/components/loader/Loader';
59
import { TicksAndScale } from 'src/content/app/entity-viewer/gene-view/components/base-pairs-ruler/BasePairsRuler';
Andrey Azov's avatar
Andrey Azov committed
60
import ShowHide from 'src/shared/components/show-hide/ShowHide';
61

Jyothish's avatar
Jyothish committed
62
import { FullGene } from 'src/shared/types/thoas/gene';
63 64
import { SortingRule } from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSlice';

65 66
import styles from './GeneView.scss';

Jyothish's avatar
Jyothish committed
67 68
type Gene = Pick<FullGene, 'symbol'> &
  GeneOverviewImageProps['gene'] &
69 70 71
  DefaultTranscriptsListProps['gene'] &
  GeneFunctionProps['gene'];

72 73 74 75 76
type GeneViewWithDataProps = {
  gene: Gene;
};

const QUERY = gql`
77 78 79
  query Gene($genomeId: String!, $geneId: String!) {
    gene(byId: { genome_id: $genomeId, stable_id: $geneId }) {
      stable_id
Jyothish's avatar
Jyothish committed
80
      symbol
81
      unversioned_stable_id
82
      version
83 84 85 86
      slice {
        location {
          start
          end
87
          length
88
        }
89 90
        strand {
          code
91 92 93
        }
      }
      transcripts {
94 95
        stable_id
        unversioned_stable_id
96 97 98 99
        slice {
          location {
            start
            end
100
            length
101
          }
102 103
          region {
            name
104 105 106
          }
          strand {
            code
107
          }
108
        }
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
        relative_location {
          start
          end
        }
        spliced_exons {
          relative_location {
            start
            end
          }
          exon {
            stable_id
            slice {
              location {
                length
              }
124 125 126
            }
          }
        }
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
        product_generating_contexts {
          product_type
          cds {
            relative_start
            relative_end
          }
          cdna {
            length
          }
          phased_exons {
            start_phase
            end_phase
            exon {
              stable_id
            }
          }
          product {
            stable_id
            unversioned_stable_id
146
            length
147 148
            external_references {
              accession_id
149
              name
150 151 152 153 154
              description
              source {
                id
              }
            }
155
          }
156
        }
157 158 159 160 161 162 163 164 165
        external_references {
          accession_id
          name
          url
          source {
            id
            name
          }
        }
166
        metadata {
167 168 169 170 171 172 173 174 175 176 177 178 179
          biotype {
            label
            value
            definition
          }
          tsl {
            label
            value
          }
          appris {
            label
            value
          }
180 181 182 183 184 185 186 187 188
          canonical {
            value
            label
            definition
          }
          mane {
            value
            label
            definition
189 190 191 192
            ncbi_transcript {
              id
              url
            }
193
          }
194 195 196 197 198 199 200 201 202
          gencode_basic {
            label
          }
          appris {
            label
          }
          tsl {
            label
          }
203
        }
204 205 206 207 208
      }
    }
  }
`;

209
const GeneView = () => {
210
  const params: { [key: string]: string } = useParams();
211
  const { genomeId, entityId } = params;
212
  const { objectId: geneId } = parseFocusIdFromUrl(entityId);
213

214
  const { loading, data } = useQuery<{ gene: Gene }>(QUERY, {
215
    variables: { geneId, genomeId }
216
  });
217

218 219 220 221 222 223 224 225
  // TODO decide about error handling
  if (loading) {
    return (
      <div className={styles.geneViewLoadingContainer}>
        <CircleLoader />
      </div>
    );
  } else if (!data) {
226 227 228
    return null;
  }

229
  return <GeneViewWithData gene={data.gene} />;
230 231
};

232 233
const COMPONENT_ID = 'entity_viewer_gene_view';

234
const GeneViewWithData = (props: GeneViewWithDataProps) => {
235 236
  const [basePairsRulerTicks, setBasePairsRulerTicks] =
    useState<TicksAndScale | null>(null);
237

238 239 240 241
  const [isFilterOpen, setFilterOpen] = useState(false);

  const sortingRule = useSelector(getSortingRule);
  const filters = useSelector(getFilters);
Jyothish's avatar
Jyothish committed
242
  const dispatch = useDispatch();
243 244 245 246 247 248 249 250 251
  const { search } = useLocation();
  const view = new URLSearchParams(search).get('view');

  const uniqueScrollReferenceId = `${COMPONENT_ID}_${props.gene.stable_id}_${view}`;

  const { targetElementRef } = useRestoreScrollPosition({
    referenceId: uniqueScrollReferenceId
  });

252 253 254
  const { genomeId, geneId, selectedTabs } = useGeneViewRouting();
  const focusId = buildFocusIdForUrl({ type: 'gene', objectId: geneId });
  const gbUrl = urlFor.browser({ genomeId, focus: focusId });
255

256
  const shouldShowFilterIndicator =
257 258
    sortingRule !== SortingRule.DEFAULT ||
    Object.values(filters).some((filter) => filter.selected);
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273

  const filterLabel = (
    <span
      className={classNames({
        [styles.labelWithActivityIndicator]: shouldShowFilterIndicator
      })}
    >
      Filter & sort
    </span>
  );

  const toggleFilter = () => {
    setFilterOpen(!isFilterOpen);
  };

Jyothish's avatar
Jyothish committed
274 275 276 277 278 279 280 281 282 283 284 285 286
  useEffect(() => {
    if (!genomeId || !props.gene) {
      return;
    }

    dispatch(
      updatePreviouslyViewedEntities({
        genomeId,
        gene: props.gene
      })
    );
  }, [genomeId, geneId]);

287
  return (
288
    <div className={styles.geneView} ref={targetElementRef}>
289
      <div className={styles.featureImage}>
290 291 292 293
        <GeneOverviewImage
          gene={props.gene}
          onTicksCalculated={setBasePairsRulerTicks}
        />
294
      </div>
295
      <div className={styles.viewInLinks}>
296
        <ViewInApp links={{ genomeBrowser: { url: gbUrl } }} />
297
      </div>
298
      <div className={styles.geneViewTabs}>
299 300 301 302 303 304 305
        <div
          className={classNames([styles.filterLabelContainer], {
            [styles.openFilterLabelContainer]: isFilterOpen
          })}
        >
          {props.gene.transcripts.length > 5 && (
            <div className={styles.filterLabelWrapper}>
Andrey Azov's avatar
Andrey Azov committed
306
              <ShowHide
307
                onClick={toggleFilter}
Andrey Azov's avatar
Andrey Azov committed
308 309 310
                isExpanded={isFilterOpen}
                label={filterLabel}
              />
311 312 313 314 315 316 317 318 319 320 321 322 323 324
            </div>
          )}
        </div>
        <div className={styles.tabWrapper}>
          <GeneViewTabs isFilterOpen={isFilterOpen} />
        </div>
        {isFilterOpen && (
          <div className={styles.filtersWrapper}>
            <TranscriptsFilter
              toggleFilter={toggleFilter}
              transcripts={props.gene.transcripts}
            />
          </div>
        )}
325
      </div>
326

327
      <div className={styles.geneViewTabContent}>
328
        {selectedTabs.primaryTab === GeneViewTabName.TRANSCRIPTS &&
329
          basePairsRulerTicks && (
330
            <DefaultTranscriptsList
331 332 333 334 335
              gene={props.gene}
              rulerTicks={basePairsRulerTicks}
            />
          )}

336
        {selectedTabs.primaryTab === GeneViewTabName.GENE_FUNCTION && (
337
          <GeneFunction gene={props.gene} />
338 339
        )}

340
        {selectedTabs.primaryTab === GeneViewTabName.GENE_RELATIONSHIPS && (
341
          <GeneRelationships />
342
        )}
343
      </div>
344
    </div>
345 346 347
  );
};

348 349 350
const isViewParameterValid = (view: string) =>
  Object.values(View).some((value) => value === view);

351 352 353 354 355 356 357
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?
358 359 360 361

  const urlSearchParams = new URLSearchParams(search);
  const view = urlSearchParams.get('view');
  const proteinId = urlSearchParams.get('protein_id');
362
  const viewInRedux = useSelector(getCurrentView) || View.TRANSCRIPTS;
363 364 365 366
  const previousGenomeId = usePrevious(genomeId); // genomeId during previous render
  const selectedTabs = useSelector(getSelectedGeneViewTabs);

  useEffect(() => {
367
    if (view && isViewParameterValid(view) && viewInRedux !== view) {
368
      dispatch(updateView(view as View));
369 370 371 372
    } else {
      const url = urlFor.entityViewer({
        genomeId,
        entityId,
373 374
        view: viewInRedux,
        proteinId
375 376
      });
      dispatch(replace(url));
377 378 379 380 381 382 383 384 385
    }
  }, [view, viewInRedux, genomeId, previousGenomeId]);

  return {
    genomeId,
    geneId,
    selectedTabs
  };
};
386

387
export default GeneView;