1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

feat: Search UI improvements (#4613)

This commit is contained in:
Mateusz Kwasniewski 2023-09-06 10:50:20 +02:00 committed by GitHub
parent 73b7cc0b5a
commit 2b85eed5b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 59 additions and 27 deletions

View File

@ -58,6 +58,7 @@ describe('project overview', () => {
cy.visit('/projects/default'); cy.visit('/projects/default');
cy.viewport(1920, 1080); cy.viewport(1920, 1080);
cy.get("[data-testid='SEARCH_INPUT']").click().type(featureToggleName); cy.get("[data-testid='SEARCH_INPUT']").click().type(featureToggleName);
cy.get('body').type('{esc}');
cy.get('table tbody tr').should('have.length', 2); cy.get('table tbody tr').should('have.length', 2);
const counter = `[data-testid="${BATCH_SELECTED_COUNT}"]`; const counter = `[data-testid="${BATCH_SELECTED_COUNT}"]`;
@ -108,6 +109,7 @@ describe('project overview', () => {
cy.get(`[data-testid='${SEARCH_INPUT}']`) cy.get(`[data-testid='${SEARCH_INPUT}']`)
.click() .click()
.type(featureToggleName); .type(featureToggleName);
cy.get('body').type('{esc}');
cy.get('table tbody tr').should('have.length', 2); cy.get('table tbody tr').should('have.length', 2);
cy.get(selectAll).click(); cy.get(selectAll).click();
@ -126,6 +128,8 @@ describe('project overview', () => {
cy.get(`[data-testid='${SEARCH_INPUT}']`) cy.get(`[data-testid='${SEARCH_INPUT}']`)
.click() .click()
.type(featureToggleName); .type(featureToggleName);
cy.get('body').type('{esc}');
cy.get('table tbody tr').should('have.length', 2); cy.get('table tbody tr').should('have.length', 2);
cy.get(selectAll).click(); cy.get(selectAll).click();

View File

@ -301,6 +301,8 @@ export const ChangeRequestsTabs = ({
} }
actions={ actions={
<Search <Search
placeholder="Search and Filter"
expandable
initialValue={searchValue} initialValue={searchValue}
onChange={setSearchValue} onChange={setSearchValue}
hasFilters hasFilters

View File

@ -12,6 +12,8 @@ import { useOnClickOutside } from 'hooks/useOnClickOutside';
interface ISearchProps { interface ISearchProps {
initialValue?: string; initialValue?: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onFocus?: () => void;
onBlur?: () => void;
className?: string; className?: string;
placeholder?: string; placeholder?: string;
hasFilters?: boolean; hasFilters?: boolean;
@ -19,15 +21,18 @@ interface ISearchProps {
getSearchContext?: () => IGetSearchContextOutput; getSearchContext?: () => IGetSearchContextOutput;
containerStyles?: React.CSSProperties; containerStyles?: React.CSSProperties;
debounceTime?: number; debounceTime?: number;
expandable?: boolean;
} }
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div', {
shouldForwardProp: prop => prop !== 'active',
})<{ active: boolean | undefined }>(({ theme, active }) => ({
display: 'flex', display: 'flex',
flexGrow: 1, flexGrow: 1,
alignItems: 'center', alignItems: 'center',
position: 'relative', position: 'relative',
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
maxWidth: '400px', maxWidth: active ? '100%' : '400px',
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {
marginTop: theme.spacing(1), marginTop: theme.spacing(1),
maxWidth: '100%', maxWidth: '100%',
@ -62,18 +67,24 @@ const StyledClose = styled(Close)(({ theme }) => ({
export const Search = ({ export const Search = ({
initialValue = '', initialValue = '',
onChange, onChange,
onFocus,
onBlur,
className, className,
placeholder: customPlaceholder, placeholder: customPlaceholder,
hasFilters, hasFilters,
disabled, disabled,
getSearchContext, getSearchContext,
containerStyles, containerStyles,
expandable = false,
debounceTime = 200, debounceTime = 200,
}: ISearchProps) => { }: ISearchProps) => {
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const suggestionsRef = useRef<HTMLInputElement>(null); const suggestionsRef = useRef<HTMLInputElement>(null);
const [showSuggestions, setShowSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false);
const hideSuggestions = () => setShowSuggestions(false); const hideSuggestions = () => {
setShowSuggestions(false);
onBlur?.();
};
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
const debouncedOnChange = useAsyncDebounce(onChange, debounceTime); const debouncedOnChange = useAsyncDebounce(onChange, debounceTime);
@ -104,7 +115,10 @@ export const Search = ({
useOnClickOutside([searchInputRef, suggestionsRef], hideSuggestions); useOnClickOutside([searchInputRef, suggestionsRef], hideSuggestions);
return ( return (
<StyledContainer style={containerStyles}> <StyledContainer
style={containerStyles}
active={expandable && showSuggestions}
>
<StyledSearch className={className}> <StyledSearch className={className}>
<SearchIcon <SearchIcon
sx={{ sx={{
@ -121,7 +135,10 @@ export const Search = ({
}} }}
value={value} value={value}
onChange={e => onSearchChange(e.target.value)} onChange={e => onSearchChange(e.target.value)}
onFocus={() => setShowSuggestions(true)} onFocus={() => {
setShowSuggestions(true);
onFocus?.();
}}
disabled={disabled} disabled={disabled}
/> />
<Box sx={{ width: theme => theme.spacing(4) }}> <Box sx={{ width: theme => theme.spacing(4) }}>

View File

@ -1,6 +1,5 @@
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IGetSearchContextOutput } from 'hooks/useSearch';
import { VFC } from 'react'; import { VFC } from 'react';
const StyledHeader = styled('span')(({ theme }) => ({ const StyledHeader = styled('span')(({ theme }) => ({
@ -11,26 +10,28 @@ const StyledHeader = styled('span')(({ theme }) => ({
const StyledCode = styled('span')(({ theme }) => ({ const StyledCode = styled('span')(({ theme }) => ({
backgroundColor: theme.palette.background.elevation2, backgroundColor: theme.palette.background.elevation2,
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: theme.spacing(0, 0.5), padding: theme.spacing(0.2, 1),
borderRadius: theme.spacing(0.5), borderRadius: theme.spacing(0.5),
})); }));
const StyledFilterHint = styled('p')(({ theme }) => ({
lineHeight: 1.75,
}));
interface ISearchInstructionsProps { interface ISearchInstructionsProps {
filters: any[]; filters: any[];
getSearchContext: () => IGetSearchContextOutput;
searchableColumnsString: string; searchableColumnsString: string;
} }
export const SearchInstructions: VFC<ISearchInstructionsProps> = ({ export const SearchInstructions: VFC<ISearchInstructionsProps> = ({
filters, filters,
getSearchContext,
searchableColumnsString, searchableColumnsString,
}) => { }) => {
return ( return (
<> <>
<StyledHeader> <StyledHeader>
{filters.length > 0 {filters.length > 0
? 'Filter your search with operators like:' ? 'Filter your results by:'
: `Start typing to search${ : `Start typing to search${
searchableColumnsString searchableColumnsString
? ` in ${searchableColumnsString}` ? ` in ${searchableColumnsString}`
@ -38,8 +39,8 @@ export const SearchInstructions: VFC<ISearchInstructionsProps> = ({
}`} }`}
</StyledHeader> </StyledHeader>
{filters.map(filter => ( {filters.map(filter => (
<p key={filter.name}> <StyledFilterHint key={filter.name}>
Filter by {filter.header}:{' '} {filter.header}:{' '}
<StyledCode> <StyledCode>
{filter.name}:{filter.options[0]} {filter.name}:{filter.options[0]}
</StyledCode> </StyledCode>
@ -55,7 +56,7 @@ export const SearchInstructions: VFC<ISearchInstructionsProps> = ({
</> </>
} }
/> />
</p> </StyledFilterHint>
))} ))}
</> </>
); );

View File

@ -36,11 +36,9 @@ 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} />); render(<SearchSuggestions getSearchContext={() => searchContext} />);
expect( expect(screen.getByText(/Filter your results by:/i)).toBeInTheDocument();
screen.getByText(/Filter your search with operators like:/i)
).toBeInTheDocument();
expect(screen.getByText(/Filter by Environment:/i)).toBeInTheDocument(); expect(screen.getByText(/Environment:/)).toBeInTheDocument();
expect( expect(
screen.getByText(/environment:"dev env",pre-prod/i) screen.getByText(/environment:"dev env",pre-prod/i)

View File

@ -43,7 +43,7 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
const StyledCode = styled('span')(({ theme }) => ({ const StyledCode = styled('span')(({ theme }) => ({
backgroundColor: theme.palette.background.elevation2, backgroundColor: theme.palette.background.elevation2,
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: theme.spacing(0, 0.5), padding: theme.spacing(0.2, 0.5),
borderRadius: theme.spacing(0.5), borderRadius: theme.spacing(0.5),
})); }));
@ -127,7 +127,6 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({
elseShow={ elseShow={
<SearchInstructions <SearchInstructions
filters={filters} filters={filters}
getSearchContext={getSearchContext}
searchableColumnsString={ searchableColumnsString={
searchableColumnsString searchableColumnsString
} }
@ -137,12 +136,11 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({
</Box> </Box>
</StyledBox> </StyledBox>
<StyledDivider /> <StyledDivider />
<ConditionallyRender <Box sx={{ lineHeight: 1.75 }}>
condition={filters.length > 0} <ConditionallyRender
show="Combine filters and search." condition={filters.length > 0}
/> show="Combine filters and search: "
<p> />
Example:{' '}
<StyledCode> <StyledCode>
{filters.map(filter => ( {filters.map(filter => (
<span key={filter.name}> <span key={filter.name}>
@ -151,7 +149,7 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({
))} ))}
<span>{suggestedTextSearch}</span> <span>{suggestedTextSearch}</span>
</StyledCode> </StyledCode>
</p> </Box>
</StyledPaper> </StyledPaper>
); );
}; };

View File

@ -303,6 +303,8 @@ export const FeatureToggleListTable: VFC = () => {
show={ show={
<> <>
<Search <Search
placeholder="Search and Filter"
expandable
initialValue={searchValue} initialValue={searchValue}
onChange={setSearchValue} onChange={setSearchValue}
hasFilters hasFilters

View File

@ -355,6 +355,8 @@ export const ProjectFeatureToggles = ({
searchParams.get('search') || '' searchParams.get('search') || ''
); );
const [showTitle, setShowTitle] = useState(true);
const featuresData = useMemo( const featuresData = useMemo(
() => () =>
features.map(feature => ({ features.map(feature => ({
@ -533,15 +535,23 @@ export const ProjectFeatureToggles = ({
className={styles.container} className={styles.container}
header={ header={
<PageHeader <PageHeader
titleElement={`Feature toggles (${rows.length})`} titleElement={
showTitle
? `Feature toggles (${rows.length})`
: null
}
actions={ actions={
<> <>
<ConditionallyRender <ConditionallyRender
condition={!isSmallScreen} condition={!isSmallScreen}
show={ show={
<Search <Search
placeholder="Search and Filter"
expandable
initialValue={searchValue} initialValue={searchValue}
onChange={setSearchValue} onChange={setSearchValue}
onFocus={() => setShowTitle(false)}
onBlur={() => setShowTitle(true)}
hasFilters hasFilters
getSearchContext={getSearchContext} getSearchContext={getSearchContext}
/> />