diff --git a/client/src/components/Pages/Structure/Builder/Builder.scss b/client/src/components/Pages/Structure/Builder/Builder.scss index cf8c083c..d2e338ee 100644 --- a/client/src/components/Pages/Structure/Builder/Builder.scss +++ b/client/src/components/Pages/Structure/Builder/Builder.scss @@ -152,6 +152,7 @@ .mid-inputs { display: flex; justify-content: space-between; + align-items: center; } .bottom-inputs { diff --git a/client/src/components/Pages/Structure/Input/StaticElement/StaticElement.tsx b/client/src/components/Pages/Structure/Input/StaticElement/StaticElement.tsx index c97b963f..ea8a1f9d 100644 --- a/client/src/components/Pages/Structure/Input/StaticElement/StaticElement.tsx +++ b/client/src/components/Pages/Structure/Input/StaticElement/StaticElement.tsx @@ -12,7 +12,7 @@ const StaticElement: React.FC = ({ handleDelete, validated: true, icon, - pendingResponse: false + pendingResponse: false, }); export default StaticElement; diff --git a/client/src/components/Pages/Structure/Input/StructuralElementInputProps.tsx b/client/src/components/Pages/Structure/Input/StructuralElementInputProps.tsx index ef4aa869..354e8913 100644 --- a/client/src/components/Pages/Structure/Input/StructuralElementInputProps.tsx +++ b/client/src/components/Pages/Structure/Input/StructuralElementInputProps.tsx @@ -4,7 +4,7 @@ export interface BaseStructuralElementProps { element: ClientElementUnion; handleDelete?: (id?: string) => void; icon: JSX.Element; - pendingResponse?: boolean + pendingResponse?: boolean; } export interface StructuralElementInputProps diff --git a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx index 0b9aef51..7180adb3 100644 --- a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx @@ -1,33 +1,28 @@ import { TextField, MenuItem, - Select, - Box, Typography, + Box, + FormControl, + InputLabel, + Select, } from "@material-ui/core"; import { ClientTranscriptSegmentElement, TranscriptSegmentElement, TxSegmentElementResponse, } from "../../../../../services/ResponseModels"; -import React, { - useEffect, - useState, - KeyboardEvent, - useContext, - ChangeEvent, -} from "react"; +import React, { useEffect, useState, KeyboardEvent, ChangeEvent } from "react"; import { - getTxSegmentElementECT, - getTxSegmentElementGCG, - getTxSegmentElementGCT, + GenomicInputType, + getTxSegmentElementEC, + getTxSegmentElementGC, getTxSegmentNomenclature, TxElementInputType, } from "../../../../../services/main"; import { GeneAutocomplete } from "../../../../main/shared/GeneAutocomplete/GeneAutocomplete"; import { StructuralElementInputProps } from "../StructuralElementInputProps"; import StructuralElementInputAccordion from "../StructuralElementInputAccordion"; -import { FusionContext } from "../../../../../global/contexts/FusionContext"; import HelpTooltip from "../../../../main/shared/HelpTooltip/HelpTooltip"; import ChromosomeField from "../../../../main/shared/ChromosomeField/ChromosomeField"; import TranscriptField from "../../../../main/shared/TranscriptField/TranscriptField"; @@ -43,12 +38,19 @@ const TxSegmentCompInput: React.FC = ({ handleDelete, icon, }) => { - const { fusion } = useContext(FusionContext); - const [txInputType, setTxInputType] = useState( (element.inputType as TxElementInputType) || TxElementInputType.default ); + const [genomicInputType, setGenomicInputType] = + useState( + element.inputGene + ? GenomicInputType.GENE + : element.inputTx + ? GenomicInputType.TRANSCRIPT + : null + ); + // "Text" variables refer to helper or warning text to set under input fields // TODO: this needs refactored so badly const [txAc, setTxAc] = useState(element.inputTx || ""); @@ -62,7 +64,6 @@ const TxSegmentCompInput: React.FC = ({ ); const [txChrom, setTxChrom] = useState(element.inputChr || ""); - const [txChromText, setTxChromText] = useState(""); const [txStartingGenomic, setTxStartingGenomic] = useState( element.inputGenomicStart || "" @@ -86,22 +87,28 @@ const TxSegmentCompInput: React.FC = ({ ); const [endingExonOffsetText, setEndingExonOffsetText] = useState(""); + const [geneTranscripts, setGeneTranscripts] = useState([]); + const [selectedTranscript, setSelectedTranscript] = useState( + element.inputTx || "" + ); + const [pendingResponse, setPendingResponse] = useState(false); const hasRequiredEnds = txEndingGenomic || endingExon || txStartingGenomic || startingExon; + const genomicInputComplete = + genomicInputType === GenomicInputType.GENE + ? txGene !== "" && selectedTranscript !== "" + : txAc !== ""; + // programming horror const inputComplete = - (txInputType === TxElementInputType.gcg && - txGene !== "" && + (txInputType === TxElementInputType.gc && + genomicInputComplete && txChrom !== "" && (txStartingGenomic !== "" || txEndingGenomic !== "")) || - (txInputType === TxElementInputType.gct && - txAc !== "" && - txChrom !== "" && - (txStartingGenomic !== "" || txEndingGenomic !== "")) || - (txInputType === TxElementInputType.ect && + (txInputType === TxElementInputType.ec && txAc !== "" && (startingExon !== "" || endingExon !== "")); @@ -109,7 +116,6 @@ const TxSegmentCompInput: React.FC = ({ inputComplete && hasRequiredEnds && txGeneText === "" && - txChromText === "" && txAcText === "" && txStartingGenomicText === "" && txEndingGenomicText === "" && @@ -165,15 +171,9 @@ const TxSegmentCompInput: React.FC = ({ setPendingResponse(false); }; - /** - * Check for, and handle, warning about invalid chromosome input - * @param responseWarnings warnings property of transcript segment response object - */ - const checkChromosomeWarning = (responseWarnings: string[]) => { - const chromWarning = `Invalid chromosome: ${txChrom}`; - if (responseWarnings.includes(chromWarning)) { - setTxChromText("Unrecognized value"); - } + const handleTranscriptSelect = (event: any) => { + setSelectedTranscript(event.target.value as string); + setTxAc(event.target.value as string); }; /** @@ -194,13 +194,13 @@ const TxSegmentCompInput: React.FC = ({ setTxEndingGenomicText("Out of range"); } } + setPendingResponse(false); }; /** * Reset warnings related to genomic coordinate values */ const clearGenomicCoordWarnings = () => { - setTxChromText(""); setTxStartingGenomicText(""); setTxEndingGenomicText(""); }; @@ -222,39 +222,12 @@ const TxSegmentCompInput: React.FC = ({ setPendingResponse(true); // fire constructor request switch (txInputType) { - case TxElementInputType.gcg: + case TxElementInputType.gc: clearGenomicCoordWarnings(); - getTxSegmentElementGCG( + getTxSegmentElementGC( txGene, txChrom, - txStartingGenomic, - txEndingGenomic, - txStrand - ).then((txSegmentResponse) => { - if ( - txSegmentResponse.warnings && - txSegmentResponse.warnings?.length > 0 - ) { - checkChromosomeWarning(txSegmentResponse.warnings); - CheckGenomicCoordWarning(txSegmentResponse.warnings); - } else { - const inputParams = { - inputType: txInputType, - inputStrand: txStrand, - inputGene: txGene, - inputChr: txChrom, - inputGenomicStart: txStartingGenomic, - inputGenomicEnd: txEndingGenomic, - }; - handleTxElementResponse(txSegmentResponse, inputParams); - } - }); - break; - case TxElementInputType.gct: - clearGenomicCoordWarnings(); - getTxSegmentElementGCT( - txAc, - txChrom, + selectedTranscript, txStartingGenomic, txEndingGenomic ).then((txSegmentResponse) => { @@ -262,14 +235,12 @@ const TxSegmentCompInput: React.FC = ({ txSegmentResponse.warnings && txSegmentResponse.warnings?.length > 0 ) { - // TODO more warnings - checkChromosomeWarning(txSegmentResponse.warnings); CheckGenomicCoordWarning(txSegmentResponse.warnings); } else { const inputParams = { inputType: txInputType, - inputTx: txAc, inputStrand: txStrand, + inputGene: txGene, inputChr: txChrom, inputGenomicStart: txStartingGenomic, inputGenomicEnd: txEndingGenomic, @@ -278,8 +249,8 @@ const TxSegmentCompInput: React.FC = ({ } }); break; - case TxElementInputType.ect: - getTxSegmentElementECT( + case TxElementInputType.ec: + getTxSegmentElementEC( txAc, startingExon as string, endingExon as string, @@ -433,7 +404,6 @@ const TxSegmentCompInput: React.FC = ({ @@ -443,31 +413,57 @@ const TxSegmentCompInput: React.FC = ({ const renderTxOptions = () => { switch (txInputType) { - case TxElementInputType.gcg: + case TxElementInputType.gc: + if (!genomicInputType) { + return <>; + } return ( - - + + {genomicInputType === GenomicInputType.GENE ? ( + <> + + + + Transcript + + + + + ) : ( + <> + {txInputField} + + )} {genomicCoordinateInfo} ); - case TxElementInputType.gct: - return ( - - {txInputField} - {genomicCoordinateInfo} - - ); - case TxElementInputType.ect: + case TxElementInputType.ec: return ( {txInputField} @@ -606,7 +602,7 @@ const TxSegmentCompInput: React.FC = ({ */ const selectInputType = (selection: TxElementInputType) => { if (txInputType !== "default") { - if (selection === "exon_coords_tx") { + if (selection === "exon_coords") { clearGenomicCoordWarnings(); } else { clearExonWarnings(); @@ -634,25 +630,42 @@ const TxSegmentCompInput: React.FC = ({ } > - + + Select input data + + {txInputType === TxElementInputType.gc ? ( + + + Gene or Transcript? + + + + ) : ( + <> + )} + {renderTxOptions()} diff --git a/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx b/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx index 91f7f830..a5eda92d 100644 --- a/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx +++ b/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx @@ -9,6 +9,8 @@ import { makeStyles, Box, Link, + InputLabel, + FormControl, } from "@material-ui/core"; import React, { ChangeEvent, useEffect, useState } from "react"; import { GeneAutocomplete } from "../../main/shared/GeneAutocomplete/GeneAutocomplete"; @@ -16,6 +18,7 @@ import { getGenomicCoords, getExonCoords, TxElementInputType, + GenomicInputType, } from "../../../services/main"; import { CoordsUtilsResponse, @@ -26,6 +29,7 @@ import TabPaper from "../../main/shared/TabPaper/TabPaper"; import { HelpPopover } from "../../main/shared/HelpPopover/HelpPopover"; import ChromosomeField from "../../main/shared/ChromosomeField/ChromosomeField"; import TranscriptField from "../../main/shared/TranscriptField/TranscriptField"; +import LoadingMessage from "../../main/shared/LoadingMessage/LoadingMessage"; const GetCoordinates: React.FC = () => { const useStyles = makeStyles(() => ({ @@ -47,7 +51,7 @@ const GetCoordinates: React.FC = () => { }, inputParams: { display: "flex", - width: "70%", + width: "100%", flexDirection: "column", justifyContent: "center", alignItems: "flex-start", @@ -71,6 +75,8 @@ const GetCoordinates: React.FC = () => { const [inputType, setInputType] = useState( TxElementInputType.default ); + const [genomicInputType, setGenomicInputType] = + useState(null); const [txAc, setTxAc] = useState(""); const [txAcText, setTxAcText] = useState(""); @@ -95,20 +101,20 @@ const GetCoordinates: React.FC = () => { const [exonStartOffset, setExonStartOffset] = useState(""); const [exonEndOffset, setExonEndOffset] = useState(""); + const [geneTranscripts, setGeneTranscripts] = useState([]); + const [selectedTranscript, setSelectedTranscript] = useState(""); + const [results, setResults] = useState(null); - const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [responseWarnings, setResponseWarnings] = useState([]); // programming horror const inputComplete = - (inputType === "genomic_coords_gene" && + (inputType === "genomic_coords" && gene !== "" && chromosome !== "" && (start !== "" || end !== "")) || - (inputType === "genomic_coords_tx" && - txAc !== "" && - chromosome !== "" && - (start !== "" || end !== "")) || - (inputType === "exon_coords_tx" && + (inputType === "exon_coords" && txAc !== "" && (exonStart !== "" || exonEnd !== "")); @@ -142,30 +148,28 @@ const GetCoordinates: React.FC = () => { const clearWarnings = () => { setTxAcText(""); setGeneText(""); - setChromosomeText(""); setStartText(""); setEndText(""); setExonStartText(""); setExonEndText(""); + setResponseWarnings([]); }; const handleResponse = (coordsResponse: CoordsUtilsResponse) => { if (coordsResponse.warnings) { + setResponseWarnings(coordsResponse.warnings); setResults(null); - clearWarnings(); coordsResponse.warnings.forEach((warning) => { if (warning.startsWith("Found more than one accession")) { setChromosomeText("Complete ID required"); } else if (warning.startsWith("Unable to get exons for")) { setTxAcText("Unrecognized transcript"); - } else if (warning.startsWith("Invalid chromosome")) { - setChromosomeText("Unrecognized value"); } else if ( warning == "Must find exactly one row for genomic data, but found: 0" ) { - setError( - "Unable to resolve coordinates lookup given provided parameters" - ); + setResponseWarnings([ + "Unable to resolve coordinates lookup given provided parameters. Double check that the coordinates entered are valid for the selected transcript.", + ]); } else if (warning.startsWith("Exon ")) { const exonPattern = /Exon (\d*) does not exist on (.*)/; const match = exonPattern.exec(warning); @@ -182,10 +186,17 @@ const GetCoordinates: React.FC = () => { clearWarnings(); setResults(coordsResponse.coordinates_data as GenomicTxSegService); } + setIsLoading(false); + }; + + const handleTranscriptSelect = (event: any) => { + setSelectedTranscript(event.target.value as string); + setTxAc(event.target.value as string); }; const fetchResults = () => { - if (inputType == "exon_coords_tx") { + setIsLoading(true); + if (inputType == "exon_coords") { getGenomicCoords( gene, txAc, @@ -194,18 +205,17 @@ const GetCoordinates: React.FC = () => { exonStartOffset, exonEndOffset ).then((coordsResponse) => handleResponse(coordsResponse)); - } else if (inputType == "genomic_coords_gene") { - getExonCoords(chromosome, start, end, gene).then((coordsResponse) => - handleResponse(coordsResponse) - ); - } else if (inputType == "genomic_coords_tx") { - getExonCoords(chromosome, start, end, "", txAc).then((coordsResponse) => + } else if (inputType == "genomic_coords") { + getExonCoords(chromosome, start, end, gene, txAc).then((coordsResponse) => handleResponse(coordsResponse) ); } }; - const renderRow = (title: string, value: string | number) => ( + const renderRow = ( + title: string, + value: string | number | null | undefined + ) => ( {title} @@ -215,6 +225,9 @@ const GetCoordinates: React.FC = () => { ); const renderResults = (): React.ReactFragment => { + if (isLoading) { + return ; + } if (inputValid) { if (results) { const txSegStart = results.seg_start; @@ -234,9 +247,6 @@ const GetCoordinates: React.FC = () => { ? renderRow("Genomic start", genomicStart) : null} {genomicEnd != null ? renderRow("Genomic end", genomicEnd) : null} - {results.strand - ? renderRow("Strand", results.strand === 1 ? "+" : "-") - : null} {renderRow("Transcript", results.tx_ac)} {txSegStart?.exon_ord != null ? renderRow("Exon start", txSegStart.exon_ord) @@ -252,16 +262,32 @@ const GetCoordinates: React.FC = () => { : null} ); - } else if (error) { - return {error}; + } else if (responseWarnings?.length > 0) { + return {responseWarnings}; } else { - return <>; + return ( + + An unknown error has occurred. Please{" "} + + submit an issue on our GitHub + {" "} + and include replication steps, along with the values entered. + + ); } } else { - return <>; // TODO error message + return <>; } }; + const txInputField = ( + + ); + const handleChromosomeChange = (e: ChangeEvent) => { setChromosome(e.target.value); }; @@ -271,7 +297,6 @@ const GetCoordinates: React.FC = () => { @@ -280,90 +305,85 @@ const GetCoordinates: React.FC = () => { const renderInputOptions = () => { switch (inputType) { - case "genomic_coords_gene": - return ( - <> - - - - {genomicCoordinateInfo} - - setStart(event.target.value)} - /> - setEnd(event.target.value)} - /> - - - ); - case "genomic_coords_tx": + case TxElementInputType.gc: + if (!genomicInputType) { + return <>; + } return ( <> - + {genomicInputType === GenomicInputType.GENE ? ( + <> + + + Transcript + + + + ) : ( + <> + {txInputField} + + )} {genomicCoordinateInfo} setStart(event.target.value)} + helperText={start ? startText : ""} /> setEnd(event.target.value)} /> ); - case "exon_coords_tx": + case TxElementInputType.ec: return ( <> - - - + {txInputField} setExonStart(event.target.value)} - error={exonStart && exonStartText !== ""} + error={exonStart === "" && exonStartText !== ""} helperText={exonStart ? exonStartText : ""} /> setExonStartOffset(event.target.value)} @@ -372,16 +392,16 @@ const GetCoordinates: React.FC = () => { setExonEnd(event.target.value)} - error={exonEnd && exonEndText !== ""} + error={exonEnd !== "" && exonEndText !== ""} helperText={exonEnd ? exonEndText : ""} /> setExonEndOffset(event.target.value)} @@ -398,21 +418,35 @@ const GetCoordinates: React.FC = () => { + {inputType === TxElementInputType.gc ? ( + + + Gene or Transcript? + + + + ) : ( + <> + )} {renderInputOptions()} diff --git a/client/src/components/Utilities/GetSequence/GetSequence.tsx b/client/src/components/Utilities/GetSequence/GetSequence.tsx index c39ae5e6..52bf4132 100644 --- a/client/src/components/Utilities/GetSequence/GetSequence.tsx +++ b/client/src/components/Utilities/GetSequence/GetSequence.tsx @@ -13,6 +13,7 @@ import HelpTooltip from "../../main/shared/HelpTooltip/HelpTooltip"; import { HelpPopover } from "../../main/shared/HelpPopover/HelpPopover"; import TabHeader from "../../main/shared/TabHeader/TabHeader"; import TabPaper from "../../main/shared/TabPaper/TabPaper"; +import LoadingMessage from "../../main/shared/LoadingMessage/LoadingMessage"; const GetSequenceIds: React.FC = () => { const [inputSequence, setInputSequence] = useState(""); @@ -20,15 +21,18 @@ const GetSequenceIds: React.FC = () => { const [refseqId, setRefseqId] = useState(""); const [ga4ghId, setGa4ghId] = useState(""); const [aliases, setAliases] = useState([]); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (inputSequence) { + setIsLoading(true); getSequenceIds(inputSequence).then((sequenceIdsResponse) => { if (sequenceIdsResponse.warnings) { setHelperText("Unrecognized sequence"); setRefseqId(""); setGa4ghId(""); setAliases([]); + setIsLoading(false); } else { if (sequenceIdsResponse.refseq_id) { setRefseqId(sequenceIdsResponse.refseq_id); @@ -40,6 +44,7 @@ const GetSequenceIds: React.FC = () => { setAliases(sequenceIdsResponse.aliases); } setHelperText(""); + setIsLoading(false); } }); } @@ -184,7 +189,9 @@ const GetSequenceIds: React.FC = () => { ); - const renderedIdInfo = ( + const renderedIdInfo = isLoading ? ( + + ) : ( diff --git a/client/src/components/Utilities/GetTranscripts/GetTranscripts.tsx b/client/src/components/Utilities/GetTranscripts/GetTranscripts.tsx index 8787864e..a81ace03 100644 --- a/client/src/components/Utilities/GetTranscripts/GetTranscripts.tsx +++ b/client/src/components/Utilities/GetTranscripts/GetTranscripts.tsx @@ -5,7 +5,6 @@ import { Box, Container, makeStyles, - Paper, Table, TableContainer, Typography, @@ -27,6 +26,7 @@ import { HelpPopover } from "../../main/shared/HelpPopover/HelpPopover"; import HelpTooltip from "../../main/shared/HelpTooltip/HelpTooltip"; import TabHeader from "../../main/shared/TabHeader/TabHeader"; import TabPaper from "../../main/shared/TabPaper/TabPaper"; +import LoadingMessage from "../../main/shared/LoadingMessage/LoadingMessage"; export const GetTranscripts: React.FC = () => { const [gene, setGene] = useState(""); @@ -35,6 +35,8 @@ export const GetTranscripts: React.FC = () => { const [transcripts, setTranscripts] = useState([]); const [transcriptWarnings, setTranscriptWarnings] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const { colorTheme } = useColorTheme(); const useStyles = makeStyles(() => ({ pageContainer: { @@ -51,6 +53,7 @@ export const GetTranscripts: React.FC = () => { overflowY: "scroll", textOverflow: "clip", width: "100%", + height: "100%", }, txAccordionContainer: { width: "90%", @@ -73,13 +76,16 @@ export const GetTranscripts: React.FC = () => { }, [gene]); const handleGet = () => { + setIsLoading(true); getTranscripts(gene).then((transcriptsResponse: GetTranscriptsResponse) => { if (transcriptsResponse.warnings) { setTranscriptWarnings(transcriptsResponse.warnings); setTranscripts([]); + setIsLoading(false); } else { setTranscriptWarnings([]); setTranscripts(transcriptsResponse.transcripts as ManeGeneTranscript[]); + setIsLoading(false); } }); }; @@ -116,6 +122,9 @@ export const GetTranscripts: React.FC = () => { ); const renderTranscripts = () => { + if (isLoading) { + return ; + } if (transcriptWarnings.length > 0) { // TODO more error handling here return {JSON.stringify(transcriptWarnings, null, 2)}; diff --git a/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx b/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx index a35b28a4..7c934451 100644 --- a/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx +++ b/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx @@ -4,18 +4,12 @@ import HelpTooltip from "../HelpTooltip/HelpTooltip"; interface Props { fieldValue: string; - errorText: string; width?: number | undefined; editable?: boolean; onChange?: (event: ChangeEvent) => void; } -const ChromosomeField: React.FC = ({ - fieldValue, - errorText, - width, - onChange, -}) => { +const ChromosomeField: React.FC = ({ fieldValue, width, onChange }) => { const useStyles = makeStyles(() => ({ textField: { height: 38, @@ -41,9 +35,7 @@ const ChromosomeField: React.FC = ({ diff --git a/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx b/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx index fbeb24b4..d15bca10 100644 --- a/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx +++ b/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx @@ -3,8 +3,16 @@ import { TextField, Typography, makeStyles } from "@material-ui/core"; import Autocomplete, { AutocompleteRenderGroupParams, } from "@material-ui/lab/Autocomplete"; -import { getGeneSuggestions } from "../../../../services/main"; -import { SuggestGeneResponse } from "../../../../services/ResponseModels"; +import { + getGeneSuggestions, + getTranscripts, + getTranscriptsForGene, +} from "../../../../services/main"; +import { + GetGeneTranscriptsResponse, + GetTranscriptsResponse, + SuggestGeneResponse, +} from "../../../../services/ResponseModels"; import HelpTooltip from "../HelpTooltip/HelpTooltip"; import { useColorTheme } from "../../../../global/contexts/Theme/ColorThemeContext"; @@ -26,6 +34,8 @@ export type SuggestedGeneOption = { const defaultGeneOption: SuggestedGeneOption = { value: "", type: GeneSuggestionType.none, + chromosome: "", + strand: "", }; interface Props { @@ -50,6 +60,8 @@ interface Props { promptText?: string | undefined; setChromosome?: CallableFunction; setStrand?: CallableFunction; + setTranscripts?: CallableFunction; + setDefaultTranscript?: CallableFunction; } export const GeneAutocomplete: React.FC = ({ @@ -61,6 +73,8 @@ export const GeneAutocomplete: React.FC = ({ promptText, setChromosome, setStrand, + setTranscripts, + setDefaultTranscript, }) => { const existingGeneOption = gene ? { value: gene, type: GeneSuggestionType.symbol } @@ -90,11 +104,42 @@ export const GeneAutocomplete: React.FC = ({ setGeneValue(selection); if (setChromosome) { // substring is to remove identifier from beginning of chromosome (ex: result in NC_000007.14 instead of NCBI:NC_000007.14) - setChromosome(selection.chromosome?.substring(selection.chromosome.indexOf(":") + 1)); + setChromosome( + selection.chromosome?.substring(selection.chromosome.indexOf(":") + 1) + ); } if (setStrand) { setStrand(selection.strand); } + if (setTranscripts) { + // if user is pressing the X button to clear the autocomplete input, set tx info to default + if (selection === defaultGeneOption) { + setTranscripts([]); + if (setDefaultTranscript) { + setDefaultTranscript(""); + } + return; + } + getTranscriptsForGene(selection.value).then( + (transcriptsResponse: GetGeneTranscriptsResponse) => { + const transcripts = transcriptsResponse.transcripts; + const sortedTranscripts = transcripts.sort((a, b) => + a.localeCompare(b) + ); + setTranscripts(sortedTranscripts); + if (setDefaultTranscript) { + // get preferred default transcript from MANE endpoint + getTranscripts(selection.value).then( + (transcriptsResponse: GetTranscriptsResponse) => { + const preferredTx = + transcriptsResponse?.transcripts?.[0].RefSeq_nuc; + setDefaultTranscript(preferredTx || transcripts[0]); + } + ); + } + } + ); + } }; // Update options @@ -102,6 +147,7 @@ export const GeneAutocomplete: React.FC = ({ if (inputValue.value === "") { setGeneText(""); setGeneOptions([]); + updateSelection(defaultGeneOption); setLoading(false); } else { setLoading(true); diff --git a/client/src/components/main/shared/LoadingMessage/LoadingMessage.tsx b/client/src/components/main/shared/LoadingMessage/LoadingMessage.tsx new file mode 100644 index 00000000..9274dbb7 --- /dev/null +++ b/client/src/components/main/shared/LoadingMessage/LoadingMessage.tsx @@ -0,0 +1,24 @@ +import { Box, CircularProgress, makeStyles } from "@material-ui/core"; +import React from "react"; + +interface LoadingMessageProps { + message?: string; +} + +export default function StrandSwitch( + props: LoadingMessageProps +): React.ReactElement { + const loadingMessage = props?.message ? props.message : "Loading..."; + return ( + + {loadingMessage} + + + ); +} diff --git a/client/src/components/main/shared/TabPaper/TabPaper.tsx b/client/src/components/main/shared/TabPaper/TabPaper.tsx index 02ae607c..cb48880e 100644 --- a/client/src/components/main/shared/TabPaper/TabPaper.tsx +++ b/client/src/components/main/shared/TabPaper/TabPaper.tsx @@ -35,7 +35,7 @@ const TabPaper: React.FC = ({ rightColumn: { width: leftColumnWidth ? `${100 - leftColumnWidth}%` : "50%", }, - rightPadder: { padding: "25px" }, + rightPadder: { padding: "25px", height: "100%" }, })); const classes = useStyles(); return ( diff --git a/client/src/components/main/shared/TranscriptField/TranscriptField.tsx b/client/src/components/main/shared/TranscriptField/TranscriptField.tsx index 1c44a9a1..7985ef78 100644 --- a/client/src/components/main/shared/TranscriptField/TranscriptField.tsx +++ b/client/src/components/main/shared/TranscriptField/TranscriptField.tsx @@ -6,7 +6,7 @@ interface Props { fieldValue: string; valueSetter: CallableFunction; errorText: string; - keyHandler: KeyboardEventHandler | undefined; + keyHandler?: KeyboardEventHandler | undefined; width?: number | undefined; } diff --git a/client/src/services/ResponseModels.ts b/client/src/services/ResponseModels.ts index 268d73af..d6d40e28 100644 --- a/client/src/services/ResponseModels.ts +++ b/client/src/services/ResponseModels.ts @@ -538,7 +538,7 @@ export interface ClientTranscriptSegmentElement { gene: Gene; elementGenomicStart?: SequenceLocation | null; elementGenomicEnd?: SequenceLocation | null; - inputType: "genomic_coords_gene" | "genomic_coords_tx" | "exon_coords_tx"; + inputType: "genomic_coords" | "exon_coords"; inputTx?: string | null; inputStrand?: Strand | null; inputGene?: string | null; @@ -768,6 +768,13 @@ export interface GetDomainResponse { warnings?: string[] | null; domain: FunctionalDomain | null; } +/** + * Response model for retrieving list of transcripts for a given gene + */ +export interface GetGeneTranscriptsResponse { + warnings?: string[] | null; + transcripts?: string[]; +} /** * Response model for MANE transcript retrieval endpoint. */ diff --git a/client/src/services/main.tsx b/client/src/services/main.tsx index 0c4edeed..e990be75 100644 --- a/client/src/services/main.tsx +++ b/client/src/services/main.tsx @@ -51,9 +51,13 @@ export enum ElementType { export enum TxElementInputType { default = "default", - gcg = "genomic_coords_gene", - gct = "genomic_coords_tx", - ect = "exon_coords_tx", + gc = "genomic_coords", + ec = "exon_coords", +} + +export enum GenomicInputType { + GENE = "gene", + TRANSCRIPT = "transcript", } export type ClientElementUnion = @@ -137,7 +141,7 @@ export const getTemplatedSequenceElement = async ( return responseJson; }; -export const getTxSegmentElementECT = async ( +export const getTxSegmentElementEC = async ( transcript: string, exonStart: string, exonEnd: string, @@ -160,47 +164,28 @@ export const getTxSegmentElementECT = async ( params.push(`exon_end_offset=${exonEndOffset}`); } const url = - "api/construct/structural_element/tx_segment_ect?" + params.join("&"); - const response = await fetch(url); - const responseJson = await response.json(); - return responseJson; -}; - -export const getTxSegmentElementGCT = async ( - transcript: string, - chromosome: string, - start: string, - end: string -): Promise => { - const params: Array = [ - `transcript=${transcript}`, - `chromosome=${chromosome}`, - ]; - if (start !== "") params.push(`start=${start}`); - if (end !== "") params.push(`end=${end}`); - const url = - "api/construct/structural_element/tx_segment_gct?" + params.join("&"); + "api/construct/structural_element/tx_segment_ec?" + params.join("&"); const response = await fetch(url); const responseJson = await response.json(); return responseJson; }; -export const getTxSegmentElementGCG = async ( +export const getTxSegmentElementGC = async ( gene: string, chromosome: string, + transcript: string, start: string, - end: string, - strand: string + end: string ): Promise => { const params: Array = [ `gene=${gene}`, + `transcript=${transcript}`, `chromosome=${chromosome}`, - `strand=${strand === "+" ? "%2B" : "-"}`, ]; if (start !== "") params.push(`start=${start}`); if (end !== "") params.push(`end=${end}`); const url = - "api/construct/structural_element/tx_segment_gcg?" + params.join("&"); + "api/construct/structural_element/tx_segment_gc?" + params.join("&"); const response = await fetch(url); const responseJson = await response.json(); return responseJson; @@ -252,6 +237,16 @@ export const getTranscripts = async ( return transcriptResponse; }; +export const getTranscriptsForGene = async ( + gene: string +): Promise => { + const response = await fetch( + `/api/utilities/get_transcripts_for_gene?gene=${gene}` + ); + const transcriptResponse = await response.json(); + return transcriptResponse; +}; + export const getExonCoords = async ( chromosome: string, start: string, diff --git a/server/pyproject.toml b/server/pyproject.toml index 93ae38e1..80bda160 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "botocore", "fusor ~= 0.4.2", "cool-seq-tool ~= 0.7.0", - "pydantic == 2.4.2", + "pydantic == 2.4.2", # validation errors with more recent versions, so don't remove this specific pin "gene-normalizer ~= 0.4.0", ] dynamic = ["version"] diff --git a/server/src/curfu/routers/constructors.py b/server/src/curfu/routers/constructors.py index d4d4d94d..d602dd63 100644 --- a/server/src/curfu/routers/constructors.py +++ b/server/src/curfu/routers/constructors.py @@ -43,13 +43,13 @@ def build_gene_element(request: Request, term: str = Query("")) -> GeneElementRe @router.get( - "/api/construct/structural_element/tx_segment_ect", + "/api/construct/structural_element/tx_segment_ec", operation_id="buildTranscriptSegmentElementECT", response_model=TxSegmentElementResponse, response_model_exclude_none=True, tags=[RouteTag.CONSTRUCTORS], ) -async def build_tx_segment_ect( +async def build_tx_segment_ec( request: Request, transcript: str, exon_start: int | None = Query(None), @@ -81,62 +81,28 @@ async def build_tx_segment_ect( @router.get( - "/api/construct/structural_element/tx_segment_gct", - operation_id="buildTranscriptSegmentElementGCT", + "/api/construct/structural_element/tx_segment_gc", + operation_id="buildTranscriptSegmentElementGC", response_model=TxSegmentElementResponse, response_model_exclude_none=True, tags=[RouteTag.CONSTRUCTORS], ) -async def build_tx_segment_gct( - request: Request, - transcript: str, - chromosome: str, - start: int | None = Query(None), - end: int | None = Query(None), -) -> TxSegmentElementResponse: - """Construct Transcript Segment element by providing transcript and genomic - coordinates (chromosome, start, end positions). - \f - :param request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. - :param transcript: transcript accession identifier - :param chromosome: chromosome (TODO how to identify?) - :param start: starting position (TODO assume residue-based?) - :param end: ending position - :return: Pydantic class with TranscriptSegment element if successful, and - warnings otherwise. - """ - tx_segment, warnings = await request.app.state.fusor.transcript_segment_element( - tx_to_genomic_coords=False, - transcript=parse_identifier(transcript), - genomic_ac=parse_identifier(chromosome), - seg_start_genomic=start, - seg_end_genomic=end, - ) - return TxSegmentElementResponse(element=tx_segment, warnings=warnings) - - -@router.get( - "/api/construct/structural_element/tx_segment_gcg", - operation_id="buildTranscriptSegmentElementGCG", - response_model=TxSegmentElementResponse, - response_model_exclude_none=True, - tags=[RouteTag.CONSTRUCTORS], -) -async def build_tx_segment_gcg( +async def build_tx_segment_gc( request: Request, gene: str, chromosome: str, + transcript: str, start: int | None = Query(None), end: int | None = Query(None), ) -> TxSegmentElementResponse: - """Construct Transcript Segment element by providing gene and genomic + """Construct Transcript Segment element by providing gene and/or transcript and genomic coordinates (chromosome, start, end positions). \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. :param gene: gene (TODO how to identify?) :param chromosome: chromosome (TODO how to identify?) + :param transcript: transcript accession identifier :param start: starting position (TODO assume residue-based?) :param end: ending position :return: Pydantic class with TranscriptSegment element if successful, and @@ -148,6 +114,7 @@ async def build_tx_segment_gcg( genomic_ac=parse_identifier(chromosome), seg_start_genomic=start, seg_end_genomic=end, + transcript=transcript, ) return TxSegmentElementResponse(element=tx_segment, warnings=warnings) diff --git a/server/src/curfu/routers/demo.py b/server/src/curfu/routers/demo.py index f2d6646c..01f791f6 100644 --- a/server/src/curfu/routers/demo.py +++ b/server/src/curfu/routers/demo.py @@ -100,7 +100,7 @@ def clientify_structural_element( if element.type == StructuralElementType.TRANSCRIPT_SEGMENT_ELEMENT: nm = tx_segment_nomenclature(element) element_args["nomenclature"] = nm - element_args["inputType"] = "exon_coords_tx" + element_args["inputType"] = "exon_coords" element_args["inputTx"] = element.transcript.split(":")[1] element_args["inputExonStart"] = str(element.exonStart) element_args["inputExonStartOffset"] = str(element.exonStartOffset) diff --git a/server/src/curfu/routers/lookup.py b/server/src/curfu/routers/lookup.py index 12b22766..33d97274 100644 --- a/server/src/curfu/routers/lookup.py +++ b/server/src/curfu/routers/lookup.py @@ -3,7 +3,12 @@ from fastapi import APIRouter, Query, Request from curfu import LookupServiceError -from curfu.schemas import NormalizeGeneResponse, ResponseDict, RouteTag +from curfu.schemas import ( + GetGeneTranscriptsResponse, + NormalizeGeneResponse, + ResponseDict, + RouteTag, +) router = APIRouter() @@ -37,3 +42,28 @@ def normalize_gene(request: Request, term: str = Query("")) -> NormalizeGeneResp response["symbol"] = None response["cased"] = None return NormalizeGeneResponse(**response) + + +@router.get( + "/api/utilities/get_transcripts_for_gene", + operation_id="getTranscriptsFromGene", + response_model=GetGeneTranscriptsResponse, + response_model_exclude_none=True, +) +async def get_transcripts_for_gene(request: Request, gene: str) -> dict: + """Get all transcripts for gene term. + \f + :param Request request: the HTTP request context, supplied by FastAPI. Use to access + FUSOR and UTA-associated tools. + :param str gene: gene term provided by user + :return: Dict containing transcripts if lookup succeeds, or warnings upon failure + """ + normalized = request.app.state.fusor.gene_normalizer.normalize(gene) + symbol = normalized.gene.label + transcripts = await request.app.state.fusor.cool_seq_tool.uta_db.get_transcripts( + gene=symbol + ) + tx_for_gene = list(transcripts.rows_by_key("tx_ac")) + if transcripts.is_empty(): + return {"warnings": [f"No matching transcripts: {gene}"], "transcripts": []} + return {"transcripts": tx_for_gene} diff --git a/server/src/curfu/schemas.py b/server/src/curfu/schemas.py index 007c6d9b..43f92051 100644 --- a/server/src/curfu/schemas.py +++ b/server/src/curfu/schemas.py @@ -51,11 +51,7 @@ class ClientStructuralElement(BaseModel): class ClientTranscriptSegmentElement(TranscriptSegmentElement, ClientStructuralElement): """TranscriptSegment element class used client-side.""" - inputType: ( - Literal["genomic_coords_gene"] - | Literal["genomic_coords_tx"] - | Literal["exon_coords_tx"] - ) + inputType: Literal["genomic_coords"] | Literal["exon_coords"] inputTx: str | None = None inputStrand: Strand | None = None inputGene: str | None = None @@ -250,6 +246,12 @@ class GetTranscriptsResponse(Response): transcripts: list[ManeGeneTranscript] | None +class GetGeneTranscriptsResponse(Response): + """Response model for retrieving list of transcripts for a given gene""" + + transcripts: list[str] = None + + class ServiceInfoResponse(Response): """Response model for service_info endpoint.""" diff --git a/server/tests/integration/test_constructors.py b/server/tests/integration/test_constructors.py index 15e0576b..31aa1b10 100644 --- a/server/tests/integration/test_constructors.py +++ b/server/tests/integration/test_constructors.py @@ -127,21 +127,21 @@ def check_temp_seq_response(response: dict, expected_response: dict): @pytest.mark.asyncio() -async def test_build_tx_segment_ect( +async def test_build_tx_segment_ec( check_response, check_tx_element_response, ntrk1_tx_element_start ): """Test correct functioning of transcript segment element construction using exon coordinates and transcript. """ await check_response( - "/api/construct/structural_element/tx_segment_ect?transcript=NM_002529.3&exon_start=2&exon_start_offset=1", + "/api/construct/structural_element/tx_segment_ec?transcript=NM_002529.3&exon_start=2&exon_start_offset=1", {"element": ntrk1_tx_element_start}, check_tx_element_response, ) # test require exonStart or exonEnd await check_response( - "/api/construct/structural_element/tx_segment_ect?transcript=NM_002529.3", + "/api/construct/structural_element/tx_segment_ec?transcript=NM_002529.3", {"warnings": ["Must provide either `exon_start` or `exon_end`"]}, check_tx_element_response, ) @@ -174,14 +174,14 @@ async def test_build_segment_gct( @pytest.mark.asyncio() -async def test_build_segment_gcg( +async def test_build_segment_gc( check_response, check_tx_element_response, tpm3_tx_g_element ): """Test correct functioning of transcript segment element construction using genomic coordinates and gene name. """ await check_response( - "/api/construct/structural_element/tx_segment_gcg?gene=TPM3&chromosome=NC_000001.11&start=154171416&end=154171417", + "/api/construct/structural_element/tx_segment_gc?gene=TPM3&chromosome=NC_000001.11&start=154171416&end=154171417", {"element": tpm3_tx_g_element}, check_tx_element_response, ) diff --git a/server/tests/integration/test_lookup.py b/server/tests/integration/test_lookup.py index 336aef91..7600756e 100644 --- a/server/tests/integration/test_lookup.py +++ b/server/tests/integration/test_lookup.py @@ -46,6 +46,8 @@ async def test_normalize_gene(async_client: AsyncClient): response = await async_client.get("/api/lookup/gene?term=sdfliuwer") assert response.status_code == 200 assert response.json() == { + "cased": "", + "symbol": "", "term": "sdfliuwer", "warnings": ["Lookup of gene term sdfliuwer failed."], }, "Failed lookup should still respond successfully"