feat(search): create result item hook and components
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -23,16 +23,18 @@ import CustomIcon from '@material-ui/icons/NoteAdd';
|
||||
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { createPlugin } from '@backstage/core-plugin-api';
|
||||
import { SearchDocument } from '@backstage/plugin-search-common';
|
||||
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { createSearchResultListItemExtension } from '../../extensions';
|
||||
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
import { SearchResultListLayout } from '../SearchResultList';
|
||||
import { SearchResultGroupLayout } from '../SearchResultGroup';
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
|
||||
import { SearchResult } from './SearchResult';
|
||||
import { SearchResultGroupLayout } from '../SearchResultGroup';
|
||||
|
||||
const mockResults = {
|
||||
results: [
|
||||
@@ -247,3 +249,18 @@ export const WithCustomNoResultsComponent = () => {
|
||||
</SearchResult>
|
||||
);
|
||||
};
|
||||
|
||||
export const UsingSearchResultItemExtensions = () => {
|
||||
const plugin = createPlugin({ id: 'plugin' });
|
||||
const DefaultResultItem = plugin.provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'DefaultResultListItem',
|
||||
component: async () => DefaultResultListItem,
|
||||
}),
|
||||
);
|
||||
return (
|
||||
<SearchResult>
|
||||
<DefaultResultItem />
|
||||
</SearchResult>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,9 +17,19 @@
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { ListItem } from '@material-ui/core';
|
||||
import {
|
||||
wrapInTestApp,
|
||||
renderInTestApp,
|
||||
renderWithEffects,
|
||||
TestApiProvider,
|
||||
} from '@backstage/test-utils';
|
||||
import { createPlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
import { searchApiRef } from '../../api';
|
||||
import { useSearch } from '../../context';
|
||||
import { createSearchResultListItemExtension } from '../../extensions';
|
||||
|
||||
import { SearchResult } from './SearchResult';
|
||||
|
||||
jest.mock('../../context', () => ({
|
||||
@@ -30,8 +40,12 @@ jest.mock('../../context', () => ({
|
||||
}));
|
||||
|
||||
describe('SearchResult', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Progress rendered on Loading state', async () => {
|
||||
(useSearch as jest.Mock).mockReturnValueOnce({
|
||||
(useSearch as jest.Mock).mockReturnValue({
|
||||
result: { loading: true },
|
||||
});
|
||||
|
||||
@@ -46,7 +60,7 @@ describe('SearchResult', () => {
|
||||
|
||||
it('Alert rendered on Error state', async () => {
|
||||
const error = new Error('some error');
|
||||
(useSearch as jest.Mock).mockReturnValueOnce({
|
||||
(useSearch as jest.Mock).mockReturnValue({
|
||||
result: { loading: false, error },
|
||||
});
|
||||
|
||||
@@ -62,7 +76,7 @@ describe('SearchResult', () => {
|
||||
});
|
||||
|
||||
it('On no result value state', async () => {
|
||||
(useSearch as jest.Mock).mockReturnValueOnce({
|
||||
(useSearch as jest.Mock).mockReturnValue({
|
||||
result: { loading: false, error: '', value: undefined },
|
||||
});
|
||||
|
||||
@@ -78,7 +92,7 @@ describe('SearchResult', () => {
|
||||
});
|
||||
|
||||
it('On empty result value state', async () => {
|
||||
(useSearch as jest.Mock).mockReturnValueOnce({
|
||||
(useSearch as jest.Mock).mockReturnValue({
|
||||
result: { loading: false, error: '', value: { results: [] } },
|
||||
});
|
||||
|
||||
@@ -94,7 +108,7 @@ describe('SearchResult', () => {
|
||||
});
|
||||
|
||||
it('On empty result value state with custom component', async () => {
|
||||
(useSearch as jest.Mock).mockReturnValueOnce({
|
||||
(useSearch as jest.Mock).mockReturnValue({
|
||||
result: { loading: false, error: '', value: { results: [] } },
|
||||
});
|
||||
|
||||
@@ -110,7 +124,7 @@ describe('SearchResult', () => {
|
||||
});
|
||||
|
||||
it('Calls children with results set to result.value', async () => {
|
||||
(useSearch as jest.Mock).mockReturnValueOnce({
|
||||
(useSearch as jest.Mock).mockReturnValue({
|
||||
result: {
|
||||
loading: false,
|
||||
error: '',
|
||||
@@ -140,4 +154,82 @@ describe('SearchResult', () => {
|
||||
|
||||
expect(getByText('Results 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders results from api', async () => {
|
||||
const results = [
|
||||
{
|
||||
type: 'some-type',
|
||||
document: {
|
||||
title: 'some-title',
|
||||
text: 'some-text',
|
||||
location: 'some-location',
|
||||
},
|
||||
},
|
||||
];
|
||||
const query = jest.fn().mockResolvedValue({ results });
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchResult query={{ types: ['techdocs'] }}>
|
||||
{value => {
|
||||
expect(value.results).toStrictEqual(results);
|
||||
return <></>;
|
||||
}}
|
||||
</SearchResult>
|
||||
</TestApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
term: '',
|
||||
filters: {},
|
||||
types: ['techdocs'],
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders using search result item extensions', async () => {
|
||||
(useSearch as jest.Mock).mockReturnValue({
|
||||
result: {
|
||||
loading: false,
|
||||
error: '',
|
||||
value: {
|
||||
totalCount: 1,
|
||||
results: [
|
||||
{
|
||||
type: 'some-type',
|
||||
document: {
|
||||
title: 'some-title',
|
||||
text: 'some-text',
|
||||
location: 'some-location',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { getByText, rerender } = await renderInTestApp(<SearchResult />);
|
||||
|
||||
expect(getByText('some-title')).toBeInTheDocument();
|
||||
|
||||
const SearchResultExtension = createPlugin({
|
||||
id: 'plugin',
|
||||
}).provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'SearchResultExtension',
|
||||
component: async () => props =>
|
||||
<ListItem>Result: {props.result?.title}</ListItem>,
|
||||
}),
|
||||
);
|
||||
|
||||
rerender(
|
||||
<SearchResult>
|
||||
<SearchResultExtension />
|
||||
</SearchResult>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Result: some-title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,19 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
|
||||
import { isFunction } from 'lodash';
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Progress,
|
||||
EmptyState,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { AnalyticsContext, useApi } from '@backstage/core-plugin-api';
|
||||
import { useApi, AnalyticsContext } from '@backstage/core-plugin-api';
|
||||
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
|
||||
|
||||
import { useSearch } from '../../context';
|
||||
import { searchApiRef } from '../../api';
|
||||
import { useSearch } from '../../context';
|
||||
import {
|
||||
SearchResultListItemExtensions,
|
||||
SearchResultListItemExtensionsProps,
|
||||
} from '../../extensions';
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultContext}
|
||||
@@ -36,7 +41,10 @@ export type SearchResultContextProps = {
|
||||
/**
|
||||
* A child function that receives an asynchronous result set and returns a react element.
|
||||
*/
|
||||
children: (state: AsyncState<SearchResultSet>) => JSX.Element | null;
|
||||
children: (
|
||||
state: AsyncState<SearchResultSet>,
|
||||
query: Partial<SearchQuery>,
|
||||
) => JSX.Element | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -62,8 +70,8 @@ export type SearchResultContextProps = {
|
||||
export const SearchResultContext = (props: SearchResultContextProps) => {
|
||||
const { children } = props;
|
||||
const context = useSearch();
|
||||
const state = context.result;
|
||||
return children(state);
|
||||
const { result: state, ...query } = context;
|
||||
return children(state, query);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -103,7 +111,7 @@ export const SearchResultApi = (props: SearchResultApiProps) => {
|
||||
return searchApi.query({ ...rest, term, types, filters });
|
||||
}, [query]);
|
||||
|
||||
return children(state);
|
||||
return children(state, query);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -165,10 +173,11 @@ export const SearchResultState = (props: SearchResultStateProps) => {
|
||||
* Props for {@link SearchResult}
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultProps = Pick<SearchResultStateProps, 'query'> & {
|
||||
children: (resultSet: SearchResultSet) => JSX.Element;
|
||||
noResultsComponent?: JSX.Element;
|
||||
};
|
||||
export type SearchResultProps = Pick<SearchResultStateProps, 'query'> &
|
||||
Omit<SearchResultListItemExtensionsProps, 'results' | 'children'> & {
|
||||
children?: ReactNode | ((resultSet: SearchResultSet) => JSX.Element);
|
||||
noResultsComponent?: JSX.Element;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders results from a parent search context or api.
|
||||
@@ -183,6 +192,7 @@ export const SearchResultComponent = (props: SearchResultProps) => {
|
||||
noResultsComponent = (
|
||||
<EmptyState missing="data" title="Sorry, no results were found" />
|
||||
),
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -205,7 +215,15 @@ export const SearchResultComponent = (props: SearchResultProps) => {
|
||||
return noResultsComponent;
|
||||
}
|
||||
|
||||
return children(value);
|
||||
if (isFunction(children)) {
|
||||
return children(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchResultListItemExtensions {...rest} results={value.results}>
|
||||
{children}
|
||||
</SearchResultListItemExtensions>
|
||||
);
|
||||
}}
|
||||
</SearchResultState>
|
||||
);
|
||||
|
||||
@@ -18,8 +18,8 @@ export {
|
||||
SearchResult,
|
||||
SearchResultApi,
|
||||
SearchResultContext,
|
||||
SearchResultState,
|
||||
SearchResultComponent,
|
||||
SearchResultState,
|
||||
} from './SearchResult';
|
||||
|
||||
export type {
|
||||
|
||||
+32
-2
@@ -27,11 +27,15 @@ import DocsIcon from '@material-ui/icons/InsertDriveFile';
|
||||
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
|
||||
import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { createPlugin, createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
|
||||
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
|
||||
import { SearchContextProvider } from '../../context';
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { createSearchResultListItemExtension } from '../../extensions';
|
||||
|
||||
import {
|
||||
SearchResultGroup,
|
||||
@@ -83,6 +87,14 @@ export default {
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<SearchContextProvider>
|
||||
<SearchResultGroup icon={<DocsIcon />} title="Documentation" />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithQuery = () => {
|
||||
const [query] = useState<Partial<SearchQuery>>({
|
||||
types: ['techdocs'],
|
||||
});
|
||||
@@ -341,3 +353,21 @@ export const WithCustomResultItem = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithResultItemExtensions = () => {
|
||||
const [query] = useState<Partial<SearchQuery>>({
|
||||
types: ['techdocs'],
|
||||
});
|
||||
const plugin = createPlugin({ id: 'plugin' });
|
||||
const DefaultSearchResultGroupItem = plugin.provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'DefaultResultListItem',
|
||||
component: async () => DefaultResultListItem,
|
||||
}),
|
||||
);
|
||||
return (
|
||||
<SearchResultGroup query={query} icon={<DocsIcon />} title="Documentation">
|
||||
<DefaultSearchResultGroupItem />
|
||||
</SearchResultGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,16 +18,21 @@ import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { MenuItem } from '@material-ui/core';
|
||||
import { ListItem, MenuItem } from '@material-ui/core';
|
||||
import DocsIcon from '@material-ui/icons/InsertDriveFile';
|
||||
|
||||
import {
|
||||
TestApiProvider,
|
||||
renderWithEffects,
|
||||
wrapInTestApp,
|
||||
renderWithEffects,
|
||||
TestApiProvider,
|
||||
MockAnalyticsApi,
|
||||
} from '@backstage/test-utils';
|
||||
import { createPlugin, analyticsApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
import { searchApiRef } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
import { createSearchResultListItemExtension } from '../../extensions';
|
||||
|
||||
import {
|
||||
SearchResultGroup,
|
||||
SearchResultGroupSelectFilterField,
|
||||
@@ -36,6 +41,7 @@ import {
|
||||
|
||||
const query = jest.fn().mockResolvedValue({ results: [] });
|
||||
const searchApiMock = { query };
|
||||
const analyticsApiMock = new MockAnalyticsApi();
|
||||
|
||||
describe('SearchResultGroup', () => {
|
||||
const results = [
|
||||
@@ -62,9 +68,18 @@ describe('SearchResultGroup', () => {
|
||||
});
|
||||
|
||||
it('Renders without exploding', async () => {
|
||||
query.mockResolvedValueOnce({
|
||||
results,
|
||||
});
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
@@ -84,10 +99,94 @@ describe('SearchResultGroup', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Defines a default link', async () => {
|
||||
it('Renders search results from context', async () => {
|
||||
query.mockResolvedValueOnce({
|
||||
results,
|
||||
});
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchContextProvider>
|
||||
<SearchResultGroup
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
title="Documentation"
|
||||
/>
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Search Result 1')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Some text from the search result 1'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Search Result 2')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Some text from the search result 2'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders search results using extensions', async () => {
|
||||
query.mockResolvedValueOnce({
|
||||
results,
|
||||
});
|
||||
|
||||
const SearchResultGroupItemExtension = createPlugin({
|
||||
id: 'plugin',
|
||||
}).provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'SearchResultGroupItemExtension',
|
||||
component: async () => props =>
|
||||
<ListItem>Result: {props.result?.title}</ListItem>,
|
||||
}),
|
||||
);
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
title="Documentation"
|
||||
>
|
||||
<SearchResultGroupItemExtension />
|
||||
</SearchResultGroup>
|
||||
</TestApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Result: Search Result 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Result: Search Result 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Defines a default link', async () => {
|
||||
query.mockResolvedValueOnce({
|
||||
results,
|
||||
});
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
@@ -108,7 +207,12 @@ describe('SearchResultGroup', () => {
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
@@ -132,7 +236,12 @@ describe('SearchResultGroup', () => {
|
||||
it('Could be customized with no results text', async () => {
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
@@ -154,7 +263,12 @@ describe('SearchResultGroup', () => {
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
@@ -184,7 +298,12 @@ describe('SearchResultGroup', () => {
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{
|
||||
types: ['techdocs'],
|
||||
@@ -232,7 +351,12 @@ describe('SearchResultGroup', () => {
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{
|
||||
types: ['techdocs'],
|
||||
@@ -276,7 +400,12 @@ describe('SearchResultGroup', () => {
|
||||
query.mockReturnValueOnce(new Promise(() => {}));
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
@@ -295,7 +424,12 @@ describe('SearchResultGroup', () => {
|
||||
query.mockResolvedValueOnce({ results: [] });
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
@@ -315,7 +449,12 @@ describe('SearchResultGroup', () => {
|
||||
query.mockResolvedValueOnce({ results: [] });
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
@@ -335,7 +474,12 @@ describe('SearchResultGroup', () => {
|
||||
query.mockRejectedValueOnce(new Error());
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultGroup
|
||||
query={{ types: ['techdocs'] }}
|
||||
icon={<DocsIcon titleAccess="Docs icon" />}
|
||||
|
||||
@@ -27,9 +27,8 @@ import {
|
||||
makeStyles,
|
||||
Theme,
|
||||
List,
|
||||
ListSubheader,
|
||||
ListItem,
|
||||
ListProps,
|
||||
ListSubheader,
|
||||
Menu,
|
||||
MenuItem,
|
||||
InputBase,
|
||||
@@ -43,17 +42,19 @@ import ArrowRightIcon from '@material-ui/icons/ArrowForwardIos';
|
||||
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import {
|
||||
EmptyState,
|
||||
Link,
|
||||
LinkProps,
|
||||
Progress,
|
||||
EmptyState,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { AnalyticsContext } from '@backstage/core-plugin-api';
|
||||
import { SearchQuery, SearchResult } from '@backstage/plugin-search-common';
|
||||
import { SearchResult } from '@backstage/plugin-search-common';
|
||||
|
||||
import { useSearchResultListItemExtensions } from '../../extensions';
|
||||
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
import { SearchResultState } from '../SearchResult';
|
||||
import { SearchResultState, SearchResultStateProps } from '../SearchResult';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
listSubheader: {
|
||||
@@ -278,6 +279,14 @@ export const SearchResultGroupSelectFilterField = (
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultGroupLayoutProps<FilterOption> = ListProps & {
|
||||
/**
|
||||
* If defined, will render a default error panel.
|
||||
*/
|
||||
error?: Error;
|
||||
/**
|
||||
* If defined, will render a default loading progress.
|
||||
*/
|
||||
loading?: boolean;
|
||||
/**
|
||||
* Icon that representing a result group.
|
||||
*/
|
||||
@@ -331,18 +340,14 @@ export type SearchResultGroupLayoutProps<FilterOption> = ListProps & {
|
||||
index: number,
|
||||
array: SearchResult[],
|
||||
) => JSX.Element | null;
|
||||
/**
|
||||
* If defined, will render a default error panel.
|
||||
*/
|
||||
error?: Error;
|
||||
/**
|
||||
* If defined, will render a default loading progress.
|
||||
*/
|
||||
loading?: boolean;
|
||||
/**
|
||||
* Optional component to render when no results. Default to <EmptyState /> component.
|
||||
*/
|
||||
noResultsComponent?: ReactNode;
|
||||
/**
|
||||
* Optional property to provide if component should not render the component when no results are found.
|
||||
*/
|
||||
disableRenderingWithNoResults?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -357,8 +362,8 @@ export function SearchResultGroupLayout<FilterOption>(
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
loading,
|
||||
icon,
|
||||
title,
|
||||
titleProps = {},
|
||||
@@ -384,7 +389,8 @@ export function SearchResultGroupLayout<FilterOption>(
|
||||
result={resultItem.document}
|
||||
/>
|
||||
),
|
||||
noResultsComponent = (
|
||||
disableRenderingWithNoResults,
|
||||
noResultsComponent = disableRenderingWithNoResults ? null : (
|
||||
<EmptyState missing="data" title="Sorry, no results were found" />
|
||||
),
|
||||
...rest
|
||||
@@ -398,6 +404,23 @@ export function SearchResultGroupLayout<FilterOption>(
|
||||
setAnchorEl(null);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ResponseErrorPanel
|
||||
title="Error encountered while fetching search results"
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!resultItems?.length) {
|
||||
return <>{noResultsComponent}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<List {...rest}>
|
||||
<ListSubheader className={classes.listSubheader}>
|
||||
@@ -440,19 +463,7 @@ export function SearchResultGroupLayout<FilterOption>(
|
||||
{link}
|
||||
</Link>
|
||||
</ListSubheader>
|
||||
{loading ? <Progress /> : null}
|
||||
{!loading && error ? (
|
||||
<ResponseErrorPanel
|
||||
title="Error encountered while fetching search results"
|
||||
error={error}
|
||||
/>
|
||||
) : null}
|
||||
{!loading && !error && resultItems?.length
|
||||
? resultItems.map(renderResultItem)
|
||||
: null}
|
||||
{!loading && !error && !resultItems?.length ? (
|
||||
<ListItem>{noResultsComponent}</ListItem>
|
||||
) : null}
|
||||
{resultItems.map(renderResultItem)}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
@@ -461,19 +472,14 @@ export function SearchResultGroupLayout<FilterOption>(
|
||||
* Props for {@link SearchResultGroup}.
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultGroupProps<FilterOption> = Omit<
|
||||
SearchResultGroupLayoutProps<FilterOption>,
|
||||
'loading' | 'error' | 'resultItems' | 'filterFields'
|
||||
> & {
|
||||
/**
|
||||
* A search query used for requesting the results to be grouped.
|
||||
*/
|
||||
query: Partial<SearchQuery>;
|
||||
/**
|
||||
* Optional property to provide if component should not render the group when no results are found.
|
||||
*/
|
||||
disableRenderingWithNoResults?: boolean;
|
||||
};
|
||||
export type SearchResultGroupProps<FilterOption> = Pick<
|
||||
SearchResultStateProps,
|
||||
'query'
|
||||
> &
|
||||
Omit<
|
||||
SearchResultGroupLayoutProps<FilterOption>,
|
||||
'loading' | 'error' | 'resultItems' | 'filterFields'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Given a query, search for results and render them as a group.
|
||||
@@ -483,22 +489,9 @@ export type SearchResultGroupProps<FilterOption> = Omit<
|
||||
export function SearchResultGroup<FilterOption>(
|
||||
props: SearchResultGroupProps<FilterOption>,
|
||||
) {
|
||||
const {
|
||||
query,
|
||||
linkProps = {},
|
||||
disableRenderingWithNoResults,
|
||||
...rest
|
||||
} = props;
|
||||
const { query, children, renderResultItem, linkProps = {}, ...rest } = props;
|
||||
|
||||
const to = `/search?${qs.stringify(
|
||||
{
|
||||
query: query.term,
|
||||
types: query.types,
|
||||
filters: query.filters,
|
||||
pageCursor: query.pageCursor,
|
||||
},
|
||||
{ arrayFormat: 'brackets' },
|
||||
)}`;
|
||||
const defaultRenderResultItem = useSearchResultListItemExtensions(children);
|
||||
|
||||
return (
|
||||
<AnalyticsContext
|
||||
@@ -508,19 +501,24 @@ export function SearchResultGroup<FilterOption>(
|
||||
}}
|
||||
>
|
||||
<SearchResultState query={query}>
|
||||
{({ loading, error, value }) => {
|
||||
if (!value?.results?.length && disableRenderingWithNoResults) {
|
||||
return null;
|
||||
}
|
||||
{(
|
||||
{ loading, error, value },
|
||||
{ term, types, pageCursor, filters = {} },
|
||||
) => {
|
||||
const to = `/search?${qs.stringify(
|
||||
{ term, types, filters, pageCursor, query: term },
|
||||
{ arrayFormat: 'brackets' },
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<SearchResultGroupLayout
|
||||
{...rest}
|
||||
loading={loading}
|
||||
error={error}
|
||||
loading={loading}
|
||||
linkProps={{ to, ...linkProps }}
|
||||
filterFields={Object.keys(filters)}
|
||||
resultItems={value?.results}
|
||||
filterFields={Object.keys(query.filters ?? {})}
|
||||
renderResultItem={renderResultItem ?? defaultRenderResultItem}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -18,12 +18,14 @@ import React, { ComponentType, useState } from 'react';
|
||||
|
||||
import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
|
||||
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { CatalogIcon, Link } from '@backstage/core-components';
|
||||
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
|
||||
import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { createPlugin, createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
|
||||
|
||||
import { SearchContextProvider } from '../../context';
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { createSearchResultListItemExtension } from '../../extensions';
|
||||
|
||||
import { SearchResultList } from './SearchResultList';
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
@@ -72,6 +74,14 @@ export default {
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<SearchContextProvider>
|
||||
<SearchResultList />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithQuery = () => {
|
||||
const [query] = useState<Partial<SearchQuery>>({
|
||||
types: ['techdocs'],
|
||||
});
|
||||
@@ -195,3 +205,21 @@ export const WithCustomResultItem = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithResultItemExtensions = () => {
|
||||
const [query] = useState<Partial<SearchQuery>>({
|
||||
types: ['techdocs'],
|
||||
});
|
||||
const plugin = createPlugin({ id: 'plugin' });
|
||||
const DefaultSearchResultListItem = plugin.provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'DefaultResultListItem',
|
||||
component: async () => DefaultResultListItem,
|
||||
}),
|
||||
);
|
||||
return (
|
||||
<SearchResultList query={query}>
|
||||
<DefaultSearchResultListItem />
|
||||
</SearchResultList>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,17 +17,23 @@
|
||||
import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { ListItem } from '@material-ui/core';
|
||||
import {
|
||||
TestApiProvider,
|
||||
renderWithEffects,
|
||||
wrapInTestApp,
|
||||
MockAnalyticsApi,
|
||||
} from '@backstage/test-utils';
|
||||
import { analyticsApiRef, createPlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
import { searchApiRef } from '../../api';
|
||||
import { createSearchResultListItemExtension } from '../../extensions';
|
||||
|
||||
import { SearchResultList } from './SearchResultList';
|
||||
|
||||
const query = jest.fn().mockResolvedValue({ results: [] });
|
||||
const searchApiMock = { query };
|
||||
const analyticsApiMock = new MockAnalyticsApi();
|
||||
|
||||
describe('SearchResultList', () => {
|
||||
const results = [
|
||||
@@ -56,7 +62,12 @@ describe('SearchResultList', () => {
|
||||
it('Renders without exploding', async () => {
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultList
|
||||
query={{
|
||||
types: ['techdocs'],
|
||||
@@ -81,7 +92,12 @@ describe('SearchResultList', () => {
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultList
|
||||
query={{
|
||||
types: ['techdocs'],
|
||||
@@ -106,7 +122,12 @@ describe('SearchResultList', () => {
|
||||
query.mockReturnValueOnce(new Promise(() => {}));
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultList
|
||||
query={{
|
||||
types: ['techdocs'],
|
||||
@@ -125,7 +146,12 @@ describe('SearchResultList', () => {
|
||||
query.mockResolvedValueOnce({ results: [] });
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultList
|
||||
query={{ types: ['techdocs'] }}
|
||||
disableRenderingWithNoResults
|
||||
@@ -143,7 +169,12 @@ describe('SearchResultList', () => {
|
||||
query.mockResolvedValueOnce({ results: [] });
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultList
|
||||
query={{ types: ['techdocs'] }}
|
||||
noResultsComponent="No results were found"
|
||||
@@ -161,7 +192,12 @@ describe('SearchResultList', () => {
|
||||
query.mockRejectedValueOnce(new Error());
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultList
|
||||
query={{
|
||||
types: ['techdocs'],
|
||||
@@ -179,4 +215,45 @@ describe('SearchResultList', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render search results using list item extensions', async () => {
|
||||
query.mockResolvedValueOnce({
|
||||
results,
|
||||
});
|
||||
|
||||
const SearchResultListItemExtension = createPlugin({
|
||||
id: 'plugin',
|
||||
}).provide(
|
||||
createSearchResultListItemExtension({
|
||||
name: 'SearchResultListItemExtension',
|
||||
component: async () => props =>
|
||||
<ListItem>Result: {props.result?.title}</ListItem>,
|
||||
}),
|
||||
);
|
||||
|
||||
await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
[analyticsApiRef, analyticsApiMock],
|
||||
]}
|
||||
>
|
||||
<SearchResultList
|
||||
query={{
|
||||
types: ['techdocs'],
|
||||
}}
|
||||
>
|
||||
<SearchResultListItemExtension />
|
||||
</SearchResultList>
|
||||
</TestApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Result: Search Result 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Result: Search Result 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,24 +16,34 @@
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { List, ListItem, ListProps } from '@material-ui/core';
|
||||
import { List, ListProps } from '@material-ui/core';
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Progress,
|
||||
EmptyState,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { AnalyticsContext } from '@backstage/core-plugin-api';
|
||||
import { SearchQuery, SearchResult } from '@backstage/plugin-search-common';
|
||||
import { SearchResult } from '@backstage/plugin-search-common';
|
||||
|
||||
import { useSearchResultListItemExtensions } from '../../extensions';
|
||||
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
import { SearchResultState } from '../SearchResult';
|
||||
import { SearchResultState, SearchResultStateProps } from '../SearchResult';
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultListLayout}
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultListLayoutProps = ListProps & {
|
||||
/**
|
||||
* If defined, will render a default error panel.
|
||||
*/
|
||||
error?: Error;
|
||||
/**
|
||||
* If defined, will render a default loading progress.
|
||||
*/
|
||||
loading?: boolean;
|
||||
/**
|
||||
* Search results to be rendered as a list.
|
||||
*/
|
||||
@@ -46,18 +56,14 @@ export type SearchResultListLayoutProps = ListProps & {
|
||||
index: number,
|
||||
array: SearchResult[],
|
||||
) => JSX.Element | null;
|
||||
/**
|
||||
* If defined, will render a default error panel.
|
||||
*/
|
||||
error?: Error;
|
||||
/**
|
||||
* If defined, will render a default loading progress.
|
||||
*/
|
||||
loading?: boolean;
|
||||
/**
|
||||
* Optional component to render when no results. Default to <EmptyState /> component.
|
||||
*/
|
||||
noResultsComponent?: ReactNode;
|
||||
/**
|
||||
* Optional property to provide if component should not render the component when no results are found.
|
||||
*/
|
||||
disableRenderingWithNoResults?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -67,8 +73,8 @@ export type SearchResultListLayoutProps = ListProps & {
|
||||
*/
|
||||
export const SearchResultListLayout = (props: SearchResultListLayoutProps) => {
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
loading,
|
||||
resultItems,
|
||||
renderResultItem = resultItem => (
|
||||
<DefaultResultListItem
|
||||
@@ -76,48 +82,39 @@ export const SearchResultListLayout = (props: SearchResultListLayoutProps) => {
|
||||
result={resultItem.document}
|
||||
/>
|
||||
),
|
||||
noResultsComponent = (
|
||||
disableRenderingWithNoResults,
|
||||
noResultsComponent = disableRenderingWithNoResults ? null : (
|
||||
<EmptyState missing="data" title="Sorry, no results were found" />
|
||||
),
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<List {...rest}>
|
||||
{loading ? <Progress /> : null}
|
||||
{!loading && error ? (
|
||||
<ResponseErrorPanel
|
||||
title="Error encountered while fetching search results"
|
||||
error={error}
|
||||
/>
|
||||
) : null}
|
||||
{!loading && !error && resultItems?.length
|
||||
? resultItems.map(renderResultItem)
|
||||
: null}
|
||||
{!loading && !error && !resultItems?.length ? (
|
||||
<ListItem>{noResultsComponent}</ListItem>
|
||||
) : null}
|
||||
</List>
|
||||
);
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ResponseErrorPanel
|
||||
title="Error encountered while fetching search results"
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!resultItems?.length) {
|
||||
return <>{noResultsComponent}</>;
|
||||
}
|
||||
|
||||
return <List {...rest}>{resultItems.map(renderResultItem)}</List>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultList}.
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultListProps = Omit<
|
||||
SearchResultListLayoutProps,
|
||||
'loading' | 'error' | 'resultItems'
|
||||
> & {
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
export type SearchResultListProps = Pick<SearchResultStateProps, 'query'> &
|
||||
Omit<SearchResultListLayoutProps, 'loading' | 'error' | 'resultItems'>;
|
||||
|
||||
/**
|
||||
* Given a query, search for results and render them as a list.
|
||||
@@ -125,7 +122,9 @@ export type SearchResultListProps = Omit<
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultList = (props: SearchResultListProps) => {
|
||||
const { query, disableRenderingWithNoResults, ...rest } = props;
|
||||
const { query, renderResultItem, children, ...rest } = props;
|
||||
|
||||
const defaultRenderResultItem = useSearchResultListItemExtensions(children);
|
||||
|
||||
return (
|
||||
<AnalyticsContext
|
||||
@@ -135,20 +134,15 @@ export const SearchResultList = (props: SearchResultListProps) => {
|
||||
}}
|
||||
>
|
||||
<SearchResultState query={query}>
|
||||
{({ loading, error, value }) => {
|
||||
if (!value?.results?.length && disableRenderingWithNoResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchResultListLayout
|
||||
{...rest}
|
||||
loading={loading}
|
||||
error={error}
|
||||
resultItems={value?.results}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
{({ loading, error, value }) => (
|
||||
<SearchResultListLayout
|
||||
{...rest}
|
||||
error={error}
|
||||
loading={loading}
|
||||
resultItems={value?.results}
|
||||
renderResultItem={renderResultItem ?? defaultRenderResultItem}
|
||||
/>
|
||||
)}
|
||||
</SearchResultState>
|
||||
</AnalyticsContext>
|
||||
);
|
||||
|
||||
@@ -18,9 +18,9 @@ export * from './HighlightedSearchResultText';
|
||||
export * from './SearchBar';
|
||||
export * from './SearchAutocomplete';
|
||||
export * from './SearchFilter';
|
||||
export * from './SearchResult';
|
||||
export * from './SearchResultPager';
|
||||
export * from './SearchPagination';
|
||||
export * from './SearchResult';
|
||||
export * from './SearchResultList';
|
||||
export * from './SearchResultGroup';
|
||||
export * from './SearchResultPager';
|
||||
export * from './DefaultResultListItem';
|
||||
|
||||
Reference in New Issue
Block a user