diff --git a/client/src/components/Shared/SearchBar/SearchBar.tsx b/client/src/components/Shared/SearchBar/SearchBar.tsx index 375d4d6a..62a084fd 100644 --- a/client/src/components/Shared/SearchBar/SearchBar.tsx +++ b/client/src/components/Shared/SearchBar/SearchBar.tsx @@ -1,12 +1,19 @@ import './SearchBar.scss'; import Autocomplete from '@mui/material/Autocomplete'; import { + Alert, + AlertTitle, Box, Button, + Checkbox, + FormControl, + FormControlLabel, + InputLabel, MenuItem, Select, SelectChangeEvent, TextField, + Tooltip, } from '@mui/material'; import { useContext, useEffect } from 'react'; import React from 'react'; @@ -15,6 +22,13 @@ import { ActionTypes } from 'stores/Global/reducers'; import { useGetNameSuggestions } from 'hooks/queries/useGetNameSuggestions'; import { SearchTypes } from 'types/types'; import { useGetIsMobile } from 'hooks/shared/useGetIsMobile'; +import HelpIcon from '@mui/icons-material/Help'; + +enum DelimiterTypes { + Comma = 'Comma', + CommaSpace = 'Comma With Space', + TabNewline = 'Tab or Newline', +} type SearchBarProps = { handleSubmit: () => void; @@ -27,6 +41,10 @@ const SearchBar: React.FC = ({ handleSubmit }) => { state.interactionMode ); const [typedSearchTerm, setTypedSearchTerm] = React.useState(''); + const [pastingFromDocument, setPastingFromDocument] = React.useState(false); + const [pastedSearchDelimiter, setPastedSearchDelimiter] = React.useState(''); + const [searchWasPasted, setSearchWasPasted] = React.useState(false); + const typeAheadQuery = useGetNameSuggestions(typedSearchTerm, searchType); let autocompleteOptions = typeAheadQuery?.data?.geneSuggestions || []; const drugAutocompleteOptions = typeAheadQuery?.data?.drugSuggestions || []; @@ -37,9 +55,11 @@ const SearchBar: React.FC = ({ handleSubmit }) => { // support searching for terms that the API may not return (add user's typed term to options if it's not already there) if ( + typedSearchTerm && autocompleteOptions.filter( (option: { suggestion: string }) => option.suggestion === typedSearchTerm - ).length === 0 + ).length === 0 && + typedSearchTerm.trim() !== '' ) { autocompleteOptions = [ { suggestion: typedSearchTerm }, @@ -52,6 +72,8 @@ const SearchBar: React.FC = ({ handleSubmit }) => { const handleAutocompleteChange = (event: any, value: any) => { if (value.length === 0) { setSelectedOptions([]); + // for clearing the paste warning, if applicable + setSearchWasPasted(false); dispatch({ type: ActionTypes.DeleteAllTerms }); } else { setSelectedOptions(value); @@ -76,33 +98,36 @@ const SearchBar: React.FC = ({ handleSubmit }) => { const handleDemoClick = () => { if (searchType === SearchTypes.Gene) { - setSelectedOptions([ - { suggestion: 'FLT1' }, - { suggestion: 'FLT2' }, - { suggestion: 'FLT3' }, - { suggestion: 'STK1' }, - { suggestion: 'MM1' }, - { suggestion: 'AQP1' }, - { suggestion: 'LOC100508755' }, - { suggestion: 'FAKE1' }, - ]); + const geneDemoList = [ + 'FLT1', + 'FLT2', + 'FLT3', + 'STK1', + 'MM1', + 'AQP1', + 'LOC100508755', + 'FAKE1', + ]; + setSelectedOptions(convertToDropdownOptions(geneDemoList)); } else if (searchType === SearchTypes.Drug) { - setSelectedOptions([ - { suggestion: 'SUNITINIB' }, - { suggestion: 'ZALCITABINE' }, - { suggestion: 'TRASTUZUMAB' }, - { suggestion: 'NOTREAL' }, - ]); + const drugDemoList = [ + 'SUNITINIB', + 'ZALCITABINE', + 'TRASTUZUMAB', + 'NOTREAL', + ]; + setSelectedOptions(convertToDropdownOptions(drugDemoList)); } else if (searchType === SearchTypes.Categories) { - setSelectedOptions([ - { suggestion: 'HER2' }, - { suggestion: 'ERBB2' }, - { suggestion: 'PTGDR' }, - { suggestion: 'EGFR' }, - { suggestion: 'RECK' }, - { suggestion: 'KCNMA1' }, - { suggestion: 'MM1' }, - ]); + const categoriesDemoList = [ + 'HER2', + 'ERBB2', + 'PTGDR', + 'EGFR', + 'RECK', + 'KCNMA1', + 'MM1', + ]; + setSelectedOptions(convertToDropdownOptions(categoriesDemoList)); } }; const handleSearchClick = () => { @@ -125,9 +150,77 @@ const SearchBar: React.FC = ({ handleSubmit }) => { } }, [selectedOptions]); + const convertToDropdownOptions = (options: string[]) => { + return options.map((item: string) => { + return { suggestion: item.trim() }; + }); + }; + + const handlePaste = (event: any) => { + let pastedText = event.clipboardData.getData('text'); + let pastedOptions: any[] = convertToDropdownOptions([pastedText]); + + const commaSepOptions = pastedText.split(','); + + if (pastedSearchDelimiter === DelimiterTypes.Comma) { + pastedOptions = convertToDropdownOptions(commaSepOptions); + } else if (pastedSearchDelimiter === DelimiterTypes.CommaSpace) { + const commaSpaceSepOptions = pastedText.split(', '); + pastedOptions = convertToDropdownOptions(commaSpaceSepOptions); + } else if (pastedSearchDelimiter === DelimiterTypes.TabNewline) { + const whitespaceRegex = /[\t\n\r\f\v]/; + const whitespaceSepOptions = pastedText.split(whitespaceRegex); + pastedOptions = convertToDropdownOptions(whitespaceSepOptions); + } else { + pastedOptions = convertToDropdownOptions(commaSepOptions); + } + setSearchWasPasted(true); + // make sure we persist the search terms already entered, combine any pre-existing search terms with the new pasted options + const newSearchOptions = selectedOptions.concat(pastedOptions); + // remove any duplicated terms (need to iterate through only the terms since objects are never equivalent in js, even if the contents are the same) + const uniqueSearchTerms = [ + ...new Set(newSearchOptions.map((option) => option.suggestion)), + ]; + setSelectedOptions(convertToDropdownOptions(uniqueSearchTerms)); + // we don't want the code to also run what's in onInputChange for the Autocomplete since everything is handled here + event.preventDefault(); + }; + + const handleCheckboxSelect = (event: any) => { + setPastingFromDocument(event.target.checked); + // reset the selected delimiter and searchWasPasted, to avoid potential weird behaviors if a user deselects the checkbox + setPastedSearchDelimiter(''); + setSearchWasPasted(false); + }; + + const handleDelimiterChange = (event: any) => { + setPastedSearchDelimiter(event.target.value as string); + }; + + const pasteAlert = + searchWasPasted && pastedSearchDelimiter === '' ? ( + + + Verify your search terms +

+ It looks like you pasted search terms. We have defaulted the + delimiter to comma-separated terms. +

+

+ If this is incorrect or you would like to use a different delimiter, + make sure to check the “Bulk search” option below and select a + delimiter from the drop down. +

+
+
+ ) : ( + <> + ); + return ( <> + {pasteAlert} + + {DelimiterTypes.Comma} + + + {DelimiterTypes.CommaSpace} + + + {DelimiterTypes.TabNewline} + + + + + );