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:
parent
73b7cc0b5a
commit
2b85eed5b5
@ -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();
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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) }}>
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user