Merge pull request #13957 from backstage/bux/search-list-empty-state
[Search] Customize <SearchResultList /> no results state
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': minor
|
||||
---
|
||||
|
||||
The `<SearchResultList />` component now accepts an optional property `disableRenderingWithNoResults` to disable rendering when no results are returned.
|
||||
Possibility to provide a custom no results component if needed through the `noResultsComponent` property.
|
||||
|
||||
Examples:
|
||||
|
||||
_Rendering a custom no results component_
|
||||
|
||||
```jsx
|
||||
<SearchResultList
|
||||
query={query}
|
||||
noResultsComponent={<ListItemText primary="No results were found" />}
|
||||
/>
|
||||
```
|
||||
|
||||
_Disable rendering when there are no results_
|
||||
|
||||
```jsx
|
||||
<SearchResultList query={query} disableRenderingWithNoResults />
|
||||
```
|
||||
@@ -329,9 +329,14 @@ export const SearchResultListLayout: (
|
||||
// @public
|
||||
export type SearchResultListLayoutProps = ListProps & {
|
||||
resultItems?: SearchResult_2[];
|
||||
renderResultItem?: (resultItem: SearchResult_2) => JSX.Element;
|
||||
renderResultItem?: (
|
||||
value: SearchResult_2,
|
||||
index: number,
|
||||
array: SearchResult_2[],
|
||||
) => JSX.Element | null;
|
||||
error?: Error;
|
||||
loading?: boolean;
|
||||
noResultsComponent?: ReactNode;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -340,6 +345,7 @@ export type SearchResultListProps = Omit<
|
||||
'loading' | 'error' | 'resultItems'
|
||||
> & {
|
||||
query: Partial<SearchQuery>;
|
||||
disableRenderingWithNoResults?: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -119,7 +119,7 @@ export const WithError = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const WithNoResults = () => {
|
||||
export const WithDefaultNoResultsComponent = () => {
|
||||
const [query] = useState<Partial<SearchQuery>>({
|
||||
types: ['techdocs'],
|
||||
});
|
||||
@@ -131,6 +131,21 @@ export const WithNoResults = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const WithCustomNoResultsComponent = () => {
|
||||
const [query] = useState<Partial<SearchQuery>>({
|
||||
types: ['techdocs'],
|
||||
});
|
||||
|
||||
return (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchResultList
|
||||
query={query}
|
||||
noResultsComponent={<ListItemText primary="No results were found" />}
|
||||
/>
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomResultListItem = (props: any) => {
|
||||
const { icon, result } = props;
|
||||
|
||||
|
||||
@@ -121,6 +121,42 @@ describe('SearchResultList', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Does not render result group if no results returned and disableRenderingWithNoResults prop is provided', async () => {
|
||||
query.mockResolvedValueOnce({ results: [] });
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<SearchResultList
|
||||
query={{ types: ['techdocs'] }}
|
||||
disableRenderingWithNoResults
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Documentation')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should render custom component when no results returned', async () => {
|
||||
query.mockResolvedValueOnce({ results: [] });
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<SearchResultList
|
||||
query={{ types: ['techdocs'] }}
|
||||
noResultsComponent="No results were found"
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No results were found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Shows an error panel when results rendering fails', async () => {
|
||||
query.mockRejectedValueOnce(new Error());
|
||||
await renderWithEffects(
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { List, ListProps } from '@material-ui/core';
|
||||
import { List, ListItem, ListProps } from '@material-ui/core';
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
@@ -41,7 +41,11 @@ export type SearchResultListLayoutProps = ListProps & {
|
||||
/**
|
||||
* Function to customize how result items are rendered.
|
||||
*/
|
||||
renderResultItem?: (resultItem: SearchResult) => JSX.Element;
|
||||
renderResultItem?: (
|
||||
value: SearchResult,
|
||||
index: number,
|
||||
array: SearchResult[],
|
||||
) => JSX.Element | null;
|
||||
/**
|
||||
* If defined, will render a default error panel.
|
||||
*/
|
||||
@@ -50,6 +54,10 @@ export type SearchResultListLayoutProps = ListProps & {
|
||||
* If defined, will render a default loading progress.
|
||||
*/
|
||||
loading?: boolean;
|
||||
/**
|
||||
* Optional component to render when no results. Default to <EmptyState /> component.
|
||||
*/
|
||||
noResultsComponent?: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -58,7 +66,21 @@ export type SearchResultListLayoutProps = ListProps & {
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultListLayout = (props: SearchResultListLayoutProps) => {
|
||||
const { loading, error, resultItems, renderResultItem, ...rest } = props;
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
resultItems,
|
||||
renderResultItem = resultItem => (
|
||||
<DefaultResultListItem
|
||||
key={resultItem.document.location}
|
||||
result={resultItem.document}
|
||||
/>
|
||||
),
|
||||
noResultsComponent = (
|
||||
<EmptyState missing="data" title="Sorry, no results were found" />
|
||||
),
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<List {...rest}>
|
||||
@@ -70,10 +92,10 @@ export const SearchResultListLayout = (props: SearchResultListLayoutProps) => {
|
||||
/>
|
||||
) : null}
|
||||
{!loading && !error && resultItems?.length
|
||||
? resultItems.map(resultItem => renderResultItem?.(resultItem) ?? null)
|
||||
? resultItems.map(renderResultItem)
|
||||
: null}
|
||||
{!loading && !error && !resultItems?.length ? (
|
||||
<EmptyState missing="data" title="Sorry, no results were found" />
|
||||
<ListItem>{noResultsComponent}</ListItem>
|
||||
) : null}
|
||||
</List>
|
||||
);
|
||||
@@ -91,6 +113,10 @@ export type SearchResultListProps = Omit<
|
||||
* A search query used for requesting the results to be listed.
|
||||
*/
|
||||
query: Partial<SearchQuery>;
|
||||
/**
|
||||
* Optional property to provide if component should not render the component when no results are found.
|
||||
*/
|
||||
disableRenderingWithNoResults?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -99,13 +125,7 @@ export type SearchResultListProps = Omit<
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultList = (props: SearchResultListProps) => {
|
||||
const {
|
||||
query,
|
||||
renderResultItem = ({ document }) => (
|
||||
<DefaultResultListItem key={document.location} result={document} />
|
||||
),
|
||||
...rest
|
||||
} = props;
|
||||
const { query, disableRenderingWithNoResults, ...rest } = props;
|
||||
|
||||
return (
|
||||
<AnalyticsContext
|
||||
@@ -115,15 +135,20 @@ export const SearchResultList = (props: SearchResultListProps) => {
|
||||
}}
|
||||
>
|
||||
<SearchResultState query={query}>
|
||||
{({ loading, error, value }) => (
|
||||
<SearchResultListLayout
|
||||
{...rest}
|
||||
loading={loading}
|
||||
error={error}
|
||||
resultItems={value?.results}
|
||||
renderResultItem={renderResultItem}
|
||||
/>
|
||||
)}
|
||||
{({ loading, error, value }) => {
|
||||
if (!value?.results?.length && disableRenderingWithNoResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchResultListLayout
|
||||
{...rest}
|
||||
loading={loading}
|
||||
error={error}
|
||||
resultItems={value?.results}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</SearchResultState>
|
||||
</AnalyticsContext>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user