Skip to content

Commit

Permalink
Autocomplete updates (#245)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsstevenson authored Sep 28, 2023
1 parent 4a364a6 commit 85ba7ff
Show file tree
Hide file tree
Showing 19 changed files with 529 additions and 380 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ const StructuralElementInputAccordion: React.FC<
>
{validated ? (
<Tooltip title="Validation successful">
<CheckCircleIcon className="input-correct" style={{ color: green[500] }} />
<CheckCircleIcon
className="input-correct"
style={{ color: green[500] }}
/>
</Tooltip>
) : (
<Tooltip title="Invalid component">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,8 @@ const TxSegmentCompInput: React.FC<TxSegmentElementInputProps> = ({
tooltipDirection="bottom"
geneText={txGeneText}
setGeneText={setTxGeneText}
style={{ width: 125 }}
setChromosome={setTxChrom}
setStrand={setTxStrand}
/>
</Box>
{genomicCoordinateInfo}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ const GetCoordinates: React.FC = () => {
setGene={setGene}
geneText={geneText}
setGeneText={setGeneText}
style={{ width: 125 }}
setChromosome={setChromosome}
setStrand={setStrand}
/>
</Box>
{genomicCoordinateInfo}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/main/App/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ h3 {
.MuiDrawer-paper {
width: 160px;
overflow-x: hidden !important;
background-color: #18252B;
background-color: #18252b;
color: white !important;
}
}
Expand Down
5 changes: 4 additions & 1 deletion client/src/components/main/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,10 @@ const App = (): JSX.Element => {
* readability.
*/
const fusionIsEmpty = () => {
if (fusion?.structural_elements.length === 0 && fusion?.regulatory_element === undefined) {
if (
fusion?.structural_elements.length === 0 &&
fusion?.regulatory_element === undefined
) {
return true;
} else if (fusion.structural_elements.length > 0) {
return false;
Expand Down
179 changes: 111 additions & 68 deletions client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React, { useState, useEffect } from "react";
import { TextField, Typography } from "@material-ui/core";
import Autocomplete from "@material-ui/lab/Autocomplete";
import { getGeneId, getGeneSuggestions } from "../../../../services/main";
import { CSSProperties } from "@material-ui/core/styles/withStyles";
import {
NormalizeGeneResponse,
SuggestGeneResponse,
} from "../../../../services/ResponseModels";
import React, { useState, useEffect, ReactNode } from "react";
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 HelpTooltip from "../HelpTooltip/HelpTooltip";
import { useColorTheme } from "../../../../global/contexts/Theme/ColorThemeContext";

export enum GeneSuggestionType {
conceptId = "Concept ID",
Expand All @@ -16,7 +15,13 @@ export enum GeneSuggestionType {
prevSymbol = "Previous Symbol",
none = "",
}
export type SuggestedGeneOption = { value: string; type: GeneSuggestionType };

export type SuggestedGeneOption = {
value: string;
type: GeneSuggestionType | string;
chromosome?: string;
strand?: string;
};

const defaultGeneOption: SuggestedGeneOption = {
value: "",
Expand All @@ -43,6 +48,8 @@ interface Props {
| "top-start"
| undefined;
promptText?: string | undefined;
setChromosome?: CallableFunction;
setStrand?: CallableFunction;
}

export const GeneAutocomplete: React.FC<Props> = ({
Expand All @@ -52,13 +59,26 @@ export const GeneAutocomplete: React.FC<Props> = ({
setGeneText,
tooltipDirection,
promptText,
setChromosome,
setStrand,
}) => {
const existingGeneOption = gene
? { value: gene, type: GeneSuggestionType.symbol }
: defaultGeneOption;
const [geneOptions, setGeneOptions] = useState<SuggestedGeneOption[]>([]);
const [geneValue, setGeneValue] = useState(existingGeneOption);
const [inputValue, setInputValue] = useState(existingGeneOption);
const [loading, setLoading] = useState(false);

const { colorTheme } = useColorTheme();
const useStyles = makeStyles(() => ({
autocompleteGroupHeader: {
paddingLeft: "8px",
color: colorTheme["--dark-gray"],
fontSizeAdjust: "0.5",
},
}));
const classes = useStyles();

/**
* Simple wrapper around state setters to ensure updates to local selected value are reflected
Expand All @@ -68,30 +88,32 @@ export const GeneAutocomplete: React.FC<Props> = ({
const updateSelection = (selection: SuggestedGeneOption) => {
setGene(selection.value);
setGeneValue(selection);
if (setChromosome) {
setChromosome(selection.chromosome);
}
if (setStrand) {
setStrand(selection.strand);
}
};

// Update options
useEffect(() => {
if (inputValue.value === "") {
setGeneText("");
setGeneOptions([]);
setLoading(false);
} else {
const delayDebounce = setTimeout(() => {
getGeneSuggestions(inputValue.value).then((suggestResponseJson) => {
if (
!suggestResponseJson.symbols &&
!suggestResponseJson.prev_symbols &&
!suggestResponseJson.aliases
) {
setGeneText("Unrecognized term");
setGeneOptions([]);
} else {
setGeneText("");
setGeneOptions(buildOptions(suggestResponseJson));
}
});
}, 300);
return () => clearTimeout(delayDebounce);
setLoading(true);
getGeneSuggestions(inputValue.value).then((suggestResponseJson) => {
setLoading(false);
if (suggestResponseJson.matches_count === 0) {
setGeneText("Unrecognized term");
setGeneOptions([]);
} else {
setGeneText("");
setGeneOptions(buildOptions(suggestResponseJson, inputValue.value));
}
});
}
}, [inputValue]);

Expand All @@ -103,71 +125,94 @@ export const GeneAutocomplete: React.FC<Props> = ({
}, [gene]);

/**
* Attempt exact match for entered text. Should be called if user-submitted text
* isn't specific enough to narrow options down to a reasonable number (the
* `MAX_SUGGESTIONS` value set server-side), in case their entered value
* happens to match a real gene term.
* No return value, but updates dropdown options if successful.
* Generate group HTML element. Needed to properly display text about # of other possible completions.
* @param params group object processed by autocomplete
* @returns group node to render
*/
const tryExactMatch = (input: string) => {
getGeneId(input).then((geneResponseJson: NormalizeGeneResponse) => {
// just provide entered term, but correctly-cased
setGeneText("");
if (geneResponseJson.cased) {
setGeneOptions([
{
value: geneResponseJson.cased,
type: geneResponseJson.cased.match(/^\w[^:]*:.+$/)
? GeneSuggestionType.conceptId
: GeneSuggestionType.symbol,
},
]);
}
});
const makeGroup = (params: AutocompleteRenderGroupParams): ReactNode => {
const children = params.group.includes("possible") ? [] : params.children;
const groupElement = (
<div key={params.key} className={classes.autocompleteGroupHeader}>
{params.group}
</div>
);
return [groupElement, children];
};

// if geneOptions is empty, try an exact match (note: keep this useEffect separately, as we want to do this after all of the autocomplete lookups)
useEffect(() => {
if (!geneOptions.length) {
tryExactMatch(inputValue.value);
}
}, [geneOptions]);

/**
* Construct options for use in MUI Autocomplete GroupBy
* @param suggestResponse response from suggestions API received from server
* @returns array of option objects
*/
const buildOptions = (
suggestResponse: SuggestGeneResponse
suggestResponse: SuggestGeneResponse,
inputValue: string
): SuggestedGeneOption[] => {
const options: SuggestedGeneOption[] = [];
if (suggestResponse.symbols) {
suggestResponse.symbols.map((suggestion) =>
options.push({ value: suggestion[0], type: GeneSuggestionType.symbol })
if (suggestResponse.concept_id) {
suggestResponse.concept_id.map((suggestion) =>
options.push({
value: suggestion[0],
type: GeneSuggestionType.conceptId,
chromosome: suggestion[3],
strand: suggestion[4],
})
);
}
if (suggestResponse.symbol) {
suggestResponse.symbol.map((suggestion) =>
options.push({
value: suggestion[0],
type: GeneSuggestionType.symbol,
chromosome: suggestion[3],
strand: suggestion[4],
})
);
}
if (suggestResponse.prev_symbols) {
suggestResponse.prev_symbols.map((suggestion) =>
options.push({
value: suggestion[0],
type: GeneSuggestionType.prevSymbol,
chromosome: suggestion[3],
strand: suggestion[4],
})
);
}
if (suggestResponse.aliases) {
suggestResponse.aliases.map((suggestion) =>
options.push({ value: suggestion[0], type: GeneSuggestionType.alias })
options.push({
value: suggestion[0],
type: GeneSuggestionType.alias,
chromosome: suggestion[3],
strand: suggestion[4],
})
);
}
// slightly hack-y way to insert message about number of possible options: create an option group
// with the message as the group title, and then in `makeGroup()`, remove all of its child elements.
// `value` needs to be set to `inputValue` (or another valid completion of user text) for the autocomplete object
// to render the group at all
if (suggestResponse.warnings) {
suggestResponse.warnings.map((warn: string) => {
if (warn.startsWith("Exceeds max matches")) {
const maxExceededMsg =
options.length > 0
? `+ ${suggestResponse.matches_count} possible options`
: `${suggestResponse.matches_count} possible options`;
options.push({
value: inputValue,
type: maxExceededMsg,
});
}
});
}
return options;
};

return (
<Autocomplete
debug
loading={loading}
value={geneValue}
style={{ minWidth: "150px" }}
clearOnBlur={false}
clearOnEscape
disableClearable={inputValue.value === ""}
onChange={(_, newValue) => {
if (newValue) {
updateSelection(newValue);
Expand All @@ -181,13 +226,11 @@ export const GeneAutocomplete: React.FC<Props> = ({
}}
options={geneOptions}
groupBy={(option) => (option ? option.type : "")}
renderGroup={makeGroup}
getOptionLabel={(option) => (option.value ? option.value : "")}
getOptionSelected={(option, selected) => {
return option.value === selected.value;
}}
clearOnBlur={false}
clearOnEscape
disableClearable={inputValue.value === ""}
renderInput={(params) => (
<HelpTooltip
placement={tooltipDirection}
Expand All @@ -207,7 +250,7 @@ export const GeneAutocomplete: React.FC<Props> = ({
variant="standard"
label={promptText ? promptText : "Gene Symbol"}
margin="dense"
style={{minWidth: "250px !important"}}
style={{ minWidth: "250px !important" }}
error={geneText !== ""}
helperText={geneText ? geneText : null}
/>
Expand Down
2 changes: 1 addition & 1 deletion client/src/global/styles/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const theme = createTheme({
secondary: {
main: COLORTHEMES.light["--secondary"],
contrastText: COLORTHEMES.light["--white"],
}
},
},
});

Expand Down
Loading

0 comments on commit 85ba7ff

Please sign in to comment.