diff --git a/.changeset/fast-dragons-turn.md b/.changeset/fast-dragons-turn.md new file mode 100644 index 0000000000..fd9c3622a1 --- /dev/null +++ b/.changeset/fast-dragons-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Support customizing the content of the `SidebarSearchModal` diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index e3c36c9329..a51ca20c37 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -49,6 +49,7 @@ import { } from '@backstage/core-components'; import { MyGroupsSidebarItem } from '@backstage/plugin-org'; import GroupIcon from '@material-ui/icons/People'; +import { SearchModal } from '../search/SearchModal'; const useSidebarLogoStyles = makeStyles({ root: { @@ -88,7 +89,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( } to="/search"> - + + {({ toggleModal }) => } + }> diff --git a/packages/app/src/components/search/SearchModal.tsx b/packages/app/src/components/search/SearchModal.tsx new file mode 100644 index 0000000000..2e18a289c9 --- /dev/null +++ b/packages/app/src/components/search/SearchModal.tsx @@ -0,0 +1,237 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + DialogActions, + DialogContent, + DialogTitle, + Grid, + List, + makeStyles, + Paper, + useTheme, +} from '@material-ui/core'; +import LaunchIcon from '@material-ui/icons/Launch'; +import { Link, useContent } from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; +import { + catalogApiRef, + CATALOG_FILTER_EXISTS, +} from '@backstage/plugin-catalog-react'; +import { + DefaultResultListItem, + SearchBar, + SearchFilter, + searchPlugin, + SearchResult, + SearchResultPager, + SearchType, + useSearch, +} from '@backstage/plugin-search'; +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; + +const useStyles = makeStyles(theme => ({ + container: { + borderRadius: 30, + display: 'flex', + height: '2.4em', + }, + filter: { + '& + &': { + marginTop: theme.spacing(2.5), + }, + }, + filters: { + padding: theme.spacing(2), + marginTop: theme.spacing(2), + }, + input: { + flex: 1, + }, + dialogActionsContainer: { padding: theme.spacing(1, 3) }, + viewResultsLink: { verticalAlign: '0.5em' }, +})); + +export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { + const getSearchLink = useRouteRef(searchPlugin.routes.root); + const classes = useStyles(); + + const catalogApi = useApi(catalogApiRef); + const { term, types } = useSearch(); + const { focusContent } = useContent(); + const { transitions } = useTheme(); + + const handleResultClick = () => { + toggleModal(); + setTimeout(focusContent, transitions.duration.leavingScreen); + }; + + const handleKeyPress = () => { + handleResultClick(); + }; + + return ( + <> + + + + + + + + + + + + {types.includes('techdocs') && ( + + { + // Return a list of entities which are documented. + const { items } = await catalogApi.getEntities({ + fields: ['metadata.name'], + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, + }); + + const names = items.map(entity => entity.metadata.name); + names.sort(); + return names; + }} + /> + + )} + + + + + + + + + { + toggleModal(); + setTimeout( + focusContent, + transitions.duration.leavingScreen, + ); + }} + to={`${getSearchLink()}?query=${term}`} + > + + View Full Results + + + + + + + + + {({ results }) => ( + + {results.map(({ type, document }) => { + let resultItem; + switch (type) { + case 'software-catalog': + resultItem = ( + + ); + break; + case 'techdocs': + resultItem = ( + + ); + break; + default: + resultItem = ( + + ); + } + return ( +
+ {resultItem} +
+ ); + })} +
+ )} +
+
+
+
+ + + + + + + + + ); +}; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 1bdd918802..5b2a1bb59a 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -211,12 +211,19 @@ export const SearchModal: ({ open, hidden, toggleModal, + children, }: SearchModalProps) => JSX.Element; +// @public (undocumented) +export interface SearchModalChildrenProps { + toggleModal: () => void; +} + // Warning: (ae-missing-release-tag) "SearchModalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export interface SearchModalProps { + children?: (props: SearchModalChildrenProps) => JSX.Element; hidden?: boolean; open?: boolean; toggleModal: () => void; @@ -313,6 +320,7 @@ export const SidebarSearchModal: ( // @public (undocumented) export type SidebarSearchModalProps = { icon?: IconComponent; + children?: (props: SearchModalChildrenProps) => JSX.Element; }; // Warning: (ae-missing-release-tag) "SidebarSearchProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index aa0ad0479a..7ce4c9f0c6 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -15,11 +15,25 @@ */ import { wrapInTestApp } from '@backstage/test-utils'; -import { Button } from '@material-ui/core'; +import { + Button, + DialogActions, + DialogContent, + DialogTitle, + Grid, + List, + Paper, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; import React, { ComponentType } from 'react'; import { rootRouteRef } from '../../plugin'; +import { DefaultResultListItem } from '../DefaultResultListItem'; +import { SearchBar } from '../SearchBar'; import { SearchApiProvider } from '../SearchContext/SearchContextForStorybook.stories'; import { SearchModal } from './SearchModal'; +import { SearchResult } from '../SearchResult'; +import { SearchResultPager } from '../SearchResultPager'; +import { SearchType } from '../SearchType'; import { useSearchModal } from './useSearchModal'; const mockResults = { @@ -77,3 +91,87 @@ export const Default = () => { ); }; + +const useStyles = makeStyles(theme => ({ + container: { + borderRadius: 30, + display: 'flex', + height: '2.4em', + }, + input: { + flex: 1, + }, + dialogActionsContainer: { padding: theme.spacing(1, 3) }, +})); + +export const CustomModal = () => { + const classes = useStyles(); + const { state, toggleModal } = useSearchModal(); + + return ( + <> + + + {() => ( + <> + + + + + + + + + + + + + {({ results }) => ( + + {results.map(({ document }) => ( +
+ +
+ ))} +
+ )} +
+
+
+
+ + + + + + + + + )} +
+ + ); +}; diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index 9b3f04ad46..c50d8af1eb 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -55,6 +55,23 @@ describe('SearchModal', () => { expect(query).toHaveBeenCalledTimes(1); }); + it('Should render a custom Modal correctly', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + + expect(screen.getByText('Custom Search Modal')).toBeInTheDocument(); + }); + it('Calls toggleModal handler', async () => { await renderInTestApp( diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index 2a71e53ea8..67b99e0926 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -37,6 +37,16 @@ import { useRouteRef } from '@backstage/core-plugin-api'; import { Link, useContent } from '@backstage/core-components'; import { rootRouteRef } from '../../plugin'; +/** + * @public + **/ +export interface SearchModalChildrenProps { + /** + * A function that should be invoked when navigating away from the modal. + */ + toggleModal: () => void; +} + export interface SearchModalProps { /** * If true, it renders the modal. @@ -54,6 +64,11 @@ export interface SearchModalProps { * should be closed. */ toggleModal: () => void; + /** + * A function that returns custom content to render in the search modal in + * place of the default. + */ + children?: (props: SearchModalChildrenProps) => JSX.Element; } const useStyles = makeStyles(theme => ({ @@ -152,6 +167,7 @@ export const SearchModal = ({ open = true, hidden, toggleModal, + children, }: SearchModalProps) => { const classes = useStyles(); @@ -169,7 +185,9 @@ export const SearchModal = ({ > {open && ( - + {(children && children({ toggleModal })) ?? ( + + )} )} diff --git a/plugins/search/src/components/SearchModal/index.tsx b/plugins/search/src/components/SearchModal/index.tsx index 646dff0a62..51e5d94c07 100644 --- a/plugins/search/src/components/SearchModal/index.tsx +++ b/plugins/search/src/components/SearchModal/index.tsx @@ -14,5 +14,5 @@ * limitations under the License. */ export { SearchModal } from './SearchModal'; -export type { SearchModalProps } from './SearchModal'; +export type { SearchModalChildrenProps, SearchModalProps } from './SearchModal'; export { useSearchModal } from './useSearchModal'; diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.tsx index 465afd2de4..09797ae8f5 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.tsx @@ -77,8 +77,9 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => { value={types.length === 0 ? '' : types[0]} onChange={changeTab} > - {definedTypes.map(type => ( + {definedTypes.map((type, idx) => ( JSX.Element; }; export const SidebarSearchModal = (props: SidebarSearchModalProps) => { @@ -35,7 +40,11 @@ export const SidebarSearchModal = (props: SidebarSearchModalProps) => { text="Search" onClick={toggleModal} /> - + ); }; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index cef0355fb4..bfffb9e84c 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -40,7 +40,10 @@ export type { SearchFilterWrapperProps, } from './components/SearchFilter'; export { SearchModal, useSearchModal } from './components/SearchModal'; -export type { SearchModalProps } from './components/SearchModal'; +export type { + SearchModalChildrenProps, + SearchModalProps, +} from './components/SearchModal'; export { SearchPage as Router } from './components/SearchPage'; export { SearchResultPager } from './components/SearchResultPager'; export { SearchType } from './components/SearchType';