mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02: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