Merge pull request #26060 from joshjung/jjung/custom-modal-search-items
Add ability to customize SearchResult children via SidebarSearchModal component
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Added ability to customize the search items within the SidebarSearchModal
|
||||
@@ -160,11 +160,14 @@ const highlightOverride = {
|
||||
|
||||
## How to render search results using extensions
|
||||
|
||||
Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages:
|
||||
Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages.
|
||||
|
||||
### 1. Providing an extension in your plugin package
|
||||
|
||||
Using the example below, you can provide an extension to be used as a default result item:
|
||||
> Note: You must use the `plugin.provide()` function to make a search item renderer available. Unlike rendering a list in a standard MUI Table or similar, you cannot simply provide
|
||||
> a rendering function to the `<SearchResult />` component.
|
||||
|
||||
Using the example below, you can provide an extension to be used as a search result item:
|
||||
|
||||
```tsx title="plugins/your-plugin/src/plugin.ts"
|
||||
import { createPlugin } from '@backstage/core-plugin-api';
|
||||
@@ -214,7 +217,7 @@ export const YourSearchResultListItemExtension = plugin.provide(
|
||||
);
|
||||
```
|
||||
|
||||
Remember to export your new extension:
|
||||
Remember to export your new extension via your plugin's `index.ts` so that it is available from within your app:
|
||||
|
||||
```tsx title="plugins/your-plugin/src/index.ts"
|
||||
export { YourSearchResultListItem } from './plugin.ts';
|
||||
@@ -222,9 +225,12 @@ export { YourSearchResultListItem } from './plugin.ts';
|
||||
|
||||
For more details, see the [createSearchResultListItemExtension](https://backstage.io/docs/reference/plugin-search-react.createsearchresultlistitemextension) API reference.
|
||||
|
||||
### 2. Using an extension in your Backstage app
|
||||
### 2. Custom search result extension in the SearchPage
|
||||
|
||||
Now that you know how a search result item is provided, let's finally see how they can be used, for example, to compose a page in your application:
|
||||
Once you have exposed your item renderer via the `plugin.provide()` function, you can now override the default search item renderers and tell the `<SearchResult>` component
|
||||
which renderers to use. Note that the order of the renderers matters! The first one that matches via its predicate function will be used.
|
||||
|
||||
Here is an example of customizing your `SearchPage`:
|
||||
|
||||
```tsx title="packages/app/src/components/searchPage.tsx"
|
||||
import React from 'react';
|
||||
@@ -276,9 +282,38 @@ const SearchPage = () => (
|
||||
export const searchPage = <SearchPage />;
|
||||
```
|
||||
|
||||
> **Important**: A default result item extension should be placed as the last child, so it can be used only when no other extensions match the result being rendered. If a non-default extension is specified, the `DefaultResultListItem` component will be used.
|
||||
> **Important**: A default result item extension (one that does not have a predicate) should be placed as the last child, so it can be used only when no other extensions match the result being rendered.
|
||||
> If a non-default extension is specified, the `DefaultResultListItem` component will be used.
|
||||
|
||||
As another example, here's a search modal that renders results with extensions:
|
||||
### 2. Custom search result extension in the SidebarSearchModal
|
||||
|
||||
You may be using the SidebarSearchModal component. In this case, you can customize the search items in this component as follows:
|
||||
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
import { SidebarSearchModal } from '@backstage/plugin-search';
|
||||
...
|
||||
export const Root = ({ children }: PropsWithChildren<{}>) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return <SidebarPage>
|
||||
<Sidebar>
|
||||
...
|
||||
<SidebarSearchModal resultItemComponents={[
|
||||
/* Provide a custom Extension search item renderer */
|
||||
<CustomSearchResultListItem icon={<CatalogIcon />} />,
|
||||
/* Provide an existing search item renderer */
|
||||
<TechDocsSearchResultListItem icon={<DocsIcon />} />
|
||||
]} />
|
||||
...
|
||||
</Sidebar>
|
||||
{children}
|
||||
</SidebarPage>;
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Custom search result extension in a custom SearchModal
|
||||
|
||||
Assuming you have completely customized your SearchModal, here's an example that renders results with extensions:
|
||||
|
||||
```tsx title="packages/app/src/components/searchModal.tsx"
|
||||
import React from 'react';
|
||||
|
||||
@@ -30,7 +30,7 @@ This search plugin is primarily responsible for the following:
|
||||
|
||||
- Providing a `<SearchPage />` routable extension.
|
||||
- Exposing various search-related components (like `<SearchModal />`,
|
||||
`<SidebarSearch />`, etc), which can be composed by a Backstage App or by
|
||||
`<SidebarSearch />`, etc.), which can be composed by a Backstage App or by
|
||||
other Backstage Plugins to power search experiences of all kinds.
|
||||
|
||||
Don't forget, a lot of functionality is available in web libraries and backend plugins:
|
||||
|
||||
@@ -12,6 +12,7 @@ import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { SearchBarBaseProps } from '@backstage/plugin-search-react';
|
||||
import { SearchResultSet } from '@backstage/plugin-search-common';
|
||||
|
||||
// @public (undocumented)
|
||||
export const HomePageSearchBar: (
|
||||
@@ -31,6 +32,9 @@ export const SearchModal: (props: SearchModalProps) => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SearchModalChildrenProps {
|
||||
resultItemComponents?:
|
||||
| ReactNode
|
||||
| ((resultSet: SearchResultSet) => JSX.Element);
|
||||
toggleModal: () => void;
|
||||
}
|
||||
|
||||
@@ -39,6 +43,7 @@ export interface SearchModalProps {
|
||||
children?: (props: SearchModalChildrenProps) => JSX.Element;
|
||||
hidden?: boolean;
|
||||
open?: boolean;
|
||||
resultItemComponents?: SearchModalChildrenProps['resultItemComponents'];
|
||||
toggleModal: () => void;
|
||||
}
|
||||
|
||||
@@ -118,12 +123,14 @@ export const SidebarSearch: (props: SidebarSearchProps) => React_2.JSX.Element;
|
||||
// @public (undocumented)
|
||||
export const SidebarSearchModal: (
|
||||
props: SidebarSearchModalProps,
|
||||
) => JSX_2.Element;
|
||||
) => JSX.Element | null;
|
||||
|
||||
// @public
|
||||
export type SidebarSearchModalProps = {
|
||||
export type SidebarSearchModalProps = Pick<
|
||||
SearchModalProps,
|
||||
'children' | 'resultItemComponents'
|
||||
> & {
|
||||
icon?: IconComponent;
|
||||
children?: (props: SearchModalChildrenProps) => JSX.Element;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
@@ -34,10 +34,11 @@ import IconButton from '@material-ui/core/IconButton';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { rootRouteRef } from '../../plugin';
|
||||
import { SearchResultSet } from '@backstage/plugin-search-common';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -47,6 +48,13 @@ export interface SearchModalChildrenProps {
|
||||
* A function that should be invoked when navigating away from the modal.
|
||||
*/
|
||||
toggleModal: () => void;
|
||||
|
||||
/**
|
||||
* Ability to provide custom components to render the result items
|
||||
*/
|
||||
resultItemComponents?:
|
||||
| ReactNode
|
||||
| ((resultSet: SearchResultSet) => JSX.Element);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,6 +82,11 @@ export interface SearchModalProps {
|
||||
* place of the default.
|
||||
*/
|
||||
children?: (props: SearchModalChildrenProps) => JSX.Element;
|
||||
|
||||
/**
|
||||
* Optional ability to pass in result item component renderers.
|
||||
*/
|
||||
resultItemComponents?: SearchModalChildrenProps['resultItemComponents'];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
@@ -100,7 +113,10 @@ const useStyles = makeStyles(theme => ({
|
||||
viewResultsLink: { verticalAlign: '0.5em' },
|
||||
}));
|
||||
|
||||
export const Modal = ({ toggleModal }: SearchModalChildrenProps) => {
|
||||
export const Modal = ({
|
||||
toggleModal,
|
||||
resultItemComponents,
|
||||
}: SearchModalChildrenProps) => {
|
||||
const classes = useStyles();
|
||||
const navigate = useNavigate();
|
||||
const { transitions } = useTheme();
|
||||
@@ -163,7 +179,9 @@ export const Modal = ({ toggleModal }: SearchModalChildrenProps) => {
|
||||
<SearchResult
|
||||
onClick={handleSearchResultClick}
|
||||
onKeyDown={handleSearchResultClick}
|
||||
/>
|
||||
>
|
||||
{resultItemComponents}
|
||||
</SearchResult>
|
||||
</DialogContent>
|
||||
<DialogActions className={classes.dialogActionsContainer}>
|
||||
<Grid container direction="row">
|
||||
@@ -180,7 +198,13 @@ export const Modal = ({ toggleModal }: SearchModalChildrenProps) => {
|
||||
* @public
|
||||
*/
|
||||
export const SearchModal = (props: SearchModalProps) => {
|
||||
const { open = true, hidden, toggleModal, children } = props;
|
||||
const {
|
||||
open = true,
|
||||
hidden,
|
||||
toggleModal,
|
||||
children,
|
||||
resultItemComponents,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
@@ -199,8 +223,15 @@ export const SearchModal = (props: SearchModalProps) => {
|
||||
>
|
||||
{open && (
|
||||
<SearchContextProvider inheritParentContextIfAvailable>
|
||||
{(children && children({ toggleModal })) ?? (
|
||||
<Modal toggleModal={toggleModal} />
|
||||
{(children &&
|
||||
children({
|
||||
toggleModal,
|
||||
resultItemComponents: resultItemComponents || [],
|
||||
})) ?? (
|
||||
<Modal
|
||||
toggleModal={toggleModal}
|
||||
resultItemComponents={resultItemComponents}
|
||||
/>
|
||||
)}
|
||||
</SearchContextProvider>
|
||||
)}
|
||||
|
||||
@@ -19,8 +19,9 @@ import { useSearch } from '@backstage/plugin-search-react';
|
||||
import Tab from '@material-ui/core/Tab';
|
||||
import Tabs from '@material-ui/core/Tabs';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Theme } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
tabs: {
|
||||
borderBottom: `1px solid ${theme.palette.textVerySubtle}`,
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import { SidebarItem } from '@backstage/core-components';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
SearchModal,
|
||||
SearchModalChildrenProps,
|
||||
SearchModalProps,
|
||||
SearchModalProvider,
|
||||
useSearchModal,
|
||||
} from '../SearchModal';
|
||||
@@ -29,9 +29,11 @@ import {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SidebarSearchModalProps = {
|
||||
export type SidebarSearchModalProps = Pick<
|
||||
SearchModalProps,
|
||||
'children' | 'resultItemComponents'
|
||||
> & {
|
||||
icon?: IconComponent;
|
||||
children?: (props: SearchModalChildrenProps) => JSX.Element;
|
||||
};
|
||||
|
||||
const SidebarSearchModalContent = (props: SidebarSearchModalProps) => {
|
||||
@@ -49,6 +51,7 @@ const SidebarSearchModalContent = (props: SidebarSearchModalProps) => {
|
||||
<SearchModal
|
||||
{...state}
|
||||
toggleModal={toggleModal}
|
||||
resultItemComponents={props.resultItemComponents}
|
||||
children={props.children}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
createComponentExtension,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { SidebarSearchModalProps } from './components/SidebarSearchModal';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'search',
|
||||
@@ -63,7 +64,9 @@ export const SearchPage = searchPlugin.provide(
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const SidebarSearchModal = searchPlugin.provide(
|
||||
export const SidebarSearchModal = searchPlugin.provide<
|
||||
(props: SidebarSearchModalProps) => JSX.Element | null
|
||||
>(
|
||||
createComponentExtension({
|
||||
name: 'SidebarSearchModal',
|
||||
component: {
|
||||
|
||||
Reference in New Issue
Block a user