From 8b532a6c02d7148f49569237cafd9e9619e76adc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 27 Dec 2021 18:40:20 +0100 Subject: [PATCH 1/6] Initial SearchTypeFacet implementation. Signed-off-by: Eric Peterson --- .changeset/search-en-jul-hemma.md | 7 + .../app/src/components/search/SearchPage.tsx | 33 +++- plugins/search/api-report.md | 14 ++ .../SearchTypeFacet.stories.tsx | 49 +++++ .../SearchTypeFacet/SearchTypeFacet.tsx | 184 ++++++++++++++++++ .../src/components/SearchTypeFacet/index.ts | 18 ++ plugins/search/src/components/index.tsx | 1 + plugins/search/src/index.ts | 2 + 8 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 .changeset/search-en-jul-hemma.md create mode 100644 plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx create mode 100644 plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx create mode 100644 plugins/search/src/components/SearchTypeFacet/index.ts diff --git a/.changeset/search-en-jul-hemma.md b/.changeset/search-en-jul-hemma.md new file mode 100644 index 0000000000..a2eee15e7c --- /dev/null +++ b/.changeset/search-en-jul-hemma.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-search': patch +--- + +Introduces a `` component, which operates on the same part of a search query as the `` component, but in a more opinionated way (as a single-select control surface suitable for faceted search UIs). + +Check the [search plugin storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchtypefacet--default) to see how it can be used. diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index 54f59d1a6e..e8d590bc92 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -14,7 +14,14 @@ * limitations under the License. */ -import { Content, Header, Lifecycle, Page } from '@backstage/core-components'; +import { + CatalogIcon, + Content, + DocsIcon, + Header, + Lifecycle, + Page, +} from '@backstage/core-components'; import { CatalogResultListItem } from '@backstage/plugin-catalog'; import { DefaultResultListItem, @@ -22,7 +29,7 @@ import { SearchFilter, SearchResult, SearchResultPager, - SearchType, + SearchTypeFacet, } from '@backstage/plugin-search'; import { DocsResultListItem } from '@backstage/plugin-techdocs'; import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; @@ -39,6 +46,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, filters: { padding: theme.spacing(2), + marginTop: theme.spacing(2), }, })); @@ -55,12 +63,23 @@ const SearchPage = () => { + , + }, + { + value: 'techdocs', + name: 'Documentation', + icon: , + }, + ]} + /> - JSX.Element; +// @public +export const SearchTypeFacet: (props: SearchTypeFacetProps) => JSX.Element; + +// @public (undocumented) +export type SearchTypeFacetProps = { + name: string; + types: Array<{ + value: string; + name: string; + icon: JSX.Element; + }>; + defaultValue?: string; +}; + // Warning: (ae-missing-release-tag) "SidebarSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx new file mode 100644 index 0000000000..c83f3d88d7 --- /dev/null +++ b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2021 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, { useState } from 'react'; +import CatalogIcon from '@material-ui/icons/MenuBook'; +import DocsIcon from '@material-ui/icons/Description'; + +import { SearchTypeFacet } from '../index'; +import { SearchContext } from '../SearchContext'; + +export default { + title: 'Plugins/Search/SearchTypeFacet', + component: SearchTypeFacet, +}; + +export const Default = () => { + const [types, setTypes] = useState([]); + + return ( + {} } as any} + > + , + }, + { value: 'techdocs', name: 'Documentation', icon: }, + ]} + /> + + ); +}; diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx new file mode 100644 index 0000000000..e054e0d028 --- /dev/null +++ b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx @@ -0,0 +1,184 @@ +/* + * Copyright 2021 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, { + ChangeEvent, + cloneElement, + Fragment, + useEffect, + useState, +} from 'react'; +import { useSearch } from '../SearchContext'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Card, + CardContent, + CardHeader, + Divider, + List, + ListItem, + ListItemIcon, + ListItemText, + makeStyles, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import AllIcon from '@material-ui/icons/FontDownload'; + +const useStyles = makeStyles(theme => ({ + card: { + backgroundColor: 'rgba(0, 0, 0, .11)', + }, + cardContent: { + paddingTop: theme.spacing(1), + }, + icon: { + color: theme.palette.common.black, + }, + list: { + width: '100%', + }, + listItemIcon: { + width: '24px', + height: '24px', + }, + accordion: { + backgroundColor: theme.palette.background.paper, + }, + accordionSummary: { + minHeight: 'auto', + '&.Mui-expanded': { + minHeight: 'auto', + }, + }, + accordionSummaryContent: { + margin: theme.spacing(2, 0), + '&.Mui-expanded': { + margin: theme.spacing(2, 0), + }, + }, + accordionDetails: { + padding: theme.spacing(0, 0, 1), + }, +})); + +/** + * @public + */ +export type SearchTypeFacetProps = { + /* what about this? */ + name: string; + types: Array<{ + value: string; + name: string; + icon: JSX.Element; + }>; + defaultValue?: string; +}; + +/** + * A control surface for the search query's "types" property, displayed as a + * single-select collapsible accordion suitable for use in faceted search UIs. + * @public + */ +export const SearchTypeFacet = (props: SearchTypeFacetProps) => { + const classes = useStyles(); + const { setPageCursor, setTypes, types } = useSearch(); + const [expanded, setExpanded] = useState(true); + const { defaultValue, name, types: givenTypes } = props; + + const handleChange = (_event: ChangeEvent<{}>, newExpanded: boolean) => + setExpanded(newExpanded); + const handleClick = (type: string) => { + return () => { + setTypes(type !== '' ? [type] : []); + setPageCursor('0'); + setExpanded(false); + }; + }; + + // Handle any provided defaultValue + useEffect(() => { + if (defaultValue) { + setTypes([defaultValue]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const definedTypes = [ + { + value: '', + name: 'All', + icon: , + }, + ...givenTypes, + ]; + const selected = types[0] || ''; + + return ( + + + + + } + IconButtonProps={{ size: 'small' }} + > + {expanded + ? 'Collapse' + : definedTypes.filter(t => t.value === selected)[0]!.name} + + + + {definedTypes.map(type => ( + + + + + {cloneElement(type.icon, { + className: classes.listItemIcon, + })} + + + + + ))} + + + + + + ); +}; diff --git a/plugins/search/src/components/SearchTypeFacet/index.ts b/plugins/search/src/components/SearchTypeFacet/index.ts new file mode 100644 index 0000000000..f98c6f8326 --- /dev/null +++ b/plugins/search/src/components/SearchTypeFacet/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 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. + */ + +export { SearchTypeFacet } from './SearchTypeFacet'; +export type { SearchTypeFacetProps } from './SearchTypeFacet'; diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index e413e9505d..82cc7bf762 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -24,6 +24,7 @@ export * from './SearchPage'; export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchType'; +export * from './SearchTypeFacet'; export * from './SidebarSearch'; export * from './SidebarSearchModal'; export * from './HomePageComponent'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 5bfdcf4318..8f760f42db 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -34,6 +34,7 @@ export { SearchPage as Router, SearchResultPager, SearchType, + SearchTypeFacet, SidebarSearch, useSearch, } from './components'; @@ -45,6 +46,7 @@ export type { FiltersState, SearchBarProps, SearchBarBaseProps, + SearchTypeFacetProps, } from './components'; export { DefaultResultListItem, From 627b98ac0771342e8a4f686b8b5893e76260f7d7 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 28 Dec 2021 11:34:03 +0100 Subject: [PATCH 2/6] -> Signed-off-by: Eric Peterson --- .changeset/search-en-jul-hemma.md | 4 +- .../app/src/components/search/SearchPage.tsx | 4 +- plugins/search/api-report.md | 24 ++++----- .../SearchType.Accordion.tsx} | 30 ++++-------- .../SearchType/SearchType.stories.tsx | 22 +++++++++ .../src/components/SearchType/SearchType.tsx | 25 +++++++--- .../search/src/components/SearchType/index.ts | 1 + .../SearchTypeFacet.stories.tsx | 49 ------------------- .../src/components/SearchTypeFacet/index.ts | 18 ------- plugins/search/src/components/index.tsx | 1 - plugins/search/src/index.ts | 4 +- 11 files changed, 71 insertions(+), 111 deletions(-) rename plugins/search/src/components/{SearchTypeFacet/SearchTypeFacet.tsx => SearchType/SearchType.Accordion.tsx} (87%) delete mode 100644 plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx delete mode 100644 plugins/search/src/components/SearchTypeFacet/index.ts diff --git a/.changeset/search-en-jul-hemma.md b/.changeset/search-en-jul-hemma.md index a2eee15e7c..9fae9fcaa5 100644 --- a/.changeset/search-en-jul-hemma.md +++ b/.changeset/search-en-jul-hemma.md @@ -2,6 +2,6 @@ '@backstage/plugin-search': patch --- -Introduces a `` component, which operates on the same part of a search query as the `` component, but in a more opinionated way (as a single-select control surface suitable for faceted search UIs). +Introduces a `` variant, which operates on the same part of a search query as the existing ``, but in a more opinionated way (as a single-select control surface suitable for faceted search UIs). -Check the [search plugin storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchtypefacet--default) to see how it can be used. +Check the [search plugin storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchtype--accordion) to see how it can be used. diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index e8d590bc92..c99bb78c84 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -29,7 +29,7 @@ import { SearchFilter, SearchResult, SearchResultPager, - SearchTypeFacet, + SearchType, } from '@backstage/plugin-search'; import { DocsResultListItem } from '@backstage/plugin-techdocs'; import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; @@ -63,7 +63,7 @@ const SearchPage = () => { - JSX.Element; -// Warning: (ae-forgotten-export) The symbol "SearchTypeProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "SearchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchType: ({ - values, - className, - name, - defaultValue, -}: SearchTypeProps) => JSX.Element; - -// @public -export const SearchTypeFacet: (props: SearchTypeFacetProps) => JSX.Element; +export const SearchType: { + (props: SearchTypeProps): JSX.Element; + Accordion(props: SearchTypeAccordionProps): JSX.Element; +}; // @public (undocumented) -export type SearchTypeFacetProps = { +export type SearchTypeAccordionProps = { name: string; types: Array<{ value: string; @@ -238,6 +232,14 @@ export type SearchTypeFacetProps = { defaultValue?: string; }; +// @public (undocumented) +export type SearchTypeProps = { + className?: string; + name: string; + values?: string[]; + defaultValue?: string[] | string | null; +}; + // Warning: (ae-missing-release-tag) "SidebarSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx similarity index 87% rename from plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx rename to plugins/search/src/components/SearchType/SearchType.Accordion.tsx index e054e0d028..9ad03db8a1 100644 --- a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx @@ -14,13 +14,7 @@ * limitations under the License. */ -import React, { - ChangeEvent, - cloneElement, - Fragment, - useEffect, - useState, -} from 'react'; +import React, { cloneElement, Fragment, useEffect, useState } from 'react'; import { useSearch } from '../SearchContext'; import { Accordion, @@ -79,8 +73,7 @@ const useStyles = makeStyles(theme => ({ /** * @public */ -export type SearchTypeFacetProps = { - /* what about this? */ +export type SearchTypeAccordionProps = { name: string; types: Array<{ value: string; @@ -90,23 +83,17 @@ export type SearchTypeFacetProps = { defaultValue?: string; }; -/** - * A control surface for the search query's "types" property, displayed as a - * single-select collapsible accordion suitable for use in faceted search UIs. - * @public - */ -export const SearchTypeFacet = (props: SearchTypeFacetProps) => { +export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { const classes = useStyles(); const { setPageCursor, setTypes, types } = useSearch(); const [expanded, setExpanded] = useState(true); const { defaultValue, name, types: givenTypes } = props; - const handleChange = (_event: ChangeEvent<{}>, newExpanded: boolean) => - setExpanded(newExpanded); + const toggleExpanded = () => setExpanded(prevState => !prevState); const handleClick = (type: string) => { return () => { setTypes(type !== '' ? [type] : []); - setPageCursor('0'); + setPageCursor(''); setExpanded(false); }; }; @@ -136,7 +123,7 @@ export const SearchTypeFacet = (props: SearchTypeFacetProps) => { { diff --git a/plugins/search/src/components/SearchType/SearchType.stories.tsx b/plugins/search/src/components/SearchType/SearchType.stories.tsx index da662d8fef..d458c59aad 100644 --- a/plugins/search/src/components/SearchType/SearchType.stories.tsx +++ b/plugins/search/src/components/SearchType/SearchType.stories.tsx @@ -14,6 +14,9 @@ * limitations under the License. */ import React, { useState } from 'react'; +import CatalogIcon from '@material-ui/icons/MenuBook'; +import DocsIcon from '@material-ui/icons/Description'; +import UsersGroupsIcon from '@material-ui/icons/Person'; import { SearchType } from '../index'; import { SearchContext } from '../SearchContext'; @@ -34,3 +37,22 @@ export const Default = () => { ); }; + +export const Accordion = () => { + const [types, setTypes] = useState([]); + const setPageCursor = () => {}; + + return ( + + }, + { value: 'value-2', name: 'Value Two', icon: }, + { value: 'value-3', name: 'Value Three', icon: }, + ]} + /> + + ); +}; diff --git a/plugins/search/src/components/SearchType/SearchType.tsx b/plugins/search/src/components/SearchType/SearchType.tsx index 5290caa0b7..f42802819a 100644 --- a/plugins/search/src/components/SearchType/SearchType.tsx +++ b/plugins/search/src/components/SearchType/SearchType.tsx @@ -25,6 +25,10 @@ import { } from '@material-ui/core'; import React, { ChangeEvent } from 'react'; import { useEffectOnce } from 'react-use'; +import { + SearchTypeAccordion, + SearchTypeAccordionProps, +} from './SearchType.Accordion'; import { useSearch } from '../SearchContext'; const useStyles = makeStyles(theme => ({ @@ -41,6 +45,9 @@ const useStyles = makeStyles(theme => ({ }, })); +/** + * @public + */ export type SearchTypeProps = { className?: string; name: string; @@ -48,12 +55,8 @@ export type SearchTypeProps = { defaultValue?: string[] | string | null; }; -const SearchType = ({ - values = [], - className, - name, - defaultValue, -}: SearchTypeProps) => { +const SearchType = (props: SearchTypeProps) => { + const { className, defaultValue, name, values = [] } = props; const classes = useStyles(); const { types, setTypes } = useSearch(); @@ -112,4 +115,14 @@ const SearchType = ({ ); }; +/** + * A control surface for the search query's "types" property, displayed as a + * single-select collapsible accordion suitable for use in faceted search UIs. + * @public + */ +SearchType.Accordion = (props: SearchTypeAccordionProps) => { + return ; +}; + export { SearchType }; +export type { SearchTypeAccordionProps }; diff --git a/plugins/search/src/components/SearchType/index.ts b/plugins/search/src/components/SearchType/index.ts index 400f3d2a46..e605276455 100644 --- a/plugins/search/src/components/SearchType/index.ts +++ b/plugins/search/src/components/SearchType/index.ts @@ -15,3 +15,4 @@ */ export { SearchType } from './SearchType'; +export type { SearchTypeAccordionProps, SearchTypeProps } from './SearchType'; diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx deleted file mode 100644 index c83f3d88d7..0000000000 --- a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2021 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, { useState } from 'react'; -import CatalogIcon from '@material-ui/icons/MenuBook'; -import DocsIcon from '@material-ui/icons/Description'; - -import { SearchTypeFacet } from '../index'; -import { SearchContext } from '../SearchContext'; - -export default { - title: 'Plugins/Search/SearchTypeFacet', - component: SearchTypeFacet, -}; - -export const Default = () => { - const [types, setTypes] = useState([]); - - return ( - {} } as any} - > - , - }, - { value: 'techdocs', name: 'Documentation', icon: }, - ]} - /> - - ); -}; diff --git a/plugins/search/src/components/SearchTypeFacet/index.ts b/plugins/search/src/components/SearchTypeFacet/index.ts deleted file mode 100644 index f98c6f8326..0000000000 --- a/plugins/search/src/components/SearchTypeFacet/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2021 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. - */ - -export { SearchTypeFacet } from './SearchTypeFacet'; -export type { SearchTypeFacetProps } from './SearchTypeFacet'; diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index 82cc7bf762..e413e9505d 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -24,7 +24,6 @@ export * from './SearchPage'; export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchType'; -export * from './SearchTypeFacet'; export * from './SidebarSearch'; export * from './SidebarSearchModal'; export * from './HomePageComponent'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 8f760f42db..5f0008c425 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -34,7 +34,6 @@ export { SearchPage as Router, SearchResultPager, SearchType, - SearchTypeFacet, SidebarSearch, useSearch, } from './components'; @@ -46,7 +45,8 @@ export type { FiltersState, SearchBarProps, SearchBarBaseProps, - SearchTypeFacetProps, + SearchTypeAccordionProps, + SearchTypeProps, } from './components'; export { DefaultResultListItem, From dd95ac91ccfc4d6437ec2850051f444df20826ed Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 28 Dec 2021 16:11:05 +0100 Subject: [PATCH 3/6] Refactor search plugin stories to use a shared mock search context provider. Signed-off-by: Eric Peterson --- .../DefaultResultListItem.stories.tsx | 71 ++++------ .../SearchBar/SearchBar.stories.tsx | 101 ++++--------- .../SearchContextForStorybook.stories.tsx | 42 ++++++ .../SearchFilter/SearchFilter.stories.tsx | 63 ++++----- .../SearchModal/SearchModal.stories.tsx | 74 +++++----- .../SearchResult/SearchResult.stories.tsx | 133 +++++++++--------- .../SearchType/SearchType.stories.tsx | 45 +++--- 7 files changed, 253 insertions(+), 276 deletions(-) create mode 100644 plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx index 0445a35943..fe2f6c2327 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx @@ -25,6 +25,17 @@ import { MemoryRouter } from 'react-router'; export default { title: 'Plugins/Search/DefaultResultListItem', component: DefaultResultListItem, + decorators: [ + (Story: () => JSX.Element) => ( + + + + + + + + ), + ], }; const mockSearchResult = { @@ -35,54 +46,34 @@ const mockSearchResult = { }; export const Default = () => { - return ( - - - - - - - - ); + return ; }; export const WithIcon = () => { return ( - - - - } - /> - - - + } + /> ); }; export const WithSecondaryAction = () => { return ( - - - - } - style={{ textTransform: 'lowercase' }} - > - {mockSearchResult.owner} - - } - /> - - - + } + style={{ textTransform: 'lowercase' }} + > + {mockSearchResult.owner} + + } + /> ); }; diff --git a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx index d00f391bc7..96c86aa16f 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx @@ -14,88 +14,58 @@ * limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { Paper, Grid, makeStyles } from '@material-ui/core'; -import { SearchBar, SearchContext } from '../index'; -import { MemoryRouter } from 'react-router'; +import { SearchBar } from '../index'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; export default { title: 'Plugins/Search/SearchBar', component: SearchBar, -}; - -const defaultValue = { - term: '', - setTerm: () => {}, + decorators: [ + (Story: ComponentType<{}>) => ( + + + + + + + + ), + ], }; export const Default = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; export const CustomPlaceholder = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; export const Focused = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - {/* decision up to adopter, read https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md#no-autofocus */} - {/* eslint-disable-next-line jsx-a11y/no-autofocus */} - - - - - - + + {/* decision up to adopter, read https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md#no-autofocus */} + {/* eslint-disable-next-line jsx-a11y/no-autofocus */} + + ); }; export const WithoutClearButton = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; @@ -112,17 +82,8 @@ const useStyles = makeStyles({ export const CustomStyles = () => { const classes = useStyles(); return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; diff --git a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx new file mode 100644 index 0000000000..4528a6e46d --- /dev/null +++ b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2021 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 { ApiProvider } from '@backstage/core-app-api'; +import { SearchResultSet } from '@backstage/search-common'; +import { TestApiRegistry } from '@backstage/test-utils'; +import React, { ComponentProps } from 'react'; +import { searchApiRef } from '../../apis'; +import { SearchContextProvider as RealSearchContextProvider } from './SearchContext'; + +type QueryResultProps = { + mockedResults?: SearchResultSet; +}; + +/** + * Utility context provider only for use in Storybook stories. + */ +export const SearchContextProvider = ( + props: ComponentProps & QueryResultProps, +) => { + const { mockedResults, ...contextProps } = props; + const query: any = () => Promise.resolve(mockedResults || {}); + const apiRegistry = TestApiRegistry.from([searchApiRef, { query }]); + + return ( + + + + ); +}; diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx index 190856fca8..806ee46e60 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx @@ -14,56 +14,45 @@ * limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { Grid, Paper } from '@material-ui/core'; -import { SearchFilter, SearchContext } from '../index'; -import { MemoryRouter } from 'react-router'; +import { SearchFilter } from '../index'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; export default { title: 'Plugins/Search/SearchFilter', component: SearchFilter, -}; - -const defaultValue = { - filters: {}, + decorators: [ + (Story: ComponentType<{}>) => ( + + + + + + + + ), + ], }; export const CheckBoxFilter = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; export const SelectFilter = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - - - - - - - - + + + ); }; diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index 6fa93c6588..d46e44c580 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -16,60 +16,50 @@ import React, { ComponentType } from 'react'; import { Button } from '@material-ui/core'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { wrapInTestApp } from '@backstage/test-utils'; import { SearchModal } from '../index'; -import { useSearch, SearchContextProvider } from '../SearchContext'; -import { searchApiRef } from '../../apis'; +import { useSearch } from '../SearchContext'; import { rootRouteRef } from '../../plugin'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; -const mockSearchApi = { - query: () => - Promise.resolve({ - results: [ - { - type: 'custom-result-item', - document: { - location: 'search/search-result-1', - title: 'Search Result 1', - text: 'some text from the search result', - }, - }, - { - type: 'no-custom-result-item', - document: { - location: 'search/search-result-2', - title: 'Search Result 2', - text: 'some text from the search result', - }, - }, - { - type: 'no-custom-result-item', - document: { - location: 'search/search-result-3', - title: 'Search Result 3', - text: 'some text from the search result', - }, - }, - ], - }), +const mockResults = { + results: [ + { + type: 'custom-result-item', + document: { + location: 'search/search-result-1', + title: 'Search Result 1', + text: 'some text from the search result', + }, + }, + { + type: 'no-custom-result-item', + document: { + location: 'search/search-result-2', + title: 'Search Result 2', + text: 'some text from the search result', + }, + }, + { + type: 'no-custom-result-item', + document: { + location: 'search/search-result-3', + title: 'Search Result 3', + text: 'some text from the search result', + }, + }, + ], }; -const apiRegistry = () => ApiRegistry.from([[searchApiRef, mockSearchApi]]); - export default { title: 'Plugins/Search/SearchModal', component: SearchModal, decorators: [ (Story: ComponentType<{}>) => wrapInTestApp( - <> - - - - - - , + + + , { mountedRoutes: { '/search': rootRouteRef } }, ), ], diff --git a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx index aebaecb8b4..bdd04ad90f 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx @@ -14,83 +14,82 @@ * limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { List, ListItem } from '@material-ui/core'; -import { SearchResult, SearchContext, DefaultResultListItem } from '../index'; +import { SearchResult, DefaultResultListItem } from '../index'; import { MemoryRouter } from 'react-router'; import { Link } from '@backstage/core-components'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; + +const mockResults = { + results: [ + { + type: 'custom-result-item', + document: { + location: 'search/search-result-1', + title: 'Search Result 1', + text: 'some text from the search result', + }, + }, + { + type: 'no-custom-result-item', + document: { + location: 'search/search-result-2', + title: 'Search Result 2', + text: 'some text from the search result', + }, + }, + { + type: 'no-custom-result-item', + document: { + location: 'search/search-result-3', + title: 'Search Result 3', + text: 'some text from the search result', + }, + }, + ], +}; export default { title: 'Plugins/Search/SearchResult', component: SearchResult, -}; - -const defaultValue = { - result: { - loading: false, - error: '', - value: { - results: [ - { - type: 'custom-result-item', - document: { - location: 'search/search-result-1', - title: 'Search Result 1', - text: 'some text from the search result', - }, - }, - { - type: 'no-custom-result-item', - document: { - location: 'search/search-result-2', - title: 'Search Result 2', - text: 'some text from the search result', - }, - }, - { - type: 'no-custom-result-item', - document: { - location: 'search/search-result-3', - title: 'Search Result 3', - text: 'some text from the search result', - }, - }, - ], - }, - }, + decorators: [ + (Story: ComponentType<{}>) => ( + + + + + + ), + ], }; export const Default = () => { return ( - - {/* @ts-ignore (defaultValue requires more than what is used here) */} - - - {({ results }) => ( - - {results.map(({ type, document }) => { - switch (type) { - case 'custom-result-item': - return ( - - ); - default: - return ( - - - {document.title} - {document.text} - - - ); - } - })} - - )} - - - + + {({ results }) => ( + + {results.map(({ type, document }) => { + switch (type) { + case 'custom-result-item': + return ( + + ); + default: + return ( + + + {document.title} - {document.text} + + + ); + } + })} + + )} + ); }; diff --git a/plugins/search/src/components/SearchType/SearchType.stories.tsx b/plugins/search/src/components/SearchType/SearchType.stories.tsx index d458c59aad..b8282d6423 100644 --- a/plugins/search/src/components/SearchType/SearchType.stories.tsx +++ b/plugins/search/src/components/SearchType/SearchType.stories.tsx @@ -13,46 +13,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; +import React, { ComponentType } from 'react'; +import { Grid, Paper } from '@material-ui/core'; import CatalogIcon from '@material-ui/icons/MenuBook'; import DocsIcon from '@material-ui/icons/Description'; import UsersGroupsIcon from '@material-ui/icons/Person'; import { SearchType } from '../index'; -import { SearchContext } from '../SearchContext'; +import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; export default { title: 'Plugins/Search/SearchType', component: SearchType, + decorators: [ + (Story: ComponentType<{}>) => ( + + + + + + + + ), + ], }; const values = ['value-1', 'value-2', 'value-3']; export const Default = () => { - const [types, setTypes] = useState([]); - return ( - + - + ); }; export const Accordion = () => { - const [types, setTypes] = useState([]); - const setPageCursor = () => {}; - return ( - - }, - { value: 'value-2', name: 'Value Two', icon: }, - { value: 'value-3', name: 'Value Three', icon: }, - ]} - /> - + }, + { value: 'value-2', name: 'Value Two', icon: }, + { value: 'value-3', name: 'Value Three', icon: }, + ]} + /> ); }; From eeb2269bb92563c122b5c0e95113dd38806aa47d Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 28 Dec 2021 17:08:01 +0100 Subject: [PATCH 4/6] Add basic tests for Signed-off-by: Eric Peterson --- .../SearchType/SearchType.Accordion.test.tsx | 119 ++++++++++++++++++ .../SearchType/SearchType.Accordion.tsx | 2 +- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx new file mode 100644 index 0000000000..d56b40b879 --- /dev/null +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2021 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 { render } from '@testing-library/react'; +import user from '@testing-library/user-event'; + +import { SearchContext } from '../SearchContext'; +import { SearchType } from './SearchType'; + +describe('SearchType.Accordion', () => { + const contextSpy = { + result: { loading: false, value: { results: [] } }, + term: '', + types: [], + filters: {}, + toggleModal: jest.fn(), + setTerm: jest.fn(), + setTypes: jest.fn(), + setFilters: jest.fn(), + setPageCursor: jest.fn(), + }; + + const expectedLabel = 'Expected Label'; + const expectedType = { + value: 'expected-type', + name: 'Expected Type', + icon: <>, + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should render as expected', () => { + const { getByText } = render( + + + , + ); + + // The given label should be rendered. + expect(getByText(expectedLabel)).toBeInTheDocument(); + + // "Collapse" is visible by default (element is not collapsed) + expect(getByText('Collapse')).toBeInTheDocument(); + + // The default "all" type should be rendered. + expect(getByText('All')).toBeInTheDocument(); + + // The given type is also visible + expect(getByText(expectedType.name)).toBeInTheDocument(); + }); + + it('should set entire types array when a type is selected', () => { + const { getByText } = render( + + + , + ); + + user.click(getByText(expectedType.name)); + + expect(contextSpy.setTypes).toHaveBeenCalledWith([expectedType.value]); + }); + + it('should reset types array when all is selected', () => { + const { getByText } = render( + + + , + ); + + user.click(getByText('All')); + + expect(contextSpy.setTypes).toHaveBeenCalledWith([]); + }); + + it('should reset page cursor when a new type is selected', () => { + const { getByText } = render( + + + , + ); + + user.click(getByText(expectedType.name)); + + expect(contextSpy.setPageCursor).toHaveBeenCalledWith(undefined); + }); + + it('should collapse when a new type is selected', () => { + const { getByText, queryByText } = render( + + + , + ); + + user.click(getByText(expectedType.name)); + + expect(queryByText('Collapse')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx index 9ad03db8a1..57e6609bab 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx @@ -93,7 +93,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { const handleClick = (type: string) => { return () => { setTypes(type !== '' ? [type] : []); - setPageCursor(''); + setPageCursor(undefined); setExpanded(false); }; }; From a6bc757a4540b6afc338d557a9f17339eb2d0a19 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 29 Dec 2021 11:54:30 +0100 Subject: [PATCH 5/6] Review notes. Signed-off-by: Eric Peterson --- .../SearchContextForStorybook.stories.tsx | 5 +++- .../SearchType/SearchType.Accordion.test.tsx | 26 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx index 4528a6e46d..e969e0d8cc 100644 --- a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx +++ b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx @@ -25,7 +25,10 @@ type QueryResultProps = { }; /** - * Utility context provider only for use in Storybook stories. + * Utility context provider only for use in Storybook stories. You should use + * the real `` exported by `@backstage/plugin-search` in + * your app instead of this! In some cases (like the search page) it may + * already be provided on your behalf. */ export const SearchContextProvider = ( props: ComponentProps & QueryResultProps, diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index d56b40b879..6b4171a024 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -15,13 +15,19 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { ApiProvider } from '@backstage/core-app-api'; +import { TestApiRegistry } from '@backstage/test-utils'; +import { act, render } from '@testing-library/react'; import user from '@testing-library/user-event'; -import { SearchContext } from '../SearchContext'; +import { searchApiRef } from '../../apis'; +import { SearchContext, SearchContextProvider } from '../SearchContext'; import { SearchType } from './SearchType'; describe('SearchType.Accordion', () => { + const query = jest.fn(); + const mockApis = TestApiRegistry.from([searchApiRef, { query }]); + const contextSpy = { result: { loading: false, value: { results: [] } }, term: '', @@ -42,14 +48,20 @@ describe('SearchType.Accordion', () => { }; beforeEach(() => { + query.mockResolvedValue({ results: [] }); + }); + + afterEach(() => { jest.resetAllMocks(); }); - it('should render as expected', () => { + it('should render as expected', async () => { const { getByText } = render( - - - , + + + + + , ); // The given label should be rendered. @@ -63,6 +75,8 @@ describe('SearchType.Accordion', () => { // The given type is also visible expect(getByText(expectedType.name)).toBeInTheDocument(); + + await act(() => Promise.resolve()); }); it('should set entire types array when a type is selected', () => { From 20af5a701f6954314ec468c8d1ff06022f9e2f39 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 29 Dec 2021 12:10:23 +0100 Subject: [PATCH 6/6] Update create-app with search accordion component. Signed-off-by: Eric Peterson --- .changeset/search-over-tysta-skogor.md | 58 +++++++++++++++++++ .../app/src/components/search/SearchPage.tsx | 30 ++++++++-- 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 .changeset/search-over-tysta-skogor.md diff --git a/.changeset/search-over-tysta-skogor.md b/.changeset/search-over-tysta-skogor.md new file mode 100644 index 0000000000..5398ae7f5f --- /dev/null +++ b/.changeset/search-over-tysta-skogor.md @@ -0,0 +1,58 @@ +--- +'@backstage/create-app': patch +--- + +The `` filter in the composed `SearchPage.tsx` was replaced with the `` variant. + +This is an entirely optional change; if you wish to display a control surface for search `types` as a single-select accordion (as opposed to the current multi-select of checkboxes), you can make the following (or similar) changes to your search page layout: + +```diff +--- a/packages/app/src/components/search/SearchPage.tsx ++++ b/packages/app/src/components/search/SearchPage.tsx +@@ -11,7 +11,7 @@ import { + SearchType, + DefaultResultListItem, + } from '@backstage/plugin-search'; +-import { Content, Header, Page } from '@backstage/core-components'; ++import { CatalogIcon, Content, DocsIcon, Header, Page } from '@backstage/core-components'; + + const useStyles = makeStyles((theme: Theme) => ({ + bar: { +@@ -19,6 +19,7 @@ const useStyles = makeStyles((theme: Theme) => ({ + }, + filters: { + padding: theme.spacing(2), ++ marginTop: theme.spacing(2), + }, + filter: { + '& + &': { +@@ -41,12 +42,23 @@ const SearchPage = () => { + + + ++ , ++ }, ++ { ++ value: 'techdocs', ++ name: 'Documentation', ++ icon: , ++ }, ++ ]} ++ /> + +- + ({ bar: { @@ -19,6 +25,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, filters: { padding: theme.spacing(2), + marginTop: theme.spacing(2), }, filter: { '& + &': { @@ -41,12 +48,23 @@ const SearchPage = () => { + , + }, + { + value: 'techdocs', + name: 'Documentation', + icon: , + }, + ]} + /> -