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:
parent
73b7cc0b5a
commit
2b85eed5b5
@ -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();
|
||||
|
||||
|
@ -301,6 +301,8 @@ export const ChangeRequestsTabs = ({
|
||||
}
|
||||
actions={
|
||||
<Search
|
||||
placeholder="Search and Filter"
|
||||
expandable
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
hasFilters
|
||||
|
@ -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) }}>
|
||||
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -303,6 +303,8 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
show={
|
||||
<>
|
||||
<Search
|
||||
placeholder="Search and Filter"
|
||||
expandable
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
hasFilters
|
||||
|
@ -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}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user