1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +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.viewport(1920, 1080);
cy.get("[data-testid='SEARCH_INPUT']").click().type(featureToggleName);
cy.get('body').type('{esc}');
cy.get('table tbody tr').should('have.length', 2);
const counter = `[data-testid="${BATCH_SELECTED_COUNT}"]`;
@ -108,6 +109,7 @@ describe('project overview', () => {
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(selectAll).click();
@ -126,6 +128,8 @@ describe('project overview', () => {
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(selectAll).click();

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import { styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IGetSearchContextOutput } from 'hooks/useSearch';
import { VFC } from 'react';
const StyledHeader = styled('span')(({ theme }) => ({
@ -11,26 +10,28 @@ 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),
padding: theme.spacing(0.2, 1),
borderRadius: theme.spacing(0.5),
}));
const StyledFilterHint = styled('p')(({ theme }) => ({
lineHeight: 1.75,
}));
interface ISearchInstructionsProps {
filters: any[];
getSearchContext: () => IGetSearchContextOutput;
searchableColumnsString: string;
}
export const SearchInstructions: VFC<ISearchInstructionsProps> = ({
filters,
getSearchContext,
searchableColumnsString,
}) => {
return (
<>
<StyledHeader>
{filters.length > 0
? 'Filter your search with operators like:'
? 'Filter your results by:'
: `Start typing to search${
searchableColumnsString
? ` in ${searchableColumnsString}`
@ -38,8 +39,8 @@ export const SearchInstructions: VFC<ISearchInstructionsProps> = ({
}`}
</StyledHeader>
{filters.map(filter => (
<p key={filter.name}>
Filter by {filter.header}:{' '}
<StyledFilterHint key={filter.name}>
{filter.header}:{' '}
<StyledCode>
{filter.name}:{filter.options[0]}
</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', () => {
render(<SearchSuggestions getSearchContext={() => searchContext} />);
expect(
screen.getByText(/Filter your search with operators like:/i)
).toBeInTheDocument();
expect(screen.getByText(/Filter your results by:/i)).toBeInTheDocument();
expect(screen.getByText(/Filter by Environment:/i)).toBeInTheDocument();
expect(screen.getByText(/Environment:/)).toBeInTheDocument();
expect(
screen.getByText(/environment:"dev env",pre-prod/i)

View File

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

View File

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

View File

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