mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Clickable search filter options (#4618)
This commit is contained in:
		
							parent
							
								
									f55c67fe2e
								
							
						
					
					
						commit
						caff040a88
					
				| @ -79,7 +79,7 @@ export const Search = ({ | ||||
|     debounceTime = 200, | ||||
| }: ISearchProps) => { | ||||
|     const searchInputRef = useRef<HTMLInputElement>(null); | ||||
|     const suggestionsRef = useRef<HTMLInputElement>(null); | ||||
|     const searchContainerRef = useRef<HTMLInputElement>(null); | ||||
|     const [showSuggestions, setShowSuggestions] = useState(false); | ||||
|     const hideSuggestions = () => { | ||||
|         setShowSuggestions(false); | ||||
| @ -112,10 +112,11 @@ export const Search = ({ | ||||
|     }); | ||||
|     const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; | ||||
| 
 | ||||
|     useOnClickOutside([searchInputRef, suggestionsRef], hideSuggestions); | ||||
|     useOnClickOutside([searchContainerRef], hideSuggestions); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledContainer | ||||
|             ref={searchContainerRef} | ||||
|             style={containerStyles} | ||||
|             active={expandable && showSuggestions} | ||||
|         > | ||||
| @ -148,7 +149,8 @@ export const Search = ({ | ||||
|                             <Tooltip title="Clear search query" arrow> | ||||
|                                 <IconButton | ||||
|                                     size="small" | ||||
|                                     onClick={() => { | ||||
|                                     onClick={e => { | ||||
|                                         e.stopPropagation(); // prevent outside click from the lazily added element
 | ||||
|                                         onSearchChange(''); | ||||
|                                         searchInputRef.current?.focus(); | ||||
|                                     }} | ||||
| @ -164,11 +166,13 @@ export const Search = ({ | ||||
|             <ConditionallyRender | ||||
|                 condition={Boolean(hasFilters) && showSuggestions} | ||||
|                 show={ | ||||
|                     <div ref={suggestionsRef}> | ||||
|                         <SearchSuggestions | ||||
|                             getSearchContext={getSearchContext!} | ||||
|                         /> | ||||
|                     </div> | ||||
|                     <SearchSuggestions | ||||
|                         onSuggestion={suggestion => { | ||||
|                             onSearchChange(suggestion); | ||||
|                             searchInputRef.current?.focus(); | ||||
|                         }} | ||||
|                         getSearchContext={getSearchContext!} | ||||
|                     /> | ||||
|                 } | ||||
|             /> | ||||
|         </StyledContainer> | ||||
|  | ||||
| @ -12,10 +12,7 @@ const StyledHeader = styled('span')(({ theme }) => ({ | ||||
| })); | ||||
| 
 | ||||
| const StyledCode = styled('span')(({ theme }) => ({ | ||||
|     backgroundColor: theme.palette.background.elevation2, | ||||
|     color: theme.palette.text.primary, | ||||
|     padding: theme.spacing(0, 0.5), | ||||
|     borderRadius: theme.spacing(0.5), | ||||
| })); | ||||
| 
 | ||||
| interface ISearchDescriptionProps { | ||||
|  | ||||
| @ -12,6 +12,7 @@ const StyledCode = styled('span')(({ theme }) => ({ | ||||
|     color: theme.palette.text.primary, | ||||
|     padding: theme.spacing(0.2, 1), | ||||
|     borderRadius: theme.spacing(0.5), | ||||
|     cursor: 'pointer', | ||||
| })); | ||||
| 
 | ||||
| const StyledFilterHint = styled('p')(({ theme }) => ({ | ||||
| @ -21,11 +22,18 @@ const StyledFilterHint = styled('p')(({ theme }) => ({ | ||||
| interface ISearchInstructionsProps { | ||||
|     filters: any[]; | ||||
|     searchableColumnsString: string; | ||||
|     onClick: (instruction: string) => void; | ||||
| } | ||||
| 
 | ||||
| const firstFilterOption = (filter: { name: string; options: string[] }) => | ||||
|     `${filter.name}:${filter.options[0]}`; | ||||
| const secondFilterOption = (filter: { name: string; options: string[] }) => | ||||
|     `${filter.name}:${filter.options.slice(0, 2).join(',')}`; | ||||
| 
 | ||||
| export const SearchInstructions: VFC<ISearchInstructionsProps> = ({ | ||||
|     filters, | ||||
|     searchableColumnsString, | ||||
|     onClick, | ||||
| }) => { | ||||
|     return ( | ||||
|         <> | ||||
| @ -41,17 +49,22 @@ export const SearchInstructions: VFC<ISearchInstructionsProps> = ({ | ||||
|             {filters.map(filter => ( | ||||
|                 <StyledFilterHint key={filter.name}> | ||||
|                     {filter.header}:{' '} | ||||
|                     <StyledCode> | ||||
|                         {filter.name}:{filter.options[0]} | ||||
|                     <StyledCode | ||||
|                         onClick={() => onClick(firstFilterOption(filter))} | ||||
|                     > | ||||
|                         {firstFilterOption(filter)} | ||||
|                     </StyledCode> | ||||
|                     <ConditionallyRender | ||||
|                         condition={filter.options.length > 1} | ||||
|                         show={ | ||||
|                             <> | ||||
|                                 {' or '} | ||||
|                                 <StyledCode> | ||||
|                                     {filter.name}: | ||||
|                                     {filter.options.slice(0, 2).join(',')} | ||||
|                                 <StyledCode | ||||
|                                     onClick={() => { | ||||
|                                         onClick(secondFilterOption(filter)); | ||||
|                                     }} | ||||
|                                 > | ||||
|                                     {secondFilterOption(filter)} | ||||
|                                 </StyledCode> | ||||
|                             </> | ||||
|                         } | ||||
|  | ||||
| @ -34,7 +34,15 @@ const searchContext = { | ||||
| }; | ||||
| 
 | ||||
| test('displays search and filter instructions when no search value is provided', () => { | ||||
|     render(<SearchSuggestions getSearchContext={() => searchContext} />); | ||||
|     let recordedSuggestion = ''; | ||||
|     render( | ||||
|         <SearchSuggestions | ||||
|             onSuggestion={suggestion => { | ||||
|                 recordedSuggestion = suggestion; | ||||
|             }} | ||||
|             getSearchContext={() => searchContext} | ||||
|         /> | ||||
|     ); | ||||
| 
 | ||||
|     expect(screen.getByText(/Filter your results by:/i)).toBeInTheDocument(); | ||||
| 
 | ||||
| @ -47,11 +55,15 @@ test('displays search and filter instructions when no search value is provided', | ||||
|     expect( | ||||
|         screen.getByText(/Combine filters and search./i) | ||||
|     ).toBeInTheDocument(); | ||||
| 
 | ||||
|     screen.getByText(/environment:"dev env",pre-prod/i).click(); | ||||
|     expect(recordedSuggestion).toBe('environment:"dev env",pre-prod'); | ||||
| }); | ||||
| 
 | ||||
| test('displays search and filter instructions when search value is provided', () => { | ||||
|     render( | ||||
|         <SearchSuggestions | ||||
|             onSuggestion={() => {}} | ||||
|             getSearchContext={() => ({ | ||||
|                 ...searchContext, | ||||
|                 searchValue: 'Title', | ||||
| @ -67,8 +79,12 @@ test('displays search and filter instructions when search value is provided', () | ||||
| }); | ||||
| 
 | ||||
| test('displays search and filter instructions when filter value is provided', () => { | ||||
|     let recordedSuggestion = ''; | ||||
|     render( | ||||
|         <SearchSuggestions | ||||
|             onSuggestion={suggestion => { | ||||
|                 recordedSuggestion = suggestion; | ||||
|             }} | ||||
|             getSearchContext={() => ({ | ||||
|                 ...searchContext, | ||||
|                 searchValue: 'environment:prod', | ||||
| @ -84,4 +100,9 @@ test('displays search and filter instructions when filter value is provided', () | ||||
|     expect( | ||||
|         screen.getByText(/Combine filters and search./i) | ||||
|     ).toBeInTheDocument(); | ||||
|     expect(screen.getByText(/environment:"dev env"/i)).toBeInTheDocument(); | ||||
|     expect(screen.getByText(/Title A/i)).toBeInTheDocument(); | ||||
| 
 | ||||
|     screen.getByText(/Title A/i).click(); | ||||
|     expect(recordedSuggestion).toBe('environment:"dev env" Title A'); | ||||
| }); | ||||
|  | ||||
| @ -45,10 +45,12 @@ const StyledCode = styled('span')(({ theme }) => ({ | ||||
|     color: theme.palette.text.primary, | ||||
|     padding: theme.spacing(0.2, 0.5), | ||||
|     borderRadius: theme.spacing(0.5), | ||||
|     cursor: 'pointer', | ||||
| })); | ||||
| 
 | ||||
| interface SearchSuggestionsProps { | ||||
|     getSearchContext: () => IGetSearchContextOutput; | ||||
|     onSuggestion: (suggestion: string) => void; | ||||
| } | ||||
| 
 | ||||
| const quote = (item: string) => (item.includes(' ') ? `"${item}"` : item); | ||||
| @ -57,14 +59,10 @@ const randomIndex = (arr: any[]) => Math.floor(Math.random() * arr.length); | ||||
| 
 | ||||
| export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({ | ||||
|     getSearchContext, | ||||
|     onSuggestion, | ||||
| }) => { | ||||
|     const searchContext = getSearchContext(); | ||||
| 
 | ||||
|     const randomRow = useMemo( | ||||
|         () => randomIndex(searchContext.data), | ||||
|         [searchContext.data] | ||||
|     ); | ||||
| 
 | ||||
|     const filters = getFilterableColumns(searchContext.columns) | ||||
|         .map(column => { | ||||
|             const filterOptions = searchContext.data.map(row => | ||||
| @ -82,8 +80,7 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({ | ||||
|                 name: column.filterName, | ||||
|                 header: column.Header ?? column.filterName, | ||||
|                 options, | ||||
|                 suggestedOption: | ||||
|                     options[randomRow] ?? `example-${column.filterName}`, | ||||
|                 suggestedOption: options[0] ?? `example-${column.filterName}`, | ||||
|                 values: getFilterValues( | ||||
|                     column.filterName, | ||||
|                     searchContext.searchValue | ||||
| @ -102,12 +99,13 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({ | ||||
| 
 | ||||
|     const suggestedTextSearch = | ||||
|         searchContext.data.length && searchableColumns.length | ||||
|             ? getColumnValues( | ||||
|                   searchableColumns[0], | ||||
|                   searchContext.data[randomRow] | ||||
|               ) | ||||
|             ? getColumnValues(searchableColumns[0], searchContext.data[0]) | ||||
|             : 'example-search-text'; | ||||
| 
 | ||||
|     const selectedFilter = filters.map( | ||||
|         filter => `${filter.name}:${filter.suggestedOption}` | ||||
|     )[0]; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledPaper className="dropdown-outline"> | ||||
|             <StyledBox> | ||||
| @ -130,6 +128,7 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({ | ||||
|                                 searchableColumnsString={ | ||||
|                                     searchableColumnsString | ||||
|                                 } | ||||
|                                 onClick={onSuggestion} | ||||
|                             /> | ||||
|                         } | ||||
|                     /> | ||||
| @ -141,12 +140,12 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({ | ||||
|                     condition={filters.length > 0} | ||||
|                     show="Combine filters and search: " | ||||
|                 /> | ||||
|                 <StyledCode> | ||||
|                     {filters.map(filter => ( | ||||
|                         <span key={filter.name}> | ||||
|                             {filter.name}:{filter.suggestedOption}{' '} | ||||
|                         </span> | ||||
|                     ))} | ||||
|                 <StyledCode | ||||
|                     onClick={() => | ||||
|                         onSuggestion(selectedFilter + ' ' + suggestedTextSearch) | ||||
|                     } | ||||
|                 > | ||||
|                     <span key={selectedFilter}>{selectedFilter}</span>{' '} | ||||
|                     <span>{suggestedTextSearch}</span> | ||||
|                 </StyledCode> | ||||
|             </Box> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user