Skip to content

Commit

Permalink
Nickakhmetov/HMP-389 Fix autocomplete selection behavior (#3252)
Browse files Browse the repository at this point in the history
* Prevent already-selected options from being suggested by autocomplete component

* Fix "getOptionLabel returned undefined" errors in console

* supress "invalid value" warning when selected genes are no longer in options list

* Convert `AutocompleteEntity` to TypeScript

* Fix alignment of genomic modality select dropdown

* prettier fix
  • Loading branch information
NickAkhmetov authored Sep 13, 2023
1 parent b1683d2 commit 49a5971
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 99 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-HMP-389.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Prevent already-selected options from being suggested by autocomplete component.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useState, useEffect } from 'react';

import Autocomplete from '@mui/material/Autocomplete';
import TextField, { TextFieldProps } from '@mui/material/TextField';
import Chip from '@mui/material/Chip';
import { useAutocompleteQuery } from './hooks';
import { AutocompleteQueryResponse } from './types';

function buildHelperText(entity: string): string {
return `Multiple ${entity} are allowed and only 'AND' queries are supported.`;
}

const labelAndHelperTextProps: Record<string, Pick<TextFieldProps, 'label' | 'helperText'>> = {
genes: { label: 'Gene Symbol', helperText: buildHelperText('gene symbols') },
proteins: { label: 'Protein', helperText: buildHelperText('proteins') },
};

type AutocompleteEntityProps = {
targetEntity: string;
setter: (value: string[]) => void;
};

function AutocompleteEntity({ targetEntity, setter }: AutocompleteEntityProps) {
const [substring, setSubstring] = useState('');
const [selectedOptions, setSelectedOptions] = useState<AutocompleteQueryResponse>([]);

useEffect(() => {
// Unwrap selected options and pass to setter to keep values in sync
setter(selectedOptions.map((match) => match.full));
}, [selectedOptions, setter]);

useEffect(() => {
// Reset selected options and substring when target entity changes
setSubstring('');
setSelectedOptions([]);
}, [targetEntity]);

const { data, isLoading } = useAutocompleteQuery({ targetEntity, substring });

// Include currently selected options to avoid invalid value errors in console
const options = selectedOptions.concat(data || []);

function handleChange({ target: { value } }: React.ChangeEvent<HTMLInputElement>) {
setSubstring(value);
}

return (
<Autocomplete
value={selectedOptions}
options={options}
multiple
filterSelectedOptions
getOptionLabel={(option) => option.full}
isOptionEqualToValue={(option, value) => option.full === value.full}
loading={isLoading}
renderOption={(props, option) => (
<li {...props}>
{option.pre}
<b>{option.match}</b>
{option.post}
</li>
)}
renderTags={(value, getTagProps) =>
value.map((option, index) => {
return <Chip label={option.full} {...getTagProps({ index })} key={option.full} />;
})
}
onChange={(_, value) => {
setSelectedOptions(value);
}}
renderInput={({ InputLabelProps, ...params }) => (
<TextField
InputLabelProps={{ shrink: true, ...InputLabelProps }}
{...labelAndHelperTextProps[targetEntity]}
placeholder={`Select ${targetEntity} to query`}
value={substring}
name="substring"
variant="outlined"
onChange={handleChange}
{...params}
/>
)}
/>
);
}

export default AutocompleteEntity;
19 changes: 19 additions & 0 deletions context/app/static/js/components/cells/AutocompleteEntity/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import useSWR from 'swr';
import CellsService from '../CellsService';
import type { AutocompleteQueryKey, AutocompleteQueryResponse } from './types';

const cellsService = new CellsService();

const cellsServiceFetcher = async ({
targetEntity,
substring,
}: AutocompleteQueryKey): Promise<AutocompleteQueryResponse> => {
if (!substring) {
return [];
}
return cellsService.searchBySubstring({ targetEntity, substring });
};

export function useAutocompleteQuery(queryKey: AutocompleteQueryKey) {
return useSWR<AutocompleteQueryResponse, Error>(queryKey, cellsServiceFetcher);
}
13 changes: 13 additions & 0 deletions context/app/static/js/components/cells/AutocompleteEntity/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface AutocompleteQueryKey {
targetEntity: string;
substring: string;
}

export interface AutocompleteResult {
match: string;
full: string;
pre: string;
post: string;
}

export type AutocompleteQueryResponse = AutocompleteResult[];
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,7 @@ function DatasetsSelectedByExpression({ completeStep, runQueryButtonRef }) {

return (
<StyledDiv>
<AutocompleteEntity
targetEntity={`${queryType}s`}
setter={setCellVariableNames}
cellVariableNames={cellVariableNames}
setCellVariableNames={setCellVariableNames}
/>
<AutocompleteEntity targetEntity={`${queryType}s`} setter={setCellVariableNames} />
{queryType === 'gene' && (
<StyledTextField
id="modality-select"
Expand All @@ -51,7 +46,7 @@ function DatasetsSelectedByExpression({ completeStep, runQueryButtonRef }) {
MenuProps: {
anchorOrigin: {
vertical: 'bottom',
horizontal: 'left',
horizontal: 'center',
},
getContentAnchorEl: null,
},
Expand Down

0 comments on commit 49a5971

Please sign in to comment.