From 5c5c79ec758f6c1c06047f743a7fef9d0fcec04e Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 30 Sep 2022 14:52:33 +0200 Subject: [PATCH 1/6] Optional property for custom no result component Co-authored-by: Camila Loiola Signed-off-by: Emma Indal --- .../components/SearchResult/SearchResult.tsx | 2 +- .../SearchResultGroup/SearchResultGroup.tsx | 43 +++++++++++++------ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx index a3e4c0e1f0..40c60c3263 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -36,7 +36,7 @@ export type SearchResultContextProps = { /** * A child function that receives an asynchronous result set and returns a react element. */ - children: (state: AsyncState) => JSX.Element; + children: (state: AsyncState) => JSX.Element | null; }; /** diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx index 9c55996ca7..2a82dd7fee 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx @@ -282,6 +282,10 @@ export type SearchResultGroupLayoutProps = ListProps & { * Icon that representing a result group. */ icon: JSX.Element; + /** + * Optional component to render when no results. Default to component. + */ + noResultsComponent?: React.ReactNode; /** * The results group title content, it could be a text or an element. */ @@ -347,6 +351,9 @@ export function SearchResultGroupLayout( const { loading, error, + noResultsComponent = ( + + ), icon, title, titleProps = {}, @@ -438,9 +445,7 @@ export function SearchResultGroupLayout( ? resultItems.map(resultItem => renderResultItem?.(resultItem) ?? null) : null} {!loading && !error && !resultItems?.length ? ( - - - + {noResultsComponent} ) : null} ); @@ -458,6 +463,10 @@ export type SearchResultGroupProps = Omit< * A search query used for requesting the results to be grouped. */ query: Partial; + /** + * Optional property to provide if component should not render the group when no results are found. + */ + disableRenderingWithNoResults?: boolean; }; /** @@ -474,6 +483,7 @@ export function SearchResultGroup( renderResultItem = ({ document }) => ( ), + disableRenderingWithNoResults, ...rest } = props; @@ -495,17 +505,22 @@ export function SearchResultGroup( }} > - {({ loading, error, value }) => ( - - )} + {({ loading, error, value }) => { + if (disableRenderingWithNoResults && value?.results.length === 0) + return null; + + return ( + + ); + }} ); From 523f32778c8f0a6802c345f6f4e771471d66f634 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 30 Sep 2022 14:53:35 +0200 Subject: [PATCH 2/6] Storybook examples for custom no result component Co-authored-by: Camila Loiola Signed-off-by: Emma Indal --- .../SearchResultGroup.stories.tsx | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx index 3a20dec36d..588033a4a3 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx @@ -269,7 +269,7 @@ export const WithFilters = () => { ); }; -export const WithNoResults = () => { +export const WithDefaultNoResultsComponent = () => { const [query] = useState>({ types: ['techdocs'], }); @@ -285,6 +285,40 @@ export const WithNoResults = () => { ); }; +export const WithCustomNoResultsComponent = () => { + const [query] = useState>({ + types: ['techdocs'], + }); + + return ( + + } + title="Documentation" + noResultsComponent="No results were found" + /> + + ); +}; + +export const DisableRenderingWithNoResults = () => { + const [query] = useState>({ + types: ['techdocs'], + }); + + return ( + + } + title="Documentation" + disableRenderingWithNoResults + /> + + ); +}; + const CustomResultListItem = (props: any) => { const { icon, result } = props; From bd9f398150381ce928b1939550d87e966ef6b8ce Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 30 Sep 2022 14:53:59 +0200 Subject: [PATCH 3/6] Tests for custom no result component Co-authored-by: Camila Loiola Signed-off-by: Emma Indal --- .../SearchResultGroup.test.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx index c1b45cc433..7943dba3f7 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx @@ -291,6 +291,46 @@ describe('SearchResultGroup', () => { }); }); + it('Does not render result group if no results returned and disableRenderingWithNoResults prop is provided', async () => { + query.mockResolvedValueOnce({ results: [] }); + await renderWithEffects( + wrapInTestApp( + + } + title="Documentation" + disableRenderingWithNoResults + /> + , + ), + ); + + await waitFor(() => { + expect(screen.queryByText('Documentation')).not.toBeInTheDocument(); + }); + }); + + it('Should render custom component when no results returned', async () => { + query.mockResolvedValueOnce({ results: [] }); + await renderWithEffects( + wrapInTestApp( + + } + title="Documentation" + noResultsComponent="No results were found" + /> + , + ), + ); + + 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( From 86e899b9775b34af8e83c039f5d2b7f6e050d099 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 30 Sep 2022 14:55:26 +0200 Subject: [PATCH 4/6] Be able to pass through index used for index ranking Co-authored-by: Camila Loiola Signed-off-by: Emma Indal --- .../SearchResultGroup/SearchResultGroup.tsx | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx index 2a82dd7fee..6040dbe560 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx @@ -282,10 +282,6 @@ export type SearchResultGroupLayoutProps = ListProps & { * Icon that representing a result group. */ icon: JSX.Element; - /** - * Optional component to render when no results. Default to component. - */ - noResultsComponent?: React.ReactNode; /** * The results group title content, it could be a text or an element. */ @@ -310,7 +306,11 @@ export type SearchResultGroupLayoutProps = ListProps & { * Function to customize how filter options are rendered. * @remarks Defaults to a menu item where its value and label bounds to the option string. */ - renderFilterOption?: (filterOption: FilterOption) => JSX.Element; + renderFilterOption?: ( + value: FilterOption, + index: number, + array: FilterOption[], + ) => JSX.Element | null; /** * A list of search filter keys, also known as filter field names. */ @@ -326,7 +326,11 @@ export type SearchResultGroupLayoutProps = 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. */ @@ -335,6 +339,10 @@ export type SearchResultGroupLayoutProps = ListProps & { * If defined, will render a default loading progress. */ loading?: boolean; + /** + * Optional component to render when no results. Default to component. + */ + noResultsComponent?: ReactNode; }; /** @@ -351,20 +359,34 @@ export function SearchResultGroupLayout( const { loading, error, - noResultsComponent = ( - - ), icon, title, titleProps = {}, - link, + link = ( + <> + See all + + + ), linkProps = {}, filterOptions, - renderFilterOption, + renderFilterOption = filterOption => ( + + {filterOption} + + ), filterFields, renderFilterField, resultItems, - renderResultItem, + renderResultItem = resultItem => ( + + ), + noResultsComponent = ( + + ), ...rest } = props; @@ -408,30 +430,14 @@ export function SearchResultGroupLayout( onClick={handleClose} keepMounted > - {filterOptions.map(filterOption => - renderFilterOption ? ( - renderFilterOption(filterOption) - ) : ( - - {filterOption} - - ), - )} + {filterOptions.map(renderFilterOption)} ) : null} {filterFields?.map( filterField => renderFilterField?.(filterField) ?? null, )} - {link ?? ( - <> - See all - - - )} + {link} {loading ? : null} @@ -442,7 +448,7 @@ export function SearchResultGroupLayout( /> ) : null} {!loading && !error && resultItems?.length - ? resultItems.map(resultItem => renderResultItem?.(resultItem) ?? null) + ? resultItems.map(renderResultItem) : null} {!loading && !error && !resultItems?.length ? ( {noResultsComponent} @@ -480,9 +486,6 @@ export function SearchResultGroup( const { query, linkProps = {}, - renderResultItem = ({ document }) => ( - - ), disableRenderingWithNoResults, ...rest } = props; @@ -506,8 +509,9 @@ export function SearchResultGroup( > {({ loading, error, value }) => { - if (disableRenderingWithNoResults && value?.results.length === 0) + if (!value?.results?.length && disableRenderingWithNoResults) { return null; + } return ( ( error={error} linkProps={{ to, ...linkProps }} resultItems={value?.results} - renderResultItem={renderResultItem} filterFields={Object.keys(query.filters ?? {})} /> ); From 6cbe962a5ff72779e510511d4654e864a6dffbb8 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 30 Sep 2022 15:01:33 +0200 Subject: [PATCH 5/6] Fixup storybook examples Co-authored-by: Camila Loiola Signed-off-by: Emma Indal --- .../SearchResultGroup.stories.tsx | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx index 588033a4a3..fab807dd84 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.stories.tsx @@ -296,24 +296,7 @@ export const WithCustomNoResultsComponent = () => { query={query} icon={} title="Documentation" - noResultsComponent="No results were found" - /> - - ); -}; - -export const DisableRenderingWithNoResults = () => { - const [query] = useState>({ - types: ['techdocs'], - }); - - return ( - - } - title="Documentation" - disableRenderingWithNoResults + noResultsComponent={} /> ); From 6faaa056260d0457f37e95f430b96f50da729717 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 30 Sep 2022 15:08:05 +0200 Subject: [PATCH 6/6] changeset and api report updates Co-authored-by: Camila Loiola Signed-off-by: Emma Indal --- .changeset/search-dull-planes-prove.md | 30 ++++++++++++++++++++++++++ plugins/search-react/api-report.md | 22 ++++++++++++++----- 2 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 .changeset/search-dull-planes-prove.md diff --git a/.changeset/search-dull-planes-prove.md b/.changeset/search-dull-planes-prove.md new file mode 100644 index 0000000000..f185207ede --- /dev/null +++ b/.changeset/search-dull-planes-prove.md @@ -0,0 +1,30 @@ +--- +'@backstage/plugin-search-react': minor +--- + +The `` 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 +} + title="Documentation" + noResultsComponent={} +/> +``` + +_Disable rendering when there are no results_ + +```jsx +} + title="Documentation" + disableRenderingWithNoResults +/> +``` diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 5ec4124fcc..194b285e49 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -213,7 +213,9 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { export const SearchResult: (props: SearchResultProps) => JSX.Element; // @public -export const SearchResultApi: (props: SearchResultApiProps) => JSX.Element; +export const SearchResultApi: ( + props: SearchResultApiProps, +) => JSX.Element | null; // @public export type SearchResultApiProps = SearchResultContextProps & { @@ -226,11 +228,11 @@ export const SearchResultComponent: (props: SearchResultProps) => JSX.Element; // @public export const SearchResultContext: ( props: SearchResultContextProps, -) => JSX.Element; +) => JSX.Element | null; // @public export type SearchResultContextProps = { - children: (state: AsyncState) => JSX.Element; + children: (state: AsyncState) => JSX.Element | null; }; // @public @@ -269,13 +271,22 @@ export type SearchResultGroupLayoutProps = ListProps & { link?: ReactNode; linkProps?: Partial; filterOptions?: FilterOption[]; - renderFilterOption?: (filterOption: FilterOption) => JSX.Element; + renderFilterOption?: ( + value: FilterOption, + index: number, + array: FilterOption[], + ) => JSX.Element | null; filterFields?: string[]; renderFilterField?: (key: string) => JSX.Element | null; 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 @@ -284,6 +295,7 @@ export type SearchResultGroupProps = Omit< 'loading' | 'error' | 'resultItems' | 'filterFields' > & { query: Partial; + disableRenderingWithNoResults?: boolean; }; // @public