1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-09 01:17:06 +02:00

feat: Clickable search filter options (#4618)

This commit is contained in:
Mateusz Kwasniewski 2023-09-06 12:50:42 +02:00 committed by GitHub
parent f55c67fe2e
commit caff040a88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 68 additions and 34 deletions

View File

@ -79,7 +79,7 @@ export const Search = ({
debounceTime = 200, debounceTime = 200,
}: ISearchProps) => { }: ISearchProps) => {
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const suggestionsRef = useRef<HTMLInputElement>(null); const searchContainerRef = useRef<HTMLInputElement>(null);
const [showSuggestions, setShowSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false);
const hideSuggestions = () => { const hideSuggestions = () => {
setShowSuggestions(false); setShowSuggestions(false);
@ -112,10 +112,11 @@ export const Search = ({
}); });
const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`;
useOnClickOutside([searchInputRef, suggestionsRef], hideSuggestions); useOnClickOutside([searchContainerRef], hideSuggestions);
return ( return (
<StyledContainer <StyledContainer
ref={searchContainerRef}
style={containerStyles} style={containerStyles}
active={expandable && showSuggestions} active={expandable && showSuggestions}
> >
@ -148,7 +149,8 @@ export const Search = ({
<Tooltip title="Clear search query" arrow> <Tooltip title="Clear search query" arrow>
<IconButton <IconButton
size="small" size="small"
onClick={() => { onClick={e => {
e.stopPropagation(); // prevent outside click from the lazily added element
onSearchChange(''); onSearchChange('');
searchInputRef.current?.focus(); searchInputRef.current?.focus();
}} }}
@ -164,11 +166,13 @@ export const Search = ({
<ConditionallyRender <ConditionallyRender
condition={Boolean(hasFilters) && showSuggestions} condition={Boolean(hasFilters) && showSuggestions}
show={ show={
<div ref={suggestionsRef}>
<SearchSuggestions <SearchSuggestions
onSuggestion={suggestion => {
onSearchChange(suggestion);
searchInputRef.current?.focus();
}}
getSearchContext={getSearchContext!} getSearchContext={getSearchContext!}
/> />
</div>
} }
/> />
</StyledContainer> </StyledContainer>

View File

@ -12,10 +12,7 @@ const StyledHeader = styled('span')(({ theme }) => ({
})); }));
const StyledCode = styled('span')(({ theme }) => ({ const StyledCode = styled('span')(({ theme }) => ({
backgroundColor: theme.palette.background.elevation2,
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: theme.spacing(0, 0.5),
borderRadius: theme.spacing(0.5),
})); }));
interface ISearchDescriptionProps { interface ISearchDescriptionProps {

View File

@ -12,6 +12,7 @@ const StyledCode = styled('span')(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: theme.spacing(0.2, 1), padding: theme.spacing(0.2, 1),
borderRadius: theme.spacing(0.5), borderRadius: theme.spacing(0.5),
cursor: 'pointer',
})); }));
const StyledFilterHint = styled('p')(({ theme }) => ({ const StyledFilterHint = styled('p')(({ theme }) => ({
@ -21,11 +22,18 @@ const StyledFilterHint = styled('p')(({ theme }) => ({
interface ISearchInstructionsProps { interface ISearchInstructionsProps {
filters: any[]; filters: any[];
searchableColumnsString: string; 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> = ({ export const SearchInstructions: VFC<ISearchInstructionsProps> = ({
filters, filters,
searchableColumnsString, searchableColumnsString,
onClick,
}) => { }) => {
return ( return (
<> <>
@ -41,17 +49,22 @@ export const SearchInstructions: VFC<ISearchInstructionsProps> = ({
{filters.map(filter => ( {filters.map(filter => (
<StyledFilterHint key={filter.name}> <StyledFilterHint key={filter.name}>
{filter.header}:{' '} {filter.header}:{' '}
<StyledCode> <StyledCode
{filter.name}:{filter.options[0]} onClick={() => onClick(firstFilterOption(filter))}
>
{firstFilterOption(filter)}
</StyledCode> </StyledCode>
<ConditionallyRender <ConditionallyRender
condition={filter.options.length > 1} condition={filter.options.length > 1}
show={ show={
<> <>
{' or '} {' or '}
<StyledCode> <StyledCode
{filter.name}: onClick={() => {
{filter.options.slice(0, 2).join(',')} onClick(secondFilterOption(filter));
}}
>
{secondFilterOption(filter)}
</StyledCode> </StyledCode>
</> </>
} }

View File

@ -34,7 +34,15 @@ const searchContext = {
}; };
test('displays search and filter instructions when no search value is provided', () => { 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(); 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( expect(
screen.getByText(/Combine filters and search./i) screen.getByText(/Combine filters and search./i)
).toBeInTheDocument(); ).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', () => { test('displays search and filter instructions when search value is provided', () => {
render( render(
<SearchSuggestions <SearchSuggestions
onSuggestion={() => {}}
getSearchContext={() => ({ getSearchContext={() => ({
...searchContext, ...searchContext,
searchValue: 'Title', 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', () => { test('displays search and filter instructions when filter value is provided', () => {
let recordedSuggestion = '';
render( render(
<SearchSuggestions <SearchSuggestions
onSuggestion={suggestion => {
recordedSuggestion = suggestion;
}}
getSearchContext={() => ({ getSearchContext={() => ({
...searchContext, ...searchContext,
searchValue: 'environment:prod', searchValue: 'environment:prod',
@ -84,4 +100,9 @@ test('displays search and filter instructions when filter value is provided', ()
expect( expect(
screen.getByText(/Combine filters and search./i) screen.getByText(/Combine filters and search./i)
).toBeInTheDocument(); ).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');
}); });

View File

@ -45,10 +45,12 @@ const StyledCode = styled('span')(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: theme.spacing(0.2, 0.5), padding: theme.spacing(0.2, 0.5),
borderRadius: theme.spacing(0.5), borderRadius: theme.spacing(0.5),
cursor: 'pointer',
})); }));
interface SearchSuggestionsProps { interface SearchSuggestionsProps {
getSearchContext: () => IGetSearchContextOutput; getSearchContext: () => IGetSearchContextOutput;
onSuggestion: (suggestion: string) => void;
} }
const quote = (item: string) => (item.includes(' ') ? `"${item}"` : item); 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> = ({ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({
getSearchContext, getSearchContext,
onSuggestion,
}) => { }) => {
const searchContext = getSearchContext(); const searchContext = getSearchContext();
const randomRow = useMemo(
() => randomIndex(searchContext.data),
[searchContext.data]
);
const filters = getFilterableColumns(searchContext.columns) const filters = getFilterableColumns(searchContext.columns)
.map(column => { .map(column => {
const filterOptions = searchContext.data.map(row => const filterOptions = searchContext.data.map(row =>
@ -82,8 +80,7 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({
name: column.filterName, name: column.filterName,
header: column.Header ?? column.filterName, header: column.Header ?? column.filterName,
options, options,
suggestedOption: suggestedOption: options[0] ?? `example-${column.filterName}`,
options[randomRow] ?? `example-${column.filterName}`,
values: getFilterValues( values: getFilterValues(
column.filterName, column.filterName,
searchContext.searchValue searchContext.searchValue
@ -102,12 +99,13 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({
const suggestedTextSearch = const suggestedTextSearch =
searchContext.data.length && searchableColumns.length searchContext.data.length && searchableColumns.length
? getColumnValues( ? getColumnValues(searchableColumns[0], searchContext.data[0])
searchableColumns[0],
searchContext.data[randomRow]
)
: 'example-search-text'; : 'example-search-text';
const selectedFilter = filters.map(
filter => `${filter.name}:${filter.suggestedOption}`
)[0];
return ( return (
<StyledPaper className="dropdown-outline"> <StyledPaper className="dropdown-outline">
<StyledBox> <StyledBox>
@ -130,6 +128,7 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({
searchableColumnsString={ searchableColumnsString={
searchableColumnsString searchableColumnsString
} }
onClick={onSuggestion}
/> />
} }
/> />
@ -141,12 +140,12 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({
condition={filters.length > 0} condition={filters.length > 0}
show="Combine filters and search: " show="Combine filters and search: "
/> />
<StyledCode> <StyledCode
{filters.map(filter => ( onClick={() =>
<span key={filter.name}> onSuggestion(selectedFilter + ' ' + suggestedTextSearch)
{filter.name}:{filter.suggestedOption}{' '} }
</span> >
))} <span key={selectedFilter}>{selectedFilter}</span>{' '}
<span>{suggestedTextSearch}</span> <span>{suggestedTextSearch}</span>
</StyledCode> </StyledCode>
</Box> </Box>