feat(search-react): add search result query prop
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -15,15 +15,19 @@
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import { List, ListItem } from '@material-ui/core';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
|
||||
import { List, ListItem } from '@material-ui/core';
|
||||
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { SearchDocument } from '@backstage/plugin-search-common';
|
||||
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
|
||||
import { SearchResult } from './SearchResult';
|
||||
|
||||
const mockResults = {
|
||||
@@ -55,15 +59,15 @@ const mockResults = {
|
||||
],
|
||||
};
|
||||
|
||||
const searchApiMock = new MockSearchApi(mockResults);
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Search/SearchResult',
|
||||
component: SearchResult,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
<MemoryRouter>
|
||||
<TestApiProvider
|
||||
apis={[[searchApiRef, new MockSearchApi(mockResults)]]}
|
||||
>
|
||||
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
|
||||
<SearchContextProvider>
|
||||
<Story />
|
||||
</SearchContextProvider>
|
||||
@@ -73,6 +77,17 @@ export default {
|
||||
],
|
||||
};
|
||||
|
||||
const CustomResultListItem = (props: { result: SearchDocument }) => {
|
||||
const { result } = props;
|
||||
return (
|
||||
<ListItem>
|
||||
<Link to={result.location}>
|
||||
{result.title} - {result.text}
|
||||
</Link>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<SearchResult>
|
||||
@@ -82,18 +97,50 @@ export const Default = () => {
|
||||
switch (type) {
|
||||
case 'custom-result-item':
|
||||
return (
|
||||
<DefaultResultListItem
|
||||
<CustomResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<ListItem>
|
||||
<Link to={document.location}>
|
||||
{document.title} - {document.text}
|
||||
</Link>
|
||||
</ListItem>
|
||||
<DefaultResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</SearchResult>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithQuery = () => {
|
||||
const query = {
|
||||
term: 'documentation',
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchResult query={query}>
|
||||
{({ results }) => (
|
||||
<List>
|
||||
{results.map(({ type, document }) => {
|
||||
switch (type) {
|
||||
case 'custom-result-item':
|
||||
return (
|
||||
<CustomResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<DefaultResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -15,69 +15,215 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Progress,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { AnalyticsContext } from '@backstage/core-plugin-api';
|
||||
import { SearchResult } from '@backstage/plugin-search-common';
|
||||
import { AnalyticsContext, useApi } from '@backstage/core-plugin-api';
|
||||
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
|
||||
|
||||
import { useSearch } from '../../context';
|
||||
import { searchApiRef } from '../../api';
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultComponent}
|
||||
*
|
||||
* Props for {@link SearchResultContext}
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultProps = {
|
||||
children: (results: { results: SearchResult[] }) => JSX.Element;
|
||||
export type SearchResultContextProps = {
|
||||
/**
|
||||
* A child function that receives an asynchronous result set and returns a react element.
|
||||
*/
|
||||
children: (state: AsyncState<SearchResultSet>) => JSX.Element;
|
||||
};
|
||||
|
||||
/**
|
||||
* A component returning the search result.
|
||||
*
|
||||
* Provides context-based results to a child function.
|
||||
* @param props - see {@link SearchResultContextProps}.
|
||||
* @example
|
||||
* ```
|
||||
* <SearchResultContext>
|
||||
* {({ loading, error, value }) => (
|
||||
* <List>
|
||||
* {value?.map(({ document }) => (
|
||||
* <DefaultSearchResultListItem
|
||||
* key={document.location}
|
||||
* result={document}
|
||||
* />
|
||||
* ))}
|
||||
* </List>
|
||||
* )}
|
||||
* </SearchResultContext>
|
||||
* ```
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultComponent = ({ children }: SearchResultProps) => {
|
||||
const {
|
||||
result: { loading, error, value },
|
||||
} = useSearch();
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<ResponseErrorPanel
|
||||
title="Error encountered while fetching search results"
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value?.results.length) {
|
||||
return <EmptyState missing="data" title="Sorry, no results were found" />;
|
||||
}
|
||||
|
||||
return <>{children({ results: value.results })}</>;
|
||||
export const SearchResultContext = (props: SearchResultContextProps) => {
|
||||
const { children } = props;
|
||||
const context = useSearch();
|
||||
const state = context.result;
|
||||
return children(state);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultApi}
|
||||
* @public
|
||||
*/
|
||||
const HigherOrderSearchResult = (props: SearchResultProps) => {
|
||||
return (
|
||||
<AnalyticsContext
|
||||
attributes={{
|
||||
pluginId: 'search',
|
||||
extension: 'SearchResult',
|
||||
}}
|
||||
>
|
||||
<SearchResultComponent {...props} />
|
||||
</AnalyticsContext>
|
||||
export type SearchResultApiProps = SearchResultContextProps & {
|
||||
query: Partial<SearchQuery>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Request results through the search api and provide them to a child function.
|
||||
* @param props - see {@link SearchResultApiProps}.
|
||||
* @example
|
||||
* ```
|
||||
* <SearchResultApi>
|
||||
* {({ loading, error, value }) => (
|
||||
* <List>
|
||||
* {value?.map(({ document }) => (
|
||||
* <DefaultSearchResultListItem
|
||||
* key={document.location}
|
||||
* result={document}
|
||||
* />
|
||||
* ))}
|
||||
* </List>
|
||||
* )}
|
||||
* </SearchResultApi>
|
||||
* ```
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultApi = (props: SearchResultApiProps) => {
|
||||
const { query, children } = props;
|
||||
const searchApi = useApi(searchApiRef);
|
||||
|
||||
const state = useAsync(
|
||||
() =>
|
||||
searchApi.query({
|
||||
term: query.term ?? '',
|
||||
types: query.types ?? [],
|
||||
filters: query.filters ?? {},
|
||||
pageCursor: query.pageCursor,
|
||||
}),
|
||||
[query],
|
||||
);
|
||||
|
||||
return children(state);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultState}
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultStateProps = SearchResultContextProps &
|
||||
Partial<SearchResultApiProps>;
|
||||
|
||||
/**
|
||||
* Call a child render function passing a search state as an argument.
|
||||
* @remarks By default, results are taken from context, but when a "query" prop is set, results are requested from the search api.
|
||||
* @param props - see {@link SearchResultStateProps}.
|
||||
* @example
|
||||
* Consuming results from context:
|
||||
* ```
|
||||
* <SearchResultState>
|
||||
* {({ loading, error, value }) => (
|
||||
* <List>
|
||||
* {value?.map(({ document }) => (
|
||||
* <DefaultSearchResultListItem
|
||||
* key={document.location}
|
||||
* result={document}
|
||||
* />
|
||||
* ))}
|
||||
* </List>
|
||||
* )}
|
||||
* </SearchResultState>
|
||||
* ```
|
||||
* @example
|
||||
* Requesting results using the search api:
|
||||
* ```
|
||||
* <SearchResultState query={{ term: 'documentation' }}>
|
||||
* {({ loading, error, value }) => (
|
||||
* <List>
|
||||
* {value?.map(({ document }) => (
|
||||
* <DefaultSearchResultListItem
|
||||
* key={document.location}
|
||||
* result={document}
|
||||
* />
|
||||
* ))}
|
||||
* </List>
|
||||
* )}
|
||||
* </SearchResultState>
|
||||
* ```
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultState = (props: SearchResultStateProps) => {
|
||||
const { query, children } = props;
|
||||
|
||||
return query ? (
|
||||
<SearchResultApi query={query}>{children}</SearchResultApi>
|
||||
) : (
|
||||
<SearchResultContext>{children}</SearchResultContext>
|
||||
);
|
||||
};
|
||||
|
||||
export { HigherOrderSearchResult as SearchResult };
|
||||
/**
|
||||
* Props for {@link SearchResult}
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultProps = Pick<SearchResultStateProps, 'query'> & {
|
||||
children: (resultSet: SearchResultSet) => JSX.Element;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders results from a parent search context or api.
|
||||
* @remarks default components for loading, error and empty variants are returned.
|
||||
* @param props - see {@link SearchResultProps}.
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultComponent = (props: SearchResultProps) => {
|
||||
const { query, children } = props;
|
||||
|
||||
return (
|
||||
<SearchResultState query={query}>
|
||||
{({ loading, error, value }) => {
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ResponseErrorPanel
|
||||
title="Error encountered while fetching search results"
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value?.results.length) {
|
||||
return (
|
||||
<EmptyState missing="data" title="Sorry, no results were found" />
|
||||
);
|
||||
}
|
||||
|
||||
return children(value);
|
||||
}}
|
||||
</SearchResultState>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A component returning the search result from a parent search context or api.
|
||||
* @param props - see {@link SearchResultProps}.
|
||||
* @public
|
||||
*/
|
||||
export const SearchResult = (props: SearchResultProps) => (
|
||||
<AnalyticsContext
|
||||
attributes={{
|
||||
pluginId: 'search',
|
||||
extension: 'SearchResult',
|
||||
}}
|
||||
>
|
||||
<SearchResultComponent {...props} />
|
||||
</AnalyticsContext>
|
||||
);
|
||||
|
||||
@@ -14,5 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { SearchResult, SearchResultComponent } from './SearchResult';
|
||||
export type { SearchResultProps } from './SearchResult';
|
||||
export {
|
||||
SearchResult,
|
||||
SearchResultApi,
|
||||
SearchResultContext,
|
||||
SearchResultState,
|
||||
SearchResultComponent,
|
||||
} from './SearchResult';
|
||||
|
||||
export type {
|
||||
SearchResultProps,
|
||||
SearchResultApiProps,
|
||||
SearchResultContextProps,
|
||||
SearchResultStateProps,
|
||||
} from './SearchResult';
|
||||
|
||||
Reference in New Issue
Block a user