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:
parent
f55c67fe2e
commit
caff040a88
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user