From d7044784f0398298ceeb98fdddf44a0a8e062308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Wed, 1 Jun 2022 14:55:10 +0200 Subject: [PATCH 01/18] Move SearchFilter from @backstage/plugin-search to @backstage/plugin-search-react and deprecate it in @backstage/plugin-search, and also remove SearchFilterNext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 82 ++++ plugins/search-react/package.json | 6 +- .../SearchFilter.Autocomplete.test.tsx | 361 +++++++++++++++ .../SearchFilter.Autocomplete.tsx | 120 +++++ .../SearchFilter/SearchFilter.stories.tsx | 13 +- .../SearchFilter/SearchFilter.test.tsx | 410 ++++++++++++++++++ .../components/SearchFilter/SearchFilter.tsx | 237 ++++++++++ .../components/SearchFilter/hooks.test.tsx | 8 +- .../src/components/SearchFilter/hooks.ts | 9 +- .../src/components/SearchFilter/index.ts | 24 + plugins/search-react/src/components/index.ts | 1 + plugins/search/api-report.md | 28 +- .../SearchFilter.Autocomplete.tsx | 15 +- .../components/SearchFilter/SearchFilter.tsx | 179 +------- .../src/components/SearchFilter/index.ts | 2 +- plugins/search/src/index.ts | 2 +- 16 files changed, 1296 insertions(+), 201 deletions(-) create mode 100644 plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx create mode 100644 plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.tsx rename plugins/{search => search-react}/src/components/SearchFilter/SearchFilter.stories.tsx (95%) create mode 100644 plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx create mode 100644 plugins/search-react/src/components/SearchFilter/SearchFilter.tsx rename plugins/{search => search-react}/src/components/SearchFilter/hooks.test.tsx (98%) rename plugins/{search => search-react}/src/components/SearchFilter/hooks.ts (96%) create mode 100644 plugins/search-react/src/components/SearchFilter/index.ts diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 9f61070854..00325d6f64 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -10,9 +10,18 @@ import { AsyncState } from 'react-use/lib/useAsync'; import { JsonObject } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; +import { ReactElement } from 'react'; import { SearchQuery } from '@backstage/plugin-search-common'; import { SearchResultSet } from '@backstage/plugin-search-common'; +// @public (undocumented) +export const AutocompleteFilter: ( + props: SearchAutocompleteFilterProps, +) => JSX.Element; + +// @public (undocumented) +export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; + // @public (undocumented) export const HighlightedSearchResultText: ({ text, @@ -45,6 +54,13 @@ export interface SearchApi { // @public (undocumented) export const searchApiRef: ApiRef; +// @public (undocumented) +export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { + filterSelectedOptions?: boolean; + limitTags?: number; + multiple?: boolean; +}; + // @public export const SearchContextProvider: ( props: SearchContextProviderProps, @@ -74,6 +90,72 @@ export type SearchContextValue = { fetchPreviousPage?: React_2.DispatchWithoutAction; } & SearchContextState; +// @public (undocumented) +export const SearchFilter: { + ({ component: Element, ...props }: SearchFilterWrapperProps): JSX.Element; + Checkbox( + props: Omit & + SearchFilterComponentProps, + ): JSX.Element; + Select( + props: Omit & + SearchFilterComponentProps, + ): JSX.Element; + Autocomplete(props: SearchAutocompleteFilterProps): JSX.Element; +}; + +// @public (undocumented) +export type SearchFilterComponentProps = { + className?: string; + name: string; + label?: string; + values?: string[] | ((partial: string) => Promise); + defaultValue?: string[] | string | null; + valuesDebounceMs?: number; +}; + +// @public (undocumented) +export type SearchFilterWrapperProps = SearchFilterComponentProps & { + component: (props: SearchFilterComponentProps) => ReactElement; + debug?: boolean; +}; + +// @public (undocumented) +export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; + +// @public +export const useAsyncFilterValues: ( + fn: ((partial: string) => Promise) | undefined, + inputValue: string, + defaultValues?: string[], + debounce?: number, +) => + | { + loading: boolean; + error?: undefined; + value?: undefined; + } + | { + loading: false; + error: Error; + value?: undefined; + } + | { + loading: true; + error?: Error | undefined; + value?: string[] | undefined; + } + | { + loading: boolean; + value: string[]; + }; + +// @public +export const useDefaultFilterValue: ( + name: string, + defaultValue?: string | string[] | null | undefined, +) => void; + // @public export const useSearch: () => SearchContextValue; diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 5646ce9ee1..ac8a43dee4 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -36,7 +36,8 @@ "@backstage/version-bridge": "^1.0.1", "react-use": "^17.3.2", "@backstage/types": "^1.0.0", - "@material-ui/core": "^4.12.2" + "@material-ui/core": "^4.12.2", + "@material-ui/lab": "4.0.0-alpha.57" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", @@ -47,7 +48,8 @@ "@backstage/test-utils": "^1.1.1-next.0", "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.0", - "@testing-library/jest-dom": "^5.10.1" + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/user-event": "^14.0.0" }, "files": [ "dist" diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx new file mode 100644 index 0000000000..14c8967b57 --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx @@ -0,0 +1,361 @@ +/* + * 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 { TestApiProvider } from '@backstage/test-utils'; +import { screen, render, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { searchApiRef } from '../../api'; +import { SearchContextProvider, useSearch } from '../../context'; +import { SearchFilter } from './SearchFilter'; + +const SearchContextFilterSpy = ({ name }: { name: string }) => { + const { filters } = useSearch(); + const value = filters[name]; + return ( + + {Array.isArray(value) ? value.join(',') : value} + + ); +}; + +describe('SearchFilter.Autocomplete', () => { + const query = jest.fn().mockResolvedValue({}); + const emptySearchContext = { + term: '', + types: [], + filters: {}, + }; + + const name = 'field'; + const values = ['value1', 'value2']; + + it('renders as expected', async () => { + render( + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + + await waitFor(() => { + screen.getByRole('listbox'); + }); + + expect(screen.getByRole('option', { name: values[0] })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: values[1] })).toBeInTheDocument(); + }); + + it('renders as expected with async values', async () => { + render( + + + values} /> + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + + await waitFor(() => { + screen.getByRole('listbox'); + }); + + expect(screen.getByRole('option', { name: values[0] })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: values[1] })).toBeInTheDocument(); + }); + + it('does not affect unrelated filter state', async () => { + render( + + + + + + + , + ); + + // The spy should show the initial value. + expect(screen.getByTestId('unrelated-filter-spy')).toHaveTextContent( + 'value', + ); + + // Select a value from the autocomplete filter. + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[1] })); + + // Wait for the autocomplete filter's value to change. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values[1], + ); + }); + + // Unrelated filter spy should maintain the same value. + expect(screen.getByTestId('unrelated-filter-spy')).toHaveTextContent( + 'value', + ); + }); + + describe('single', () => { + it('renders as expected with defaultValue', async () => { + render( + + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + + await waitFor(() => { + expect(input).toHaveValue(values[1]); + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values[1], + ); + }); + }); + + it('renders as expected with initial context', async () => { + render( + + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + + await waitFor(() => { + expect(input).toHaveValue(values[0]); + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values[0], + ); + }); + }); + + it('sets filter state when selecting a value', async () => { + render( + + + + + + , + ); + + // Select the first option in the autocomplete. + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[0] })); + + // The value should be present in the context. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values[0], + ); + }); + + // Click the "Clear" button to remove the value. + const clearButton = within(autocomplete).getByLabelText('Clear'); + await userEvent.click(clearButton); + + // That value should have been unset from the context. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(''); + }); + }); + }); + + describe('multiple', () => { + it('renders as expected with defaultValue', async () => { + render( + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText(values[0])).toBeInTheDocument(); + expect(screen.getByText(values[1])).toBeInTheDocument(); + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values.join(','), + ); + }); + }); + + it('renders as expected with initial context', async () => { + render( + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText(values[0])).toBeInTheDocument(); + expect(screen.getByText(values[1])).toBeInTheDocument(); + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values.join(','), + ); + }); + }); + + it('respects tag limit configuration', async () => { + render( + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + const input = within(autocomplete).getByRole('textbox'); + + // Select the second value. + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[1] })); + await waitFor(() => { + expect( + screen.getByRole('button', { name: values[1] }), + ).toBeInTheDocument(); + }); + + // Select the first value. + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[0] })); + await waitFor(() => { + expect( + screen.getByRole('button', { name: values[0] }), + ).toBeInTheDocument(); + }); + + // Blur the field and only one tag should be shown with a +1. + input.blur(); + expect( + screen.queryByRole('button', { name: values[0] }), + ).not.toBeInTheDocument(); + expect(screen.getByText('+1')).toBeInTheDocument(); + }); + + it('sets filter state when selecting a value', async () => { + render( + + + + + + , + ); + + const autocomplete = screen.getByRole('combobox'); + + // Select both values in the autocomplete. + const input = within(autocomplete).getByRole('textbox'); + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[0] })); + await userEvent.click(input); + await waitFor(() => { + screen.getByRole('listbox'); + }); + await userEvent.click(screen.getByRole('option', { name: values[1] })); + + // Both options should be present in the context. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent( + values.join(','), + ); + }); + + // Click the "Clear" button to remove the value. + const clearButton = within(autocomplete).getByLabelText('Clear'); + await userEvent.click(clearButton); + + // There should be no content in the filter context. + await waitFor(() => { + expect(screen.getByTestId(`${name}-filter-spy`)).toHaveTextContent(''); + }); + }); + }); +}); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.tsx new file mode 100644 index 0000000000..35d1f376e5 --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -0,0 +1,120 @@ +/* + * 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, { ChangeEvent, useState } from 'react'; +import { Chip, TextField } from '@material-ui/core'; +import { + Autocomplete, + AutocompleteGetTagProps, + AutocompleteRenderInputParams, +} from '@material-ui/lab'; + +import { useSearch } from '../../context'; +import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; +import { SearchFilterComponentProps } from './SearchFilter'; + +/** + * @public + */ +export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { + filterSelectedOptions?: boolean; + limitTags?: number; + multiple?: boolean; +}; + +/** + * @public + */ +export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => { + const { + className, + defaultValue, + name, + values: givenValues, + valuesDebounceMs, + label, + filterSelectedOptions, + limitTags, + multiple, + } = props; + const [inputValue, setInputValue] = useState(''); + useDefaultFilterValue(name, defaultValue); + const asyncValues = + typeof givenValues === 'function' ? givenValues : undefined; + const defaultValues = + typeof givenValues === 'function' ? undefined : givenValues; + const { value: values, loading } = useAsyncFilterValues( + asyncValues, + inputValue, + defaultValues, + valuesDebounceMs, + ); + const { filters, setFilters } = useSearch(); + const filterValue = + (filters[name] as string | string[] | undefined) || (multiple ? [] : null); + + // Set new filter values on input change. + const handleChange = ( + _: ChangeEvent<{}>, + newValue: string | string[] | null, + ) => { + setFilters(prevState => { + const { [name]: filter, ...others } = prevState; + + if (newValue) { + return { ...others, [name]: newValue }; + } + return { ...others }; + }); + }; + + // Provide the input field. + const renderInput = (params: AutocompleteRenderInputParams) => ( + + ); + + // Render tags as primary-colored chips. + const renderTags = ( + tagValue: string[], + getTagProps: AutocompleteGetTagProps, + ) => + tagValue.map((option: string, index: number) => ( + + )); + + return ( + setInputValue(newValue)} + renderInput={renderInput} + renderTags={renderTags} + /> + ); +}; diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx similarity index 95% rename from plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx rename to plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx index 7e8a7d585a..2c8a629f01 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -14,14 +14,13 @@ * limitations under the License. */ -import { Grid, Paper } from '@material-ui/core'; import React, { ComponentType } from 'react'; -import { - searchApiRef, - MockSearchApi, - SearchContextProvider, -} from '@backstage/plugin-search-react'; +import { Grid, Paper } from '@material-ui/core'; + import { TestApiProvider } from '@backstage/test-utils'; + +import { searchApiRef, MockSearchApi } from '../../api'; +import { SearchContextProvider } from '../../context'; import { SearchFilter } from './SearchFilter'; export default { diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx new file mode 100644 index 0000000000..89a1818e8d --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx @@ -0,0 +1,410 @@ +/* + * 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 { screen, render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { SearchContextProvider } from '../../context'; +import { SearchFilter } from './SearchFilter'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn().mockReturnValue({}), +})); + +describe('SearchFilter', () => { + const initialState = { + term: '', + filters: {}, + types: [], + }; + + const label = 'Field'; + const name = 'field'; + const values = ['value1', 'value2']; + const filters = { unrelated: 'unrelated' }; + + const query = jest.fn().mockResolvedValue({}); + (useApi as jest.Mock).mockReturnValue({ query: query }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Check that element was rendered and received props', async () => { + const CustomFilter = (props: { name: string }) =>
{props.name}
; + + render(); + + expect(screen.getByRole('heading', { name })).toBeInTheDocument(); + }); + + describe('Checkbox', () => { + it('Renders field name and values when provided as props', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + expect( + screen.getByRole('checkbox', { name: values[0] }), + ).toBeInTheDocument(); + expect( + screen.getByRole('checkbox', { name: values[1] }), + ).toBeInTheDocument(); + }); + + it('Renders correctly based on filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + expect( + screen.getByRole('checkbox', { name: values[0] }), + ).not.toBeChecked(); + expect(screen.getByRole('checkbox', { name: values[1] })).toBeChecked(); + }); + + it('Renders correctly based on defaultValue', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + expect(screen.getByRole('checkbox', { name: values[0] })).toBeChecked(); + expect( + screen.getByRole('checkbox', { name: values[1] }), + ).not.toBeChecked(); + }); + + it('Checking / unchecking a value sets filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + const checkBox = screen.getByRole('checkbox', { name: values[0] }); + + // Check the box. + await userEvent.click(checkBox); + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ filters: { field: [values[0]] } }), + ); + }); + + // Uncheck the box. + await userEvent.click(checkBox); + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ filters: {} }), + ); + }); + }); + + it('Checking / unchecking a value maintains unrelated filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + const checkBox = screen.getByRole('checkbox', { name: values[0] }); + + // Check the box. + await userEvent.click(checkBox); + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: { ...filters, field: [values[0]] }, + }), + ); + }); + + // Uncheck the box. + await userEvent.click(checkBox); + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ filters }), + ); + }); + }); + }); + + describe('Select', () => { + it('Renders field name and values when provided as props', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + expect( + screen.getByRole('option', { name: values[0] }), + ).toBeInTheDocument(); + expect( + screen.getByRole('option', { name: values[1] }), + ).toBeInTheDocument(); + }); + + it('Renders values when provided asynchronously', async () => { + render( + + values} + /> + , + ); + + await waitFor(() => { + expect(screen.getByRole('button')).toBeInTheDocument(); + expect( + screen.getByRole('button').getAttribute('aria-disabled'), + ).not.toBe('true'); + }); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + expect( + screen.getByRole('option', { name: values[0] }), + ).toBeInTheDocument(); + expect( + screen.getByRole('option', { name: values[1] }), + ).toBeInTheDocument(); + }); + + it('Renders correctly based on filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + expect(screen.getByRole('option', { name: values[0] })).toHaveAttribute( + 'aria-selected', + 'true', + ); + expect( + screen.getByRole('option', { name: values[1] }), + ).not.toHaveAttribute('aria-selected'); + expect(screen.getByRole('option', { name: 'All' })).not.toHaveAttribute( + 'aria-selected', + ); + }); + + it('Renders correctly based on defaultValue', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('button')); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + expect(screen.getByRole('option', { name: values[0] })).toHaveAttribute( + 'aria-selected', + 'true', + ); + expect( + screen.getByRole('option', { name: values[1] }), + ).not.toHaveAttribute('aria-selected'); + expect(screen.getByRole('option', { name: 'All' })).not.toHaveAttribute( + 'aria-selected', + ); + }); + + it('Selecting a value sets filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + const button = screen.getByRole('button'); + + await userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('option', { name: values[0] })); + + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: { [name]: values[0] }, + }), + ); + }); + + await userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('option', { name: 'All' })); + + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: {}, + }), + ); + }); + }); + + it('Selecting a value maintains unrelated filter state', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + const button = screen.getByRole('button'); + + await userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('option', { name: values[0] })); + + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + filters: { ...filters, [name]: values[0] }, + }), + ); + }); + + await userEvent.click(button); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('option', { name: 'All' })); + + await waitFor(() => { + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ filters }), + ); + }); + }); + }); +}); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx new file mode 100644 index 0000000000..2a6d7baafa --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.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, { ReactElement, ChangeEvent } from 'react'; +import { + makeStyles, + FormControl, + FormControlLabel, + InputLabel, + Checkbox, + Select, + MenuItem, + FormLabel, +} from '@material-ui/core'; + +import { useSearch } from '../../context'; +import { + AutocompleteFilter, + SearchAutocompleteFilterProps, +} from './SearchFilter.Autocomplete'; +import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; + +const useStyles = makeStyles({ + label: { + textTransform: 'capitalize', + }, +}); + +/** + * @public + */ +export type SearchFilterComponentProps = { + className?: string; + name: string; + label?: string; + /** + * Either an array of values directly, or an async function to return a list + * of values to be used in the filter. In the autocomplete filter, the last + * input value is provided as an input to allow values to be filtered. This + * function is debounced and values cached. + */ + values?: string[] | ((partial: string) => Promise); + defaultValue?: string[] | string | null; + /** + * Debounce time in milliseconds, used when values is an async callback. + * Defaults to 250ms. + */ + valuesDebounceMs?: number; +}; + +/** + * @public + */ +export type SearchFilterWrapperProps = SearchFilterComponentProps & { + component: (props: SearchFilterComponentProps) => ReactElement; + debug?: boolean; +}; + +/** + * @public + */ +export const CheckboxFilter = (props: SearchFilterComponentProps) => { + const { + className, + defaultValue, + label, + name, + values: givenValues = [], + valuesDebounceMs, + } = props; + const classes = useStyles(); + const { filters, setFilters } = useSearch(); + useDefaultFilterValue(name, defaultValue); + const asyncValues = + typeof givenValues === 'function' ? givenValues : undefined; + const defaultValues = + typeof givenValues === 'function' ? undefined : givenValues; + const { value: values = [], loading } = useAsyncFilterValues( + asyncValues, + '', + defaultValues, + valuesDebounceMs, + ); + + const handleChange = (e: ChangeEvent) => { + const { + target: { value, checked }, + } = e; + + setFilters(prevFilters => { + const { [name]: filter, ...others } = prevFilters; + const rest = ((filter as string[]) || []).filter(i => i !== value); + const items = checked ? [...rest, value] : rest; + return items.length ? { ...others, [name]: items } : others; + }); + }; + + return ( + + {label ? {label} : null} + {values.map((value: string) => ( + + } + label={value} + /> + ))} + + ); +}; + +/** + * @public + */ +export const SelectFilter = (props: SearchFilterComponentProps) => { + const { + className, + defaultValue, + label, + name, + values: givenValues, + valuesDebounceMs, + } = props; + const classes = useStyles(); + useDefaultFilterValue(name, defaultValue); + const asyncValues = + typeof givenValues === 'function' ? givenValues : undefined; + const defaultValues = + typeof givenValues === 'function' ? undefined : givenValues; + const { value: values = [], loading } = useAsyncFilterValues( + asyncValues, + '', + defaultValues, + valuesDebounceMs, + ); + const { filters, setFilters } = useSearch(); + + const handleChange = (e: ChangeEvent<{ value: unknown }>) => { + const { + target: { value }, + } = e; + + setFilters(prevFilters => { + const { [name]: filter, ...others } = prevFilters; + return value ? { ...others, [name]: value as string } : others; + }); + }; + + return ( + + {label ? ( + + {label} + + ) : null} + + + ); +}; + +/** + * @public + */ +const SearchFilter = ({ + component: Element, + ...props +}: SearchFilterWrapperProps) => ; + +SearchFilter.Checkbox = ( + props: Omit & + SearchFilterComponentProps, +) => ; + +SearchFilter.Select = ( + props: Omit & + SearchFilterComponentProps, +) => ; + +/** + * A control surface for a given filter field name, rendered as an autocomplete + * textfield. A hard-coded list of values may be provided, or an async function + * which returns values may be provided instead. + * + * @public + */ +SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => ( + +); + +export { SearchFilter }; diff --git a/plugins/search/src/components/SearchFilter/hooks.test.tsx b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx similarity index 98% rename from plugins/search/src/components/SearchFilter/hooks.test.tsx rename to plugins/search-react/src/components/SearchFilter/hooks.test.tsx index 14346a29eb..80c4a3a3a2 100644 --- a/plugins/search/src/components/SearchFilter/hooks.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx @@ -17,11 +17,9 @@ import React from 'react'; import { ApiProvider } from '@backstage/core-app-api'; import { TestApiRegistry } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; -import { - SearchContextProvider, - useSearch, - searchApiRef, -} from '@backstage/plugin-search-react'; + +import { searchApiRef } from '../../api'; +import { SearchContextProvider, useSearch } from '../../context'; import { useDefaultFilterValue, useAsyncFilterValues } from './hooks'; jest.useFakeTimers(); diff --git a/plugins/search/src/components/SearchFilter/hooks.ts b/plugins/search-react/src/components/SearchFilter/hooks.ts similarity index 96% rename from plugins/search/src/components/SearchFilter/hooks.ts rename to plugins/search-react/src/components/SearchFilter/hooks.ts index da30466522..5771cc5288 100644 --- a/plugins/search/src/components/SearchFilter/hooks.ts +++ b/plugins/search-react/src/components/SearchFilter/hooks.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -17,11 +17,14 @@ import { useEffect, useRef } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import useDebounce from 'react-use/lib/useDebounce'; -import { useSearch } from '@backstage/plugin-search-react'; + +import { useSearch } from '../../context'; /** * Utility hook for either asynchronously loading filter values from a given * function or synchronously providing a given list of default values. + * + * @public */ export const useAsyncFilterValues = ( fn: ((partial: string) => Promise) | undefined, @@ -75,6 +78,8 @@ export const useAsyncFilterValues = ( /** * Utility hook for applying a given default value to the search context. + * + * @public */ export const useDefaultFilterValue = ( name: string, diff --git a/plugins/search-react/src/components/SearchFilter/index.ts b/plugins/search-react/src/components/SearchFilter/index.ts new file mode 100644 index 0000000000..e159cff93d --- /dev/null +++ b/plugins/search-react/src/components/SearchFilter/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +export { CheckboxFilter, SearchFilter, SelectFilter } from './SearchFilter'; +export { AutocompleteFilter } from './SearchFilter.Autocomplete'; +export { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; +export type { + SearchFilterComponentProps, + SearchFilterWrapperProps, +} from './SearchFilter'; +export type { SearchAutocompleteFilterProps } from './SearchFilter.Autocomplete'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 679a254827..8e45c66fce 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -15,3 +15,4 @@ */ export * from './HighlightedSearchResultText'; +export * from './SearchFilter'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 21b9df5dda..ec60569af4 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -12,7 +12,9 @@ import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; +import { SearchAutocompleteFilterProps as SearchAutocompleteFilterProps_2 } from '@backstage/plugin-search-react'; import { SearchDocument } from '@backstage/plugin-search-common'; +import { SearchFilterComponentProps as SearchFilterComponentProps_2 } from '@backstage/plugin-search-react'; import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; // Warning: (ae-missing-release-tag) "DefaultResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -78,8 +80,8 @@ export type HomePageSearchBarProps = Partial< // @public (undocumented) export const Router: () => JSX.Element; -// @public (undocumented) -export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { +// @public @deprecated (undocumented) +export type SearchAutocompleteFilterProps = SearchFilterComponentProps_2 & { filterSelectedOptions?: boolean; limitTags?: number; multiple?: boolean; @@ -124,7 +126,7 @@ export type SearchBarProps = Partial; // Warning: (ae-missing-release-tag) "SearchFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export const SearchFilter: { ({ component: Element, ...props }: SearchFilterWrapperProps): JSX.Element; Checkbox( @@ -135,10 +137,10 @@ export const SearchFilter: { props: Omit & SearchFilterComponentProps, ): JSX.Element; - Autocomplete(props: SearchAutocompleteFilterProps): JSX.Element; + Autocomplete(props: SearchAutocompleteFilterProps_2): JSX.Element; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type SearchFilterComponentProps = { className?: string; name: string; @@ -148,23 +150,7 @@ export type SearchFilterComponentProps = { valuesDebounceMs?: number; }; -// Warning: (ae-missing-release-tag) "SearchFilterNext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) -export const SearchFilterNext: { - ({ component: Element, ...props }: SearchFilterWrapperProps): JSX.Element; - Checkbox( - props: Omit & - SearchFilterComponentProps, - ): JSX.Element; - Select( - props: Omit & - SearchFilterComponentProps, - ): JSX.Element; - Autocomplete(props: SearchAutocompleteFilterProps): JSX.Element; -}; - -// @public (undocumented) export type SearchFilterWrapperProps = SearchFilterComponentProps & { component: (props: SearchFilterComponentProps) => ReactElement; debug?: boolean; diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx index 03f4f28943..1a74decd31 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -21,11 +21,16 @@ import { AutocompleteGetTagProps, AutocompleteRenderInputParams, } from '@material-ui/lab'; -import { useSearch } from '@backstage/plugin-search-react'; -import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; -import { SearchFilterComponentProps } from './SearchFilter'; +import { + SearchFilterComponentProps, + useSearch, + useAsyncFilterValues, + useDefaultFilterValue, +} from '@backstage/plugin-search-react'; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * @public */ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { @@ -34,6 +39,10 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { multiple?: boolean; }; +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + * + */ export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => { const { className, diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.tsx index bfbab8531c..b88160f363 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.tsx @@ -14,32 +14,18 @@ * limitations under the License. */ -import React, { ReactElement, ChangeEvent } from 'react'; -import { - makeStyles, - FormControl, - FormControlLabel, - InputLabel, - Checkbox, - Select, - MenuItem, - FormLabel, -} from '@material-ui/core'; +import React, { ReactElement } from 'react'; import { AutocompleteFilter, + CheckboxFilter, SearchAutocompleteFilterProps, -} from './SearchFilter.Autocomplete'; -import { useSearch } from '@backstage/plugin-search-react'; -import { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; - -const useStyles = makeStyles({ - label: { - textTransform: 'capitalize', - }, -}); + SelectFilter, +} from '@backstage/plugin-search-react'; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * @public */ export type SearchFilterComponentProps = { @@ -62,6 +48,8 @@ export type SearchFilterComponentProps = { }; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * @public */ export type SearchFilterWrapperProps = SearchFilterComponentProps & { @@ -69,152 +57,33 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; -const CheckboxFilter = (props: SearchFilterComponentProps) => { - const { - className, - defaultValue, - label, - name, - values: givenValues = [], - valuesDebounceMs, - } = props; - const classes = useStyles(); - const { filters, setFilters } = useSearch(); - useDefaultFilterValue(name, defaultValue); - const asyncValues = - typeof givenValues === 'function' ? givenValues : undefined; - const defaultValues = - typeof givenValues === 'function' ? undefined : givenValues; - const { value: values = [], loading } = useAsyncFilterValues( - asyncValues, - '', - defaultValues, - valuesDebounceMs, - ); - - const handleChange = (e: ChangeEvent) => { - const { - target: { value, checked }, - } = e; - - setFilters(prevFilters => { - const { [name]: filter, ...others } = prevFilters; - const rest = ((filter as string[]) || []).filter(i => i !== value); - const items = checked ? [...rest, value] : rest; - return items.length ? { ...others, [name]: items } : others; - }); - }; - - return ( - - {label ? {label} : null} - {values.map((value: string) => ( - - } - label={value} - /> - ))} - - ); -}; - -const SelectFilter = (props: SearchFilterComponentProps) => { - const { - className, - defaultValue, - label, - name, - values: givenValues, - valuesDebounceMs, - } = props; - const classes = useStyles(); - useDefaultFilterValue(name, defaultValue); - const asyncValues = - typeof givenValues === 'function' ? givenValues : undefined; - const defaultValues = - typeof givenValues === 'function' ? undefined : givenValues; - const { value: values = [], loading } = useAsyncFilterValues( - asyncValues, - '', - defaultValues, - valuesDebounceMs, - ); - const { filters, setFilters } = useSearch(); - - const handleChange = (e: ChangeEvent<{ value: unknown }>) => { - const { - target: { value }, - } = e; - - setFilters(prevFilters => { - const { [name]: filter, ...others } = prevFilters; - return value ? { ...others, [name]: value as string } : others; - }); - }; - - return ( - - {label ? ( - - {label} - - ) : null} - - - ); -}; - +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ const SearchFilter = ({ component: Element, ...props }: SearchFilterWrapperProps) => ; +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ SearchFilter.Checkbox = ( props: Omit & SearchFilterComponentProps, ) => ; +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ SearchFilter.Select = ( props: Omit & SearchFilterComponentProps, ) => ; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * A control surface for a given filter field name, rendered as an autocomplete * textfield. A hard-coded list of values may be provided, or an async function * which returns values may be provided instead. @@ -224,12 +93,4 @@ SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => ( ); -/** - * @deprecated This component was used for rapid prototyping of the Backstage - * Search platform. Now that the API has stabilized, you should use the - * component instead. This component will be removed in an - * upcoming release. - */ -const SearchFilterNext = SearchFilter; - -export { SearchFilter, SearchFilterNext }; +export { SearchFilter }; diff --git a/plugins/search/src/components/SearchFilter/index.ts b/plugins/search/src/components/SearchFilter/index.ts index 86cd66ff6b..3050330038 100644 --- a/plugins/search/src/components/SearchFilter/index.ts +++ b/plugins/search/src/components/SearchFilter/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { SearchFilter, SearchFilterNext } from './SearchFilter'; +export { SearchFilter } from './SearchFilter'; export type { SearchFilterComponentProps, SearchFilterWrapperProps, diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 1457ac781d..2eb2ed277b 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -28,7 +28,7 @@ export type { SearchBarBaseProps, SearchBarProps, } from './components/SearchBar'; -export { SearchFilter, SearchFilterNext } from './components/SearchFilter'; +export { SearchFilter } from './components/SearchFilter'; export type { SearchAutocompleteFilterProps, SearchFilterComponentProps, From 01abb3bc7e4ee0a7d1b806fc63662a1ad6106688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Wed, 1 Jun 2022 16:37:19 +0200 Subject: [PATCH 02/18] Move SearchResult from @backstage/plugin-search to @backstage/plugin-search-react and deprecate it in @backstage/plugin-search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 9 ++ plugins/search-react/package.json | 7 +- .../SearchResult/SearchResult.stories.tsx | 11 +- .../SearchResult/SearchResult.test.tsx | 125 ++++++++++++++++++ .../components/SearchResult/SearchResult.tsx | 60 +++++++++ .../src/components/SearchResult/index.tsx | 18 +++ plugins/search-react/src/components/index.ts | 1 + .../components/SearchResult/SearchResult.tsx | 3 + 8 files changed, 225 insertions(+), 9 deletions(-) rename plugins/{search => search-react}/src/components/SearchResult/SearchResult.stories.tsx (92%) create mode 100644 plugins/search-react/src/components/SearchResult/SearchResult.test.tsx create mode 100644 plugins/search-react/src/components/SearchResult/SearchResult.tsx create mode 100644 plugins/search-react/src/components/SearchResult/index.tsx diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 00325d6f64..09dd3d77a8 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -12,6 +12,7 @@ import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; import { SearchResultSet } from '@backstage/plugin-search-common'; // @public (undocumented) @@ -120,6 +121,14 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; +// @public (undocumented) +export const SearchResult: ({ children }: SearchResultProps) => JSX.Element; + +// @public (undocumented) +export type SearchResultProps = { + children: (results: { results: SearchResult_2[] }) => JSX.Element; +}; + // @public (undocumented) export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index ac8a43dee4..04932bd5ea 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -31,13 +31,16 @@ "start": "backstage-cli package start" }, "dependencies": { + "@backstage/plugin-search": "0.8.2-next.1", "@backstage/plugin-search-common": "^0.3.5-next.0", + "@backstage/core-components": "^0.9.5-next.1", "@backstage/core-plugin-api": "^1.0.3-next.0", "@backstage/version-bridge": "^1.0.1", - "react-use": "^17.3.2", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", - "@material-ui/lab": "4.0.0-alpha.57" + "@material-ui/lab": "4.0.0-alpha.57", + "react-router": "6.0.0-beta.0", + "react-use": "^17.3.2" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx similarity index 92% rename from plugins/search/src/components/SearchResult/SearchResult.stories.tsx rename to plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx index 207f1ae1d7..38be586f97 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -18,13 +18,10 @@ import { Link } from '@backstage/core-components'; import { List, ListItem } from '@material-ui/core'; import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router'; -import { DefaultResultListItem } from '../DefaultResultListItem'; +import { DefaultResultListItem } from '@backstage/plugin-search'; -import { - searchApiRef, - MockSearchApi, - SearchContextProvider, -} from '@backstage/plugin-search-react'; +import { searchApiRef, MockSearchApi } from '../../api'; +import { SearchContextProvider } from '../../context'; import { SearchResult } from './SearchResult'; import { TestApiProvider } from '@backstage/test-utils'; diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx new file mode 100644 index 0000000000..10437b3af7 --- /dev/null +++ b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx @@ -0,0 +1,125 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { useSearch } from '../../context'; +import { SearchResult } from './SearchResult'; + +jest.mock('../../context', () => ({ + ...jest.requireActual('../../context'), + useSearch: jest.fn().mockReturnValue({ + result: {}, + }), +})); + +describe('SearchResult', () => { + it('Progress rendered on Loading state', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: true }, + }); + + const { getByRole } = await renderInTestApp( + {() => <>}, + ); + + await waitFor(() => { + expect(getByRole('progressbar')).toBeInTheDocument(); + }); + }); + + it('Alert rendered on Error state', async () => { + const error = new Error('some error'); + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, error }, + }); + + const { getByRole } = await renderInTestApp( + {() => <>}, + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + new RegExp(`Error encountered while fetching search results.*${error}`), + ); + }); + }); + + it('On no result value state', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, error: '', value: undefined }, + }); + + const { getByRole } = await renderInTestApp( + {() => <>}, + ); + + await waitFor(() => { + expect( + getByRole('heading', { name: 'Sorry, no results were found' }), + ).toBeInTheDocument(); + }); + }); + + it('On empty result value state', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, error: '', value: { results: [] } }, + }); + + const { getByRole } = await renderInTestApp( + {() => <>}, + ); + + await waitFor(() => { + expect( + getByRole('heading', { name: 'Sorry, no results were found' }), + ).toBeInTheDocument(); + }); + }); + + it('Calls children with results set to result.value', async () => { + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { + loading: false, + error: '', + value: { + totalCount: 1, + results: [ + { + type: 'some-type', + document: { + title: 'some-title', + text: 'some-text', + location: 'some-location', + }, + }, + ], + }, + }, + }); + + const { getByText } = await renderInTestApp( + + {({ results }) => { + return <>Results {results.length}; + }} + , + ); + + expect(getByText('Results 1')).toBeInTheDocument(); + }); +}); diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx new file mode 100644 index 0000000000..a1ce3cea25 --- /dev/null +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -0,0 +1,60 @@ +/* + * 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 { + EmptyState, + Progress, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { SearchResult } from '@backstage/plugin-search-common'; +import React from 'react'; +import { useSearch } from '../../context'; + +/** + * @public + */ +export type SearchResultProps = { + children: (results: { results: SearchResult[] }) => JSX.Element; +}; + +/** + * @public + */ +export const SearchResultComponent = ({ children }: SearchResultProps) => { + const { + result: { loading, error, value }, + } = useSearch(); + + if (loading) { + return ; + } + if (error) { + return ( + + ); + } + + if (!value?.results.length) { + return ; + } + + return <>{children({ results: value.results })}; +}; + +export { SearchResultComponent as SearchResult }; diff --git a/plugins/search-react/src/components/SearchResult/index.tsx b/plugins/search-react/src/components/SearchResult/index.tsx new file mode 100644 index 0000000000..2032fbbed9 --- /dev/null +++ b/plugins/search-react/src/components/SearchResult/index.tsx @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { SearchResult } from './SearchResult'; +export type { SearchResultProps } from './SearchResult'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 8e45c66fce..db9214d299 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -16,3 +16,4 @@ export * from './HighlightedSearchResultText'; export * from './SearchFilter'; +export * from './SearchResult'; diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index a62dddb924..77ee883164 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -27,6 +27,9 @@ type Props = { children: (results: { results: SearchResult[] }) => JSX.Element; }; +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ export const SearchResultComponent = ({ children }: Props) => { const { result: { loading, error, value }, From 193ae37f15760d95ea2e45bdd0c830e4551aac61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Wed, 1 Jun 2022 16:50:03 +0200 Subject: [PATCH 03/18] Move SearchResultPager from @backstage/plugin-search to @backstage/plugin-search-react and deprecate it in @backstage/plugin-search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 3 + plugins/search-react/package.json | 1 + .../SearchResultPager.test.tsx | 58 ++++++++++++++++ .../SearchResultPager/SearchResultPager.tsx | 66 +++++++++++++++++++ .../src/components/SearchResultPager/index.ts | 17 +++++ plugins/search-react/src/components/index.ts | 1 + plugins/search/api-report.md | 2 +- .../SearchResultPager/SearchResultPager.tsx | 3 + 8 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 plugins/search-react/src/components/SearchResultPager/SearchResultPager.test.tsx create mode 100644 plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx create mode 100644 plugins/search-react/src/components/SearchResultPager/index.ts diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 09dd3d77a8..5558fd3257 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -124,6 +124,9 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { // @public (undocumented) export const SearchResult: ({ children }: SearchResultProps) => JSX.Element; +// @public (undocumented) +export const SearchResultPager: () => JSX.Element; + // @public (undocumented) export type SearchResultProps = { children: (results: { results: SearchResult_2[] }) => JSX.Element; diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 04932bd5ea..95507e64b7 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -38,6 +38,7 @@ "@backstage/version-bridge": "^1.0.1", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "react-router": "6.0.0-beta.0", "react-use": "^17.3.2" diff --git a/plugins/search-react/src/components/SearchResultPager/SearchResultPager.test.tsx b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.test.tsx new file mode 100644 index 0000000000..24d9a76e51 --- /dev/null +++ b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.test.tsx @@ -0,0 +1,58 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { useSearch } from '../../context'; +import { SearchResultPager } from './SearchResultPager'; + +jest.mock('../../context', () => ({ + ...jest.requireActual('../../context'), + useSearch: jest.fn().mockReturnValue({ + result: {}, + }), +})); + +describe('SearchResultPager', () => { + it('renders pager buttons', async () => { + const fetchNextPage = jest.fn(); + const fetchPreviousPage = jest.fn(); + (useSearch as jest.Mock).mockReturnValueOnce({ + result: { loading: false, value: [] }, + fetchNextPage, + fetchPreviousPage, + }); + + const { getByLabelText } = await renderInTestApp(); + + await waitFor(() => { + expect(getByLabelText('previous page')).toBeInTheDocument(); + }); + await userEvent.click(getByLabelText('previous page')); + expect(fetchPreviousPage).toBeCalled(); + + await waitFor(() => { + expect(getByLabelText('next page')).toBeInTheDocument(); + }); + await userEvent.click(getByLabelText('next page')); + + expect(fetchNextPage).toBeCalled(); + }); +}); diff --git a/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx new file mode 100644 index 0000000000..68f8223ca0 --- /dev/null +++ b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx @@ -0,0 +1,66 @@ +/* + * 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 { Button, makeStyles } from '@material-ui/core'; +import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; +import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; + +import { useSearch } from '../../context'; + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + justifyContent: 'space-between', + gap: theme.spacing(2), + margin: theme.spacing(2, 0), + }, +})); + +/** + * @public + */ +export const SearchResultPager = () => { + const { fetchNextPage, fetchPreviousPage } = useSearch(); + const classes = useStyles(); + + if (!fetchNextPage && !fetchPreviousPage) { + return <>; + } + + return ( + + ); +}; diff --git a/plugins/search-react/src/components/SearchResultPager/index.ts b/plugins/search-react/src/components/SearchResultPager/index.ts new file mode 100644 index 0000000000..5ff203b15d --- /dev/null +++ b/plugins/search-react/src/components/SearchResultPager/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { SearchResultPager } from './SearchResultPager'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index db9214d299..bfd65d471f 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -17,3 +17,4 @@ export * from './HighlightedSearchResultText'; export * from './SearchFilter'; export * from './SearchResult'; +export * from './SearchResultPager'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index ec60569af4..25a7f40394 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -237,7 +237,7 @@ export const SearchResult: ({ // Warning: (ae-missing-release-tag) "SearchResultPager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export const SearchResultPager: () => JSX.Element; // Warning: (ae-missing-release-tag) "SearchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx index 6f4c8628a6..0e8d59a694 100644 --- a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx +++ b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx @@ -29,6 +29,9 @@ const useStyles = makeStyles(theme => ({ }, })); +/** + * @deprecated Moved to `@backstage/plugin-search-react`. + */ export const SearchResultPager = () => { const { fetchNextPage, fetchPreviousPage } = useSearch(); const classes = useStyles(); From ccbabfd4bdaea8319a911d2c4f718a8af153473b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Thu, 2 Jun 2022 10:49:09 +0200 Subject: [PATCH 04/18] Move SearchBar, SearchBarBase, and SearchTracker from @backstage/plugin-search to @backstage/plugin-search-react and deprecate SearchBar and SearchbarBase in @backstage/plugin-search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 37 ++ .../SearchBar/SearchBar.stories.tsx | 14 +- .../components/SearchBar/SearchBar.test.tsx | 352 ++++++++++++++++++ .../src/components/SearchBar/SearchBar.tsx | 179 +++++++++ .../src/components/SearchBar/index.tsx | 18 + .../SearchTracker/SearchTracker.tsx | 6 +- .../src/components/SearchTracker/index.ts | 2 +- plugins/search-react/src/components/index.ts | 2 + plugins/search/api-report.md | 8 +- .../src/components/SearchBar/SearchBar.tsx | 10 +- 10 files changed, 613 insertions(+), 15 deletions(-) rename plugins/{search => search-react}/src/components/SearchBar/SearchBar.stories.tsx (93%) create mode 100644 plugins/search-react/src/components/SearchBar/SearchBar.test.tsx create mode 100644 plugins/search-react/src/components/SearchBar/SearchBar.tsx create mode 100644 plugins/search-react/src/components/SearchBar/index.tsx rename plugins/{search => search-react}/src/components/SearchTracker/SearchTracker.tsx (91%) rename plugins/{search => search-react}/src/components/SearchTracker/index.ts (93%) diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 5558fd3257..27279e09a9 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; +import { InputBaseProps } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; @@ -62,6 +63,35 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { multiple?: boolean; }; +// @public +export const SearchBar: ({ onChange, ...props }: SearchBarProps) => JSX.Element; + +// @public +export const SearchBarBase: ({ + onChange, + onKeyDown, + onSubmit, + debounceTime, + clearButton, + fullWidth, + value: defaultValue, + inputProps: defaultInputProps, + endAdornment: defaultEndAdornment, + ...props +}: SearchBarBaseProps) => JSX.Element; + +// @public +export type SearchBarBaseProps = Omit & { + debounceTime?: number; + clearButton?: boolean; + onClear?: () => void; + onSubmit?: () => void; + onChange: (value: string) => void; +}; + +// @public +export type SearchBarProps = Partial; + // @public export const SearchContextProvider: ( props: SearchContextProviderProps, @@ -135,6 +165,13 @@ export type SearchResultProps = { // @public (undocumented) export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; +// @public +export const TrackSearch: ({ + children, +}: { + children: React_2.ReactChild; +}) => JSX.Element; + // @public export const useAsyncFilterValues: ( fn: ((partial: string) => Promise) | undefined, diff --git a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx similarity index 93% rename from plugins/search/src/components/SearchBar/SearchBar.stories.tsx rename to plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx index 9728101e97..88aed35f8d 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -14,14 +14,14 @@ * limitations under the License. */ -import { Grid, makeStyles, Paper } from '@material-ui/core'; import React, { ComponentType } from 'react'; -import { - searchApiRef, - MockSearchApi, - SearchContextProvider, -} from '@backstage/plugin-search-react'; +import { Grid, makeStyles, Paper } from '@material-ui/core'; + import { TestApiProvider } from '@backstage/test-utils'; + +import { searchApiRef, MockSearchApi } from '../../api'; +import { SearchContextProvider } from '../../context'; + import { SearchBar } from './SearchBar'; export default { diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx new file mode 100644 index 0000000000..4e2c4d7a5c --- /dev/null +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -0,0 +1,352 @@ +/* + * 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 { screen, render, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { configApiRef, analyticsApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; +import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils'; + +import { searchApiRef } from '../../api'; +import { SearchContextProvider } from '../../context'; +import { SearchBar } from './SearchBar'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), +})); + +describe('SearchBar', () => { + const initialState = { + term: '', + filters: {}, + types: ['*'], + pageCursor: '', + }; + + const query = jest.fn().mockResolvedValue({}); + const analyticsApiSpy = new MockAnalyticsApi(); + let apiRegistry: TestApiRegistry; + + apiRegistry = TestApiRegistry.from( + [ + configApiRef, + new ConfigReader({ + app: { title: 'Mock title' }, + }), + ], + [searchApiRef, { query }], + ); + + const name = 'Search'; + const term = 'term'; + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Renders without exploding', async () => { + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Search in Mock title'), + ).toBeInTheDocument(); + }); + }); + + it('Renders with custom placeholder', async () => { + render( + + + + + , + , + ); + + await waitFor(() => { + expect( + screen.getByPlaceholderText('This is a custom placeholder'), + ).toBeInTheDocument(); + }); + }); + + it('Renders based on initial search', async () => { + render( + + + + + , + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toHaveValue(term); + }); + }); + + it('Updates term state when text is entered', async () => { + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + const defaultDebounceTime = 200; + + render( + + + + + , + , + ); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + await user.type(textbox, value); + + act(() => { + jest.advanceTimersByTime(defaultDebounceTime); + }); + + await waitFor(() => { + expect(textbox).toHaveValue(value); + }); + + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + jest.useRealTimers(); + }); + + it('Clear button clears term state', async () => { + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toHaveValue(term); + }); + + await userEvent.click(screen.getByRole('button', { name: 'Clear' })); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toHaveValue(''); + }); + + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ term: '' }), + ); + }); + + it('Should not show clear button', async () => { + render( + + + + + , + ); + + await waitFor(() => { + expect( + screen.queryByRole('button', { name: 'Clear' }), + ).not.toBeInTheDocument(); + }); + }); + + it('Adheres to provided debounceTime', async () => { + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + + const debounceTime = 600; + + render( + + + + + , + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + await user.type(textbox, value); + + expect(query).not.toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + expect(textbox).toHaveValue(value); + + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ term: value }), + ); + jest.useRealTimers(); + }); + + it('does not capture analytics event if not enabled in app', async () => { + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + + const debounceTime = 600; + + render( + + + + + , + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + await user.type(textbox, value); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => expect(textbox).toHaveValue(value)); + + expect(analyticsApiSpy.getEvents()).toHaveLength(0); + jest.useRealTimers(); + }); + + it('captures analytics events if enabled in app', async () => { + const user = userEvent.setup({ delay: null }); + jest.useFakeTimers(); + + const debounceTime = 600; + + apiRegistry = TestApiRegistry.from( + [analyticsApiRef, analyticsApiSpy], + [ + configApiRef, + new ConfigReader({ + app: { + title: 'Mock title', + analytics: { + ga: { + trackingId: 'xyz123', + }, + }, + }, + }), + ], + [searchApiRef, { query }], + ); + + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('textbox', { name })).toBeInTheDocument(); + }); + + const textbox = screen.getByRole('textbox', { name }); + + const value = 'value'; + + await user.type(textbox, value); + + expect(analyticsApiSpy.getEvents()).toHaveLength(0); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => expect(textbox).toHaveValue(value)); + + expect(analyticsApiSpy.getEvents()).toHaveLength(1); + expect(analyticsApiSpy.getEvents()[0]).toEqual({ + action: 'search', + context: { + extension: 'App', + pluginId: 'root', + routeRef: 'unknown', + searchTypes: 'software-catalog,techdocs', + }, + subject: 'value', + }); + + await user.clear(textbox); + + // make sure new term is captured + await user.type(textbox, 'new value'); + + act(() => { + jest.advanceTimersByTime(debounceTime); + }); + + await waitFor(() => expect(textbox).toHaveValue('new value')); + + expect(analyticsApiSpy.getEvents()).toHaveLength(2); + expect(analyticsApiSpy.getEvents()[1]).toEqual({ + action: 'search', + context: { + extension: 'App', + pluginId: 'root', + routeRef: 'unknown', + searchTypes: 'software-catalog,techdocs', + }, + subject: 'new value', + }); + jest.useRealTimers(); + }); +}); diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.tsx new file mode 100644 index 0000000000..b3c8ec9fa5 --- /dev/null +++ b/plugins/search-react/src/components/SearchBar/SearchBar.tsx @@ -0,0 +1,179 @@ +/* + * 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, { + ChangeEvent, + KeyboardEvent, + useState, + useEffect, + useCallback, +} from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { + InputBase, + InputBaseProps, + InputAdornment, + IconButton, +} from '@material-ui/core'; +import SearchIcon from '@material-ui/icons/Search'; +import ClearButton from '@material-ui/icons/Clear'; + +import { configApiRef, useApi } from '@backstage/core-plugin-api'; + +import { + SearchContextProvider, + useSearch, + useSearchContextCheck, +} from '../../context'; +import { TrackSearch } from '../SearchTracker'; + +/** + * Props for {@link SearchBarBase}. + * + * @public + */ +export type SearchBarBaseProps = Omit & { + debounceTime?: number; + clearButton?: boolean; + onClear?: () => void; + onSubmit?: () => void; + onChange: (value: string) => void; +}; + +/** + * All search boxes exported by the search plugin are based on the , + * and this one is based on the component from Material UI. + * Recommended if you don't use Search Provider or Search Context. + * + * @public + */ +export const SearchBarBase = ({ + onChange, + onKeyDown, + onSubmit, + debounceTime = 200, + clearButton = true, + fullWidth = true, + value: defaultValue, + inputProps: defaultInputProps = {}, + endAdornment: defaultEndAdornment, + ...props +}: SearchBarBaseProps) => { + const configApi = useApi(configApiRef); + const [value, setValue] = useState(defaultValue as string); + const hasSearchContext = useSearchContextCheck(); + + useEffect(() => { + setValue(prevValue => + prevValue !== defaultValue ? (defaultValue as string) : prevValue, + ); + }, [defaultValue]); + + useDebounce(() => onChange(value), debounceTime, [value]); + + const handleChange = useCallback( + (e: ChangeEvent) => { + setValue(e.target.value); + }, + [setValue], + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (onKeyDown) onKeyDown(e); + if (onSubmit && e.key === 'Enter') { + onSubmit(); + } + }, + [onKeyDown, onSubmit], + ); + + const handleClear = useCallback(() => { + onChange(''); + }, [onChange]); + + const placeholder = `Search in ${ + configApi.getOptionalString('app.title') || 'Backstage' + }`; + + const startAdornment = ( + + + + + + ); + + const endAdornment = ( + + + + + + ); + + const searchBar = ( + + + + ); + + return hasSearchContext ? ( + searchBar + ) : ( + {searchBar} + ); +}; + +/** + * Props for {@link SearchBar}. + * + * @public + */ +export type SearchBarProps = Partial; + +/** + * Recommended search bar when you use the Search Provider or Search Context. + * + * @public + */ +export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { + const { term, setTerm } = useSearch(); + + const handleChange = useCallback( + (newValue: string) => { + if (onChange) { + onChange(newValue); + } else { + setTerm(newValue); + } + }, + [onChange, setTerm], + ); + + return ; +}; diff --git a/plugins/search-react/src/components/SearchBar/index.tsx b/plugins/search-react/src/components/SearchBar/index.tsx new file mode 100644 index 0000000000..075a0c7dc2 --- /dev/null +++ b/plugins/search-react/src/components/SearchBar/index.tsx @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { SearchBar, SearchBarBase } from './SearchBar'; +export type { SearchBarProps, SearchBarBaseProps } from './SearchBar'; diff --git a/plugins/search/src/components/SearchTracker/SearchTracker.tsx b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx similarity index 91% rename from plugins/search/src/components/SearchTracker/SearchTracker.tsx rename to plugins/search-react/src/components/SearchTracker/SearchTracker.tsx index 35cfa1ec38..8ca7373298 100644 --- a/plugins/search/src/components/SearchTracker/SearchTracker.tsx +++ b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -16,10 +16,12 @@ import React, { useEffect } from 'react'; import { useAnalytics } from '@backstage/core-plugin-api'; -import { useSearch } from '@backstage/plugin-search-react'; +import { useSearch } from '../../context'; /** * Capture search event on term change. + * + * @public */ export const TrackSearch = ({ children }: { children: React.ReactChild }) => { const analytics = useAnalytics(); diff --git a/plugins/search/src/components/SearchTracker/index.ts b/plugins/search-react/src/components/SearchTracker/index.ts similarity index 93% rename from plugins/search/src/components/SearchTracker/index.ts rename to plugins/search-react/src/components/SearchTracker/index.ts index 5e6a75aa25..9932f2eaec 100644 --- a/plugins/search/src/components/SearchTracker/index.ts +++ b/plugins/search-react/src/components/SearchTracker/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index bfd65d471f..5b7767859e 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -18,3 +18,5 @@ export * from './HighlightedSearchResultText'; export * from './SearchFilter'; export * from './SearchResult'; export * from './SearchResultPager'; +export * from './SearchBar'; +export * from './SearchTracker'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 25a7f40394..ea5f0b4d0d 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -87,10 +87,10 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps_2 & { multiple?: boolean; }; -// @public +// @public @deprecated (undocumented) export const SearchBar: ({ onChange, ...props }: SearchBarProps) => JSX.Element; -// @public +// @public @deprecated (undocumented) export const SearchBarBase: ({ onChange, onKeyDown, @@ -104,7 +104,7 @@ export const SearchBarBase: ({ ...props }: SearchBarBaseProps) => JSX.Element; -// @public +// @public @deprecated (undocumented) export type SearchBarBaseProps = Omit & { debounceTime?: number; clearButton?: boolean; @@ -121,7 +121,7 @@ export const SearchBarNext: ({ ...props }: Partial) => JSX.Element; -// @public +// @public @deprecated (undocumented) export type SearchBarProps = Partial; // Warning: (ae-missing-release-tag) "SearchFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index b5d55fed8e..cb61089eb6 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -36,10 +36,12 @@ import { SearchContextProvider, useSearch, useSearchContextCheck, + TrackSearch, } from '@backstage/plugin-search-react'; -import { TrackSearch } from '../SearchTracker'; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * Props for {@link SearchBarBase}. * * @public @@ -53,6 +55,8 @@ export type SearchBarBaseProps = Omit & { }; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * All search boxes exported by the search plugin are based on the , * and this one is based on the component from Material UI. * Recommended if you don't use Search Provider or Search Context. @@ -149,6 +153,8 @@ export const SearchBarBase = ({ }; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * Props for {@link SearchBar}. * * @public @@ -156,6 +162,8 @@ export const SearchBarBase = ({ export type SearchBarProps = Partial; /** + * @deprecated Moved to `@backstage/plugin-search-react`. + * * Recommended search bar when you use the Search Provider or Search Context. * * @public From b3389f1eeef23a8d025370b849f63b0be6856963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Thu, 2 Jun 2022 13:15:29 +0200 Subject: [PATCH 05/18] remove Search*Next components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search/api-report.md | 14 ----------- plugins/search/src/index.ts | 2 -- plugins/search/src/plugin.ts | 49 ------------------------------------ 3 files changed, 65 deletions(-) diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index ea5f0b4d0d..0527c46fe8 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -113,14 +113,6 @@ export type SearchBarBaseProps = Omit & { onChange: (value: string) => void; }; -// Warning: (ae-missing-release-tag) "SearchBarNext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const SearchBarNext: ({ - onChange, - ...props -}: Partial) => JSX.Element; - // @public @deprecated (undocumented) export type SearchBarProps = Partial; @@ -208,18 +200,12 @@ export type SearchModalValue = { // @public (undocumented) export const SearchPage: () => JSX.Element; -// Warning: (ae-missing-release-tag) "SearchPageNext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const SearchPageNext: () => JSX.Element; - // Warning: (ae-missing-release-tag) "searchPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) const searchPlugin: BackstagePlugin< { root: RouteRef; - nextRoot: RouteRef; }, {} >; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 2eb2ed277b..bcb8f40506 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -60,9 +60,7 @@ export type { SidebarSearchModalProps } from './components/SidebarSearchModal'; export { DefaultResultListItem, HomePageSearchBar, - SearchBarNext, SearchPage, - SearchPageNext, searchPlugin as plugin, searchPlugin, SearchResult, diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 96e7bc1eed..e535295604 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -30,10 +30,6 @@ export const rootRouteRef = createRouteRef({ id: 'search', }); -export const rootNextRouteRef = createRouteRef({ - id: 'search:next', -}); - export const searchPlugin = createPlugin({ id: 'search', apis: [ @@ -47,7 +43,6 @@ export const searchPlugin = createPlugin({ ], routes: { root: rootRouteRef, - nextRoot: rootNextRouteRef, }, }); @@ -59,20 +54,6 @@ export const SearchPage = searchPlugin.provide( }), ); -/** - * @deprecated This component was used for rapid prototyping of the Backstage - * Search platform. Now that the API has stabilized, you should use the - * component instead. This component will be removed in an - * upcoming release. - */ -export const SearchPageNext = searchPlugin.provide( - createRoutableExtension({ - name: 'SearchPageNext', - component: () => import('./components/SearchPage').then(m => m.SearchPage), - mountPoint: rootNextRouteRef, - }), -); - export const SearchBar = searchPlugin.provide( createComponentExtension({ name: 'SearchBar', @@ -82,21 +63,6 @@ export const SearchBar = searchPlugin.provide( }), ); -/** - * @deprecated This component was used for rapid prototyping of the Backstage - * Search platform. Now that the API has stabilized, you should use the - * component instead. This component will be removed in an - * upcoming release. - */ -export const SearchBarNext = searchPlugin.provide( - createComponentExtension({ - name: 'SearchBarNext', - component: { - lazy: () => import('./components/SearchBar').then(m => m.SearchBar), - }, - }), -); - export const SearchResult = searchPlugin.provide( createComponentExtension({ name: 'SearchResult', @@ -106,21 +72,6 @@ export const SearchResult = searchPlugin.provide( }), ); -/** - * @deprecated This component was used for rapid prototyping of the Backstage - * Search platform. Now that the API has stabilized, you should use the - * component instead. This component will be removed in an - * upcoming release. - */ -export const SearchResultNext = searchPlugin.provide( - createComponentExtension({ - name: 'SearchResultNext', - component: { - lazy: () => import('./components/SearchResult').then(m => m.SearchResult), - }, - }), -); - export const SidebarSearchModal = searchPlugin.provide( createComponentExtension({ name: 'SidebarSearchModal', From bc6ce63e7bf5794cddf976e9e7eb8897d71a6f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 09:41:14 +0200 Subject: [PATCH 06/18] Move DefaultResultListItem from @backstage/plugin-search to @backstage/plugin-search-react and deprecate it in @backstage/plugin-search, and properly deprecate SearchResult in @backstage/plugin-search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 28 +++- plugins/search-react/package.json | 2 +- .../DefaultResultListItem.stories.tsx | 2 +- .../DefaultResultListItem.test.tsx | 2 +- .../DefaultResultListItem.tsx | 43 +++++- .../components/DefaultResultListItem/index.ts | 3 +- .../SearchResult/SearchResult.stories.tsx | 9 +- .../SearchResult/SearchResult.test.tsx | 6 +- .../components/SearchResult/SearchResult.tsx | 29 +++- .../src/components/SearchResult/index.tsx | 2 +- plugins/search-react/src/components/index.ts | 1 + plugins/search/api-report.md | 31 ++--- .../SearchModal/SearchModal.stories.tsx | 4 +- .../components/SearchModal/SearchModal.tsx | 4 +- .../SearchResult/SearchResult.test.tsx | 125 ------------------ .../components/SearchResult/SearchResult.tsx | 57 -------- .../src/components/SearchResult/index.tsx | 17 --- plugins/search/src/plugin.ts | 33 ++--- 18 files changed, 131 insertions(+), 267 deletions(-) rename plugins/{search => search-react}/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx (98%) rename plugins/{search => search-react}/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx (98%) rename plugins/{search => search-react}/src/components/DefaultResultListItem/DefaultResultListItem.tsx (71%) rename plugins/{search => search-react}/src/components/DefaultResultListItem/index.ts (84%) delete mode 100644 plugins/search/src/components/SearchResult/SearchResult.test.tsx delete mode 100644 plugins/search/src/components/SearchResult/SearchResult.tsx delete mode 100644 plugins/search/src/components/SearchResult/index.tsx diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 27279e09a9..c36e7c67fe 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -12,6 +12,9 @@ import { JsonObject } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; +import { ReactNode } from 'react'; +import { ResultHighlight } from '@backstage/plugin-search-common'; +import { SearchDocument } from '@backstage/plugin-search-common'; import { SearchQuery } from '@backstage/plugin-search-common'; import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; import { SearchResultSet } from '@backstage/plugin-search-common'; @@ -24,6 +27,20 @@ export const AutocompleteFilter: ( // @public (undocumented) export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; +// @public +export const DefaultResultListItem: ( + props: DefaultResultListItemProps, +) => JSX.Element; + +// @public +export type DefaultResultListItemProps = { + icon?: ReactNode; + secondaryAction?: ReactNode; + result: SearchDocument; + highlight?: ResultHighlight; + lineClamp?: number; +}; + // @public (undocumented) export const HighlightedSearchResultText: ({ text, @@ -151,13 +168,18 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; -// @public (undocumented) -export const SearchResult: ({ children }: SearchResultProps) => JSX.Element; +// @public +export const SearchResult: (props: SearchResultProps) => JSX.Element; + +// @public +export const SearchResultComponent: ({ + children, +}: SearchResultProps) => JSX.Element; // @public (undocumented) export const SearchResultPager: () => JSX.Element; -// @public (undocumented) +// @public export type SearchResultProps = { children: (results: { results: SearchResult_2[] }) => JSX.Element; }; diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 95507e64b7..60cdc479bb 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -31,11 +31,11 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/plugin-search": "0.8.2-next.1", "@backstage/plugin-search-common": "^0.3.5-next.0", "@backstage/core-components": "^0.9.5-next.1", "@backstage/core-plugin-api": "^1.0.3-next.0", "@backstage/version-bridge": "^1.0.1", + "@backstage/theme": "^0.2.15", "@backstage/types": "^1.0.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx similarity index 98% rename from plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx rename to plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx index fb56d1ecdb..3b54e69a19 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx similarity index 98% rename from plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx rename to plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx index 26d081b3b6..e94d394639 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx similarity index 71% rename from plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx rename to plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx index 5ce2d0744a..48f12213e4 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -15,11 +15,12 @@ */ import React, { ReactNode } from 'react'; +import { AnalyticsContext } from '@backstage/core-plugin-api'; import { ResultHighlight, SearchDocument, } from '@backstage/plugin-search-common'; -import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; +import { HighlightedSearchResultText } from '../HighlightedSearchResultText'; import { ListItem, ListItemIcon, @@ -29,7 +30,12 @@ import { } from '@material-ui/core'; import { Link } from '@backstage/core-components'; -type Props = { +/** + * Props for {@link DefaultResultListItem} + * + * @public + */ +export type DefaultResultListItemProps = { icon?: ReactNode; secondaryAction?: ReactNode; result: SearchDocument; @@ -37,13 +43,18 @@ type Props = { lineClamp?: number; }; -export const DefaultResultListItem = ({ +/** + * A default result list item. + * + * @public + */ +export const DefaultResultListItemComponent = ({ result, highlight, icon, secondaryAction, lineClamp = 5, -}: Props) => { +}: DefaultResultListItemProps) => { return ( @@ -88,3 +99,25 @@ export const DefaultResultListItem = ({ ); }; + +/** + * A higher order function providing AnalyticsContext to the DefaultResultListItemComponent. + * + * @public + */ +const HigherOrderDefaultResultListItem = ( + props: DefaultResultListItemProps, +) => { + return ( + + + + ); +}; + +export { HigherOrderDefaultResultListItem as DefaultResultListItem }; diff --git a/plugins/search/src/components/DefaultResultListItem/index.ts b/plugins/search-react/src/components/DefaultResultListItem/index.ts similarity index 84% rename from plugins/search/src/components/DefaultResultListItem/index.ts rename to plugins/search-react/src/components/DefaultResultListItem/index.ts index 77f975a9ef..a555f25c02 100644 --- a/plugins/search/src/components/DefaultResultListItem/index.ts +++ b/plugins/search-react/src/components/DefaultResultListItem/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -15,3 +15,4 @@ */ export { DefaultResultListItem } from './DefaultResultListItem'; +export type { DefaultResultListItemProps } from './DefaultResultListItem'; diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx index 38be586f97..32f9a19a07 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx @@ -14,16 +14,17 @@ * limitations under the License. */ -import { Link } from '@backstage/core-components'; -import { List, ListItem } from '@material-ui/core'; import React, { ComponentType } from 'react'; +import { List, ListItem } from '@material-ui/core'; import { MemoryRouter } from 'react-router'; -import { DefaultResultListItem } from '@backstage/plugin-search'; + +import { Link } from '@backstage/core-components'; +import { TestApiProvider } from '@backstage/test-utils'; import { searchApiRef, MockSearchApi } from '../../api'; import { SearchContextProvider } from '../../context'; +import { DefaultResultListItem } from '../DefaultResultListItem'; import { SearchResult } from './SearchResult'; -import { TestApiProvider } from '@backstage/test-utils'; const mockResults = { results: [ diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx index 10437b3af7..988d59d7cf 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ -import { renderInTestApp } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; import React from 'react'; +import { waitFor } from '@testing-library/react'; + +import { renderInTestApp } from '@backstage/test-utils'; + import { useSearch } from '../../context'; import { SearchResult } from './SearchResult'; diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx index a1ce3cea25..54024d546a 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -14,16 +14,21 @@ * limitations under the License. */ +import React from 'react'; + import { EmptyState, Progress, ResponseErrorPanel, } from '@backstage/core-components'; +import { AnalyticsContext } from '@backstage/core-plugin-api'; import { SearchResult } from '@backstage/plugin-search-common'; -import React from 'react'; + import { useSearch } from '../../context'; /** + * Props for {@link SearchResultComponent} + * * @public */ export type SearchResultProps = { @@ -31,6 +36,8 @@ export type SearchResultProps = { }; /** + * A component returning the search result. + * * @public */ export const SearchResultComponent = ({ children }: SearchResultProps) => { @@ -57,4 +64,22 @@ export const SearchResultComponent = ({ children }: SearchResultProps) => { return <>{children({ results: value.results })}; }; -export { SearchResultComponent as SearchResult }; +/** + * A higher order function providing AnalyticsContext to the SearchResultComponent. + * + * @public + */ +const HigherOrderSearchResult = (props: SearchResultProps) => { + return ( + + + + ); +}; + +export { HigherOrderSearchResult as SearchResult }; diff --git a/plugins/search-react/src/components/SearchResult/index.tsx b/plugins/search-react/src/components/SearchResult/index.tsx index 2032fbbed9..503ac47bbd 100644 --- a/plugins/search-react/src/components/SearchResult/index.tsx +++ b/plugins/search-react/src/components/SearchResult/index.tsx @@ -14,5 +14,5 @@ * limitations under the License. */ -export { SearchResult } from './SearchResult'; +export { SearchResult, SearchResultComponent } from './SearchResult'; export type { SearchResultProps } from './SearchResult'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 5b7767859e..155a4c3b12 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -20,3 +20,4 @@ export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchBar'; export * from './SearchTracker'; +export * from './DefaultResultListItem'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 0527c46fe8..746e23e880 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -6,33 +6,22 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DefaultResultListItemProps } from '@backstage/plugin-search-react/src/components/DefaultResultListItem/DefaultResultListItem'; import { IconComponent } from '@backstage/core-plugin-api'; import { InputBaseProps } from '@material-ui/core'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; -import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; import { SearchAutocompleteFilterProps as SearchAutocompleteFilterProps_2 } from '@backstage/plugin-search-react'; -import { SearchDocument } from '@backstage/plugin-search-common'; import { SearchFilterComponentProps as SearchFilterComponentProps_2 } from '@backstage/plugin-search-react'; -import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; +import { SearchResultProps } from '@backstage/plugin-search-react'; // Warning: (ae-missing-release-tag) "DefaultResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export const DefaultResultListItem: ({ - result, - highlight, - icon, - secondaryAction, - lineClamp, -}: { - icon?: ReactNode; - secondaryAction?: ReactNode; - result: SearchDocument; - highlight?: ResultHighlight | undefined; - lineClamp?: number | undefined; -}) => JSX.Element; +// @public @deprecated (undocumented) +export const DefaultResultListItem: ( + props: DefaultResultListItemProps, +) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "FiltersProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Filters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -214,12 +203,8 @@ export { searchPlugin }; // Warning: (ae-missing-release-tag) "SearchResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export const SearchResult: ({ - children, -}: { - children: (results: { results: SearchResult_2[] }) => JSX.Element; -}) => JSX.Element; +// @public @deprecated (undocumented) +export const SearchResult: (props: SearchResultProps) => JSX.Element; // Warning: (ae-missing-release-tag) "SearchResultPager" 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 a4e92f53ec..870f73e472 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -27,16 +27,16 @@ import { 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 { + DefaultResultListItem, searchApiRef, MockSearchApi, SearchContextProvider, + SearchResult, } from '@backstage/plugin-search-react'; import { TestApiProvider } from '@backstage/test-utils'; import { SearchModal } from './SearchModal'; -import { SearchResult } from '../SearchResult'; import { SearchResultPager } from '../SearchResultPager'; import { SearchType } from '../SearchType'; import { useSearchModal } from './useSearchModal'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index b3df52d42e..18776d444d 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -29,10 +29,10 @@ import { import LaunchIcon from '@material-ui/icons/Launch'; import { makeStyles } from '@material-ui/core/styles'; import { SearchBar } from '../SearchBar'; -import { DefaultResultListItem } from '../DefaultResultListItem'; -import { SearchResult } from '../SearchResult'; import { + DefaultResultListItem, SearchContextProvider, + SearchResult, useSearch, } from '@backstage/plugin-search-react'; import { SearchResultPager } from '../SearchResultPager'; diff --git a/plugins/search/src/components/SearchResult/SearchResult.test.tsx b/plugins/search/src/components/SearchResult/SearchResult.test.tsx deleted file mode 100644 index d93eacaef0..0000000000 --- a/plugins/search/src/components/SearchResult/SearchResult.test.tsx +++ /dev/null @@ -1,125 +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 { renderInTestApp } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; -import React from 'react'; -import { useSearch } from '@backstage/plugin-search-react'; -import { SearchResult } from './SearchResult'; - -jest.mock('@backstage/plugin-search-react', () => ({ - ...jest.requireActual('@backstage/plugin-search-react'), - useSearch: jest.fn().mockReturnValue({ - result: {}, - }), -})); - -describe('SearchResult', () => { - it('Progress rendered on Loading state', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { loading: true }, - }); - - const { getByRole } = await renderInTestApp( - {() => <>}, - ); - - await waitFor(() => { - expect(getByRole('progressbar')).toBeInTheDocument(); - }); - }); - - it('Alert rendered on Error state', async () => { - const error = new Error('some error'); - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { loading: false, error }, - }); - - const { getByRole } = await renderInTestApp( - {() => <>}, - ); - - await waitFor(() => { - expect(getByRole('alert')).toHaveTextContent( - new RegExp(`Error encountered while fetching search results.*${error}`), - ); - }); - }); - - it('On no result value state', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { loading: false, error: '', value: undefined }, - }); - - const { getByRole } = await renderInTestApp( - {() => <>}, - ); - - await waitFor(() => { - expect( - getByRole('heading', { name: 'Sorry, no results were found' }), - ).toBeInTheDocument(); - }); - }); - - it('On empty result value state', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { loading: false, error: '', value: { results: [] } }, - }); - - const { getByRole } = await renderInTestApp( - {() => <>}, - ); - - await waitFor(() => { - expect( - getByRole('heading', { name: 'Sorry, no results were found' }), - ).toBeInTheDocument(); - }); - }); - - it('Calls children with results set to result.value', async () => { - (useSearch as jest.Mock).mockReturnValueOnce({ - result: { - loading: false, - error: '', - value: { - totalCount: 1, - results: [ - { - type: 'some-type', - document: { - title: 'some-title', - text: 'some-text', - location: 'some-location', - }, - }, - ], - }, - }, - }); - - const { getByText } = await renderInTestApp( - - {({ results }) => { - return <>Results {results.length}; - }} - , - ); - - expect(getByText('Results 1')).toBeInTheDocument(); - }); -}); diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx deleted file mode 100644 index 77ee883164..0000000000 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ /dev/null @@ -1,57 +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 { - EmptyState, - Progress, - ResponseErrorPanel, -} from '@backstage/core-components'; -import { SearchResult } from '@backstage/plugin-search-common'; -import React from 'react'; -import { useSearch } from '@backstage/plugin-search-react'; - -type Props = { - children: (results: { results: SearchResult[] }) => JSX.Element; -}; - -/** - * @deprecated Moved to `@backstage/plugin-search-react`. - */ -export const SearchResultComponent = ({ children }: Props) => { - const { - result: { loading, error, value }, - } = useSearch(); - - if (loading) { - return ; - } - if (error) { - return ( - - ); - } - - if (!value?.results.length) { - return ; - } - - return <>{children({ results: value.results })}; -}; - -export { SearchResultComponent as SearchResult }; diff --git a/plugins/search/src/components/SearchResult/index.tsx b/plugins/search/src/components/SearchResult/index.tsx deleted file mode 100644 index e7d088f286..0000000000 --- a/plugins/search/src/components/SearchResult/index.tsx +++ /dev/null @@ -1,17 +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 { SearchResult } from './SearchResult'; diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index e535295604..fdfa2f59d0 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -15,7 +15,11 @@ */ import { SearchClient } from './apis'; -import { searchApiRef } from '@backstage/plugin-search-react'; +import { + searchApiRef, + SearchResult as RealSearchResult, + DefaultResultListItem as RealDefaultResultListItem, +} from '@backstage/plugin-search-react'; import { createApiFactory, createPlugin, @@ -63,14 +67,10 @@ export const SearchBar = searchPlugin.provide( }), ); -export const SearchResult = searchPlugin.provide( - createComponentExtension({ - name: 'SearchResult', - component: { - lazy: () => import('./components/SearchResult').then(m => m.SearchResult), - }, - }), -); +/** + * @deprecated Import from `@backstage/plugin-search-react` instead. + */ +export const SearchResult = RealSearchResult; export const SidebarSearchModal = searchPlugin.provide( createComponentExtension({ @@ -84,17 +84,10 @@ export const SidebarSearchModal = searchPlugin.provide( }), ); -export const DefaultResultListItem = searchPlugin.provide( - createComponentExtension({ - name: 'DefaultResultListItem', - component: { - lazy: () => - import('./components/DefaultResultListItem').then( - m => m.DefaultResultListItem, - ), - }, - }), -); +/** + * @deprecated Import from `@backstage/plugin-search-react` instead. + */ +export const DefaultResultListItem = RealDefaultResultListItem; export const HomePageSearchBar = searchPlugin.provide( createComponentExtension({ From a7a0e2520ae5ccc0b699ea64a37d7d3817db661d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 09:44:22 +0200 Subject: [PATCH 07/18] cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search/api-report.md | 2 +- plugins/search/src/plugin.ts | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 746e23e880..3cbbf8072c 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -6,7 +6,7 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { DefaultResultListItemProps } from '@backstage/plugin-search-react/src/components/DefaultResultListItem/DefaultResultListItem'; +import { DefaultResultListItemProps } from '@backstage/plugin-search-react'; import { IconComponent } from '@backstage/core-plugin-api'; import { InputBaseProps } from '@material-ui/core'; import { ReactElement } from 'react'; diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index fdfa2f59d0..103d67b81a 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -58,15 +58,6 @@ export const SearchPage = searchPlugin.provide( }), ); -export const SearchBar = searchPlugin.provide( - createComponentExtension({ - name: 'SearchBar', - component: { - lazy: () => import('./components/SearchBar').then(m => m.SearchBar), - }, - }), -); - /** * @deprecated Import from `@backstage/plugin-search-react` instead. */ From b3d64b448ec9f9f9e82ac52c4335f684dcb041e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 11:35:52 +0200 Subject: [PATCH 08/18] api-report cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search-react/api-report.md | 2 +- .../HighlightedSearchResultText.tsx | 2 + plugins/search/api-report.md | 73 +++++++------------ .../search/src/components/Filters/Filters.tsx | 16 +++- .../src/components/Filters/FiltersButton.tsx | 10 ++- .../search/src/components/Filters/index.tsx | 3 +- .../HomePageComponent/HomePageSearchBar.tsx | 4 +- .../src/components/SearchBar/SearchBar.tsx | 12 +-- .../SearchFilter.Autocomplete.tsx | 4 +- .../components/SearchFilter/SearchFilter.tsx | 11 ++- .../components/SearchModal/SearchModal.tsx | 6 ++ .../src/components/SearchPage/SearchPage.tsx | 3 + .../SearchResultPager/SearchResultPager.tsx | 1 + .../src/components/SearchType/SearchType.tsx | 5 ++ .../SidebarSearch/SidebarSearch.tsx | 8 ++ .../SidebarSearchModal/SidebarSearchModal.tsx | 5 ++ plugins/search/src/index.ts | 7 +- plugins/search/src/plugin.ts | 14 ++++ 18 files changed, 116 insertions(+), 70 deletions(-) diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index c36e7c67fe..93ed657d85 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -48,7 +48,7 @@ export const HighlightedSearchResultText: ({ postTag, }: HighlightedSearchResultTextProps) => JSX.Element; -// @public (undocumented) +// @public export type HighlightedSearchResultTextProps = { text: string; preTag: string; diff --git a/plugins/search-react/src/components/HighlightedSearchResultText/HighlightedSearchResultText.tsx b/plugins/search-react/src/components/HighlightedSearchResultText/HighlightedSearchResultText.tsx index a749f184fe..bd6ef2d09c 100644 --- a/plugins/search-react/src/components/HighlightedSearchResultText/HighlightedSearchResultText.tsx +++ b/plugins/search-react/src/components/HighlightedSearchResultText/HighlightedSearchResultText.tsx @@ -25,6 +25,8 @@ const useStyles = makeStyles( ); /** + * Props for {@link HighlightedSearchResultText}. + * * @public */ export type HighlightedSearchResultTextProps = { diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 3cbbf8072c..c2f1e93bff 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -16,16 +16,17 @@ import { SearchAutocompleteFilterProps as SearchAutocompleteFilterProps_2 } from import { SearchFilterComponentProps as SearchFilterComponentProps_2 } from '@backstage/plugin-search-react'; import { SearchResultProps } from '@backstage/plugin-search-react'; -// Warning: (ae-missing-release-tag) "DefaultResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const DefaultResultListItem: ( props: DefaultResultListItemProps, ) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "FiltersProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "Filters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export type FilterOptions = { + kind: Array; + lifecycle: Array; +}; + // @public (undocumented) export const Filters: ({ filters, @@ -35,25 +36,33 @@ export const Filters: ({ updateChecked, }: FiltersProps) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "FiltersButtonProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "FiltersButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const FiltersButton: ({ numberOfSelectedFilters, handleToggleFilters, }: FiltersButtonProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "FiltersState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type FiltersButtonProps = { + numberOfSelectedFilters: number; + handleToggleFilters: () => void; +}; + +// @public +export type FiltersProps = { + filters: FiltersState; + filterOptions: FilterOptions; + resetFilters: () => void; + updateSelected: (filter: string) => void; + updateChecked: (filter: string) => void; +}; + // @public (undocumented) export type FiltersState = { selected: string; checked: Array; }; -// Warning: (ae-missing-release-tag) "HomePageSearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const HomePageSearchBar: ({ ...props @@ -64,8 +73,6 @@ export type HomePageSearchBarProps = Partial< Omit >; -// Warning: (ae-missing-release-tag) "SearchPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const Router: () => JSX.Element; @@ -76,10 +83,10 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps_2 & { multiple?: boolean; }; -// @public @deprecated (undocumented) +// @public @deprecated export const SearchBar: ({ onChange, ...props }: SearchBarProps) => JSX.Element; -// @public @deprecated (undocumented) +// @public @deprecated export const SearchBarBase: ({ onChange, onKeyDown, @@ -93,7 +100,7 @@ export const SearchBarBase: ({ ...props }: SearchBarBaseProps) => JSX.Element; -// @public @deprecated (undocumented) +// @public @deprecated export type SearchBarBaseProps = Omit & { debounceTime?: number; clearButton?: boolean; @@ -102,11 +109,9 @@ export type SearchBarBaseProps = Omit & { onChange: (value: string) => void; }; -// @public @deprecated (undocumented) +// @public @deprecated export type SearchBarProps = Partial; -// Warning: (ae-missing-release-tag) "SearchFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const SearchFilter: { ({ component: Element, ...props }: SearchFilterWrapperProps): JSX.Element; @@ -137,8 +142,6 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; -// Warning: (ae-missing-release-tag) "SearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const SearchModal: ({ open, @@ -152,8 +155,6 @@ 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; @@ -184,13 +185,9 @@ export type SearchModalValue = { setOpen: (open: boolean) => void; }; -// Warning: (ae-missing-release-tag) "SearchPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const SearchPage: () => JSX.Element; -// Warning: (ae-missing-release-tag) "searchPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) const searchPlugin: BackstagePlugin< { @@ -201,18 +198,12 @@ const searchPlugin: BackstagePlugin< export { searchPlugin as plugin }; export { searchPlugin }; -// Warning: (ae-missing-release-tag) "SearchResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const SearchResult: (props: SearchResultProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "SearchResultPager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const SearchResultPager: () => JSX.Element; -// 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: { (props: SearchTypeProps): JSX.Element; @@ -231,7 +222,7 @@ export type SearchTypeAccordionProps = { defaultValue?: string; }; -// @public (undocumented) +// @public export type SearchTypeProps = { className?: string; name: string; @@ -248,29 +239,21 @@ export type SearchTypeTabsProps = { 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) export const SidebarSearch: (props: SidebarSearchProps) => JSX.Element; -// Warning: (ae-missing-release-tag) "SidebarSearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const SidebarSearchModal: ( props: SidebarSearchModalProps, ) => JSX.Element; -// Warning: (ae-missing-release-tag) "SidebarSearchModalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public 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) -// -// @public (undocumented) +// @public export type SidebarSearchProps = { icon?: IconComponent; }; diff --git a/plugins/search/src/components/Filters/Filters.tsx b/plugins/search/src/components/Filters/Filters.tsx index a0ca4914a2..9bbe90d32f 100644 --- a/plugins/search/src/components/Filters/Filters.tsx +++ b/plugins/search/src/components/Filters/Filters.tsx @@ -44,17 +44,28 @@ const useStyles = makeStyles(theme => ({ }, })); +/** + * @public + */ export type FiltersState = { selected: string; checked: Array; }; +/** + * @public + */ export type FilterOptions = { kind: Array; lifecycle: Array; }; -type FiltersProps = { +/** + * Props for {@link Filters}. + * + * @public + */ +export type FiltersProps = { filters: FiltersState; filterOptions: FilterOptions; resetFilters: () => void; @@ -62,6 +73,9 @@ type FiltersProps = { updateChecked: (filter: string) => void; }; +/** + * @public + */ export const Filters = ({ filters, filterOptions, diff --git a/plugins/search/src/components/Filters/FiltersButton.tsx b/plugins/search/src/components/Filters/FiltersButton.tsx index 1c2829defe..8d896bbf30 100644 --- a/plugins/search/src/components/Filters/FiltersButton.tsx +++ b/plugins/search/src/components/Filters/FiltersButton.tsx @@ -28,11 +28,19 @@ const useStyles = makeStyles(theme => ({ }, })); -type FiltersButtonProps = { +/** + * Props for {@link FiltersButton}. + * + * @public + */ +export type FiltersButtonProps = { numberOfSelectedFilters: number; handleToggleFilters: () => void; }; +/** + * @public + */ export const FiltersButton = ({ numberOfSelectedFilters, handleToggleFilters, diff --git a/plugins/search/src/components/Filters/index.tsx b/plugins/search/src/components/Filters/index.tsx index de890e0eaf..dc4d811dbd 100644 --- a/plugins/search/src/components/Filters/index.tsx +++ b/plugins/search/src/components/Filters/index.tsx @@ -15,5 +15,6 @@ */ export { FiltersButton } from './FiltersButton'; +export type { FiltersButtonProps } from './FiltersButton'; export { Filters } from './Filters'; -export type { FiltersState } from './Filters'; +export type { FilterOptions, FiltersProps, FiltersState } from './Filters'; diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx index bc4b409d57..b4e661b916 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx @@ -37,9 +37,7 @@ export type HomePageSearchBarProps = Partial< >; /** - * The search bar created specifically for the composable home page - * - * @public + * The search bar created specifically for the composable home page. */ export const HomePageSearchBar = ({ ...props }: HomePageSearchBarProps) => { const classes = useStyles(props); diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index cb61089eb6..587478ed46 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -40,11 +40,10 @@ import { } from '@backstage/plugin-search-react'; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * Props for {@link SearchBarBase}. * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchBarBaseProps = Omit & { debounceTime?: number; @@ -55,13 +54,12 @@ export type SearchBarBaseProps = Omit & { }; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * All search boxes exported by the search plugin are based on the , * and this one is based on the component from Material UI. * Recommended if you don't use Search Provider or Search Context. * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export const SearchBarBase = ({ onChange, @@ -153,20 +151,18 @@ export const SearchBarBase = ({ }; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * Props for {@link SearchBar}. * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchBarProps = Partial; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * Recommended search bar when you use the Search Provider or Search Context. * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { const { term, setTerm } = useSearch(); diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx index 1a74decd31..ac37e3bd2c 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -29,9 +29,8 @@ import { } from '@backstage/plugin-search-react'; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { filterSelectedOptions?: boolean; @@ -41,7 +40,6 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { /** * @deprecated Moved to `@backstage/plugin-search-react`. - * */ export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => { const { diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.tsx index b88160f363..e7982ca5c4 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.tsx @@ -24,9 +24,8 @@ import { } from '@backstage/plugin-search-react'; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchFilterComponentProps = { className?: string; @@ -48,9 +47,8 @@ export type SearchFilterComponentProps = { }; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ export type SearchFilterWrapperProps = SearchFilterComponentProps & { component: (props: SearchFilterComponentProps) => ReactElement; @@ -58,6 +56,7 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { }; /** + * @public * @deprecated Moved to `@backstage/plugin-search-react`. */ const SearchFilter = ({ @@ -82,12 +81,12 @@ SearchFilter.Select = ( ) => ; /** - * @deprecated Moved to `@backstage/plugin-search-react`. - * * A control surface for a given filter field name, rendered as an autocomplete * textfield. A hard-coded list of values may be provided, or an async function * which returns values may be provided instead. + * * @public + * @deprecated Moved to `@backstage/plugin-search-react`. */ SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => ( diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index 18776d444d..ebc2979acc 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -50,6 +50,9 @@ export interface SearchModalChildrenProps { toggleModal: () => void; } +/** + * @public + **/ export interface SearchModalProps { /** * If true, it renders the modal. @@ -167,6 +170,9 @@ export const Modal = ({ toggleModal }: SearchModalProps) => { ); }; +/** + * @public + */ export const SearchModal = ({ open = true, hidden, diff --git a/plugins/search/src/components/SearchPage/SearchPage.tsx b/plugins/search/src/components/SearchPage/SearchPage.tsx index 29a22a6f5c..7145c7a5ef 100644 --- a/plugins/search/src/components/SearchPage/SearchPage.tsx +++ b/plugins/search/src/components/SearchPage/SearchPage.tsx @@ -87,6 +87,9 @@ export const UrlUpdater = () => { return null; }; +/** + * @public + */ export const SearchPage = () => { const outlet = useOutlet(); diff --git a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx index 0e8d59a694..f99b6cf29f 100644 --- a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx +++ b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx @@ -30,6 +30,7 @@ const useStyles = makeStyles(theme => ({ })); /** + * @public * @deprecated Moved to `@backstage/plugin-search-react`. */ export const SearchResultPager = () => { diff --git a/plugins/search/src/components/SearchType/SearchType.tsx b/plugins/search/src/components/SearchType/SearchType.tsx index 79c3dda2b9..b20e8b5fba 100644 --- a/plugins/search/src/components/SearchType/SearchType.tsx +++ b/plugins/search/src/components/SearchType/SearchType.tsx @@ -47,6 +47,8 @@ const useStyles = makeStyles(theme => ({ })); /** + * Props for {@link SearchType}. + * * @public */ export type SearchTypeProps = { @@ -56,6 +58,9 @@ export type SearchTypeProps = { defaultValue?: string[] | string | null; }; +/** + * @public + */ const SearchType = (props: SearchTypeProps) => { const { className, defaultValue, name, values = [] } = props; const classes = useStyles(); diff --git a/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx b/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx index 25d41fa10f..36ead2ab15 100644 --- a/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx +++ b/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx @@ -21,10 +21,18 @@ import { rootRouteRef } from '../../plugin'; import { useRouteRef, IconComponent } from '@backstage/core-plugin-api'; import { SidebarSearchField, useContent } from '@backstage/core-components'; +/** + * Props for {@link SidebarSearch}. + * + * @public + */ export type SidebarSearchProps = { icon?: IconComponent; }; +/** + * @public + */ export const SidebarSearch = (props: SidebarSearchProps) => { const searchRoute = useRouteRef(rootRouteRef); const { focusContent } = useContent(); diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx index 191a09395c..fb4afc75fd 100644 --- a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -24,6 +24,11 @@ import { useSearchModal, } from '../SearchModal'; +/** + * Props for {@link SidebarSearchModal}. + * + * @public + */ export type SidebarSearchModalProps = { icon?: IconComponent; children?: (props: SearchModalChildrenProps) => JSX.Element; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index bcb8f40506..fd0e70c61a 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -21,7 +21,12 @@ */ export { Filters, FiltersButton } from './components/Filters'; -export type { FiltersState } from './components/Filters'; +export type { + FilterOptions, + FiltersState, + FiltersProps, + FiltersButtonProps, +} from './components/Filters'; export type { HomePageSearchBarProps } from './components/HomePageComponent'; export { SearchBar, SearchBarBase } from './components/SearchBar'; export type { diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 103d67b81a..19fd501129 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -34,6 +34,9 @@ export const rootRouteRef = createRouteRef({ id: 'search', }); +/** + * @public + */ export const searchPlugin = createPlugin({ id: 'search', apis: [ @@ -50,6 +53,9 @@ export const searchPlugin = createPlugin({ }, }); +/** + * @public + */ export const SearchPage = searchPlugin.provide( createRoutableExtension({ name: 'SearchPage', @@ -59,10 +65,14 @@ export const SearchPage = searchPlugin.provide( ); /** + * @public * @deprecated Import from `@backstage/plugin-search-react` instead. */ export const SearchResult = RealSearchResult; +/** + * @public + */ export const SidebarSearchModal = searchPlugin.provide( createComponentExtension({ name: 'SidebarSearchModal', @@ -76,10 +86,14 @@ export const SidebarSearchModal = searchPlugin.provide( ); /** + * @public * @deprecated Import from `@backstage/plugin-search-react` instead. */ export const DefaultResultListItem = RealDefaultResultListItem; +/** + * @public + */ export const HomePageSearchBar = searchPlugin.provide( createComponentExtension({ name: 'HomePageSearchBar', From 1b1c47f4a774ec9b6f116d6de7e8bfcbffb94f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 12:01:12 +0200 Subject: [PATCH 09/18] update imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- packages/app/src/components/search/SearchModal.tsx | 9 ++++----- packages/app/src/components/search/SearchPage.tsx | 6 +++--- .../packages/app/src/components/search/SearchPage.tsx | 8 ++++---- .../components/HomePageComponent/HomePageSearchBar.tsx | 5 ++++- .../src/components/SearchModal/SearchModal.stories.tsx | 2 +- .../search/src/components/SearchModal/SearchModal.tsx | 4 ++-- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/app/src/components/search/SearchModal.tsx b/packages/app/src/components/search/SearchModal.tsx index 4af5511443..f5f4204302 100644 --- a/packages/app/src/components/search/SearchModal.tsx +++ b/packages/app/src/components/search/SearchModal.tsx @@ -38,16 +38,15 @@ import { catalogApiRef, CATALOG_FILTER_EXISTS, } from '@backstage/plugin-catalog-react'; +import { searchPlugin, SearchType } from '@backstage/plugin-search'; import { DefaultResultListItem, - SearchBar, SearchFilter, - searchPlugin, + SearchBar, SearchResult, SearchResultPager, - SearchType, -} from '@backstage/plugin-search'; -import { useSearch } from '@backstage/plugin-search-react'; + useSearch, +} from '@backstage/plugin-search-react'; import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; const useStyles = makeStyles(theme => ({ diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index ee144b2cde..9a2e85bd8c 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -29,15 +29,15 @@ import { catalogApiRef, CATALOG_FILTER_EXISTS, } from '@backstage/plugin-catalog-react'; +import { SearchType } from '@backstage/plugin-search'; import { DefaultResultListItem, SearchBar, SearchFilter, SearchResult, SearchResultPager, - SearchType, -} from '@backstage/plugin-search'; -import { useSearch } from '@backstage/plugin-search-react'; + useSearch, +} from '@backstage/plugin-search-react'; import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; import React from 'react'; diff --git a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx index d4c7c92346..08166bb308 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx @@ -8,14 +8,14 @@ import { } from '@backstage/plugin-catalog-react'; import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; +import { SearchType } from '@backstage/plugin-search'; import { + DefaultResultListItem, SearchBar, SearchFilter, SearchResult, - SearchType, - DefaultResultListItem, -} from '@backstage/plugin-search'; -import { useSearch } from '@backstage/plugin-search-react'; + useSearch +} from '@backstage/plugin-search-react'; import { CatalogIcon, Content, diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx index b4e661b916..6e9787c2e0 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx @@ -16,7 +16,10 @@ import React, { useCallback, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import { SearchBarBase, SearchBarBaseProps } from '../SearchBar'; +import { + SearchBarBase, + SearchBarBaseProps, +} from '@backstage/plugin-search-react'; import { useNavigateToQuery } from '../util'; const useStyles = makeStyles({ diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index 870f73e472..aad018c334 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -34,10 +34,10 @@ import { MockSearchApi, SearchContextProvider, SearchResult, + SearchResultPager, } from '@backstage/plugin-search-react'; import { TestApiProvider } from '@backstage/test-utils'; import { SearchModal } from './SearchModal'; -import { SearchResultPager } from '../SearchResultPager'; import { SearchType } from '../SearchType'; import { useSearchModal } from './useSearchModal'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index ebc2979acc..6c37f20001 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -28,14 +28,14 @@ import { } from '@material-ui/core'; import LaunchIcon from '@material-ui/icons/Launch'; import { makeStyles } from '@material-ui/core/styles'; -import { SearchBar } from '../SearchBar'; import { DefaultResultListItem, SearchContextProvider, + SearchBar, SearchResult, + SearchResultPager, useSearch, } from '@backstage/plugin-search-react'; -import { SearchResultPager } from '../SearchResultPager'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Link, useContent } from '@backstage/core-components'; import { rootRouteRef } from '../../plugin'; From f28bc288341a79f620149475e998df69da7a739a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 13:09:59 +0200 Subject: [PATCH 10/18] update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- docs/assets/search/architecture.drawio.svg | 148 ++++++++++++++++----- docs/features/search/getting-started.md | 8 +- plugins/search/README.md | 13 +- 3 files changed, 127 insertions(+), 42 deletions(-) diff --git a/docs/assets/search/architecture.drawio.svg b/docs/assets/search/architecture.drawio.svg index 709a008b43..5c7c98ebbf 100644 --- a/docs/assets/search/architecture.drawio.svg +++ b/docs/assets/search/architecture.drawio.svg @@ -1,17 +1,36 @@ - + + + + + +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
- + -
+
@@ -21,7 +40,7 @@
- + @@ -103,11 +122,11 @@ - + -
+
@backstage/plugin-xyz @@ -115,7 +134,7 @@
- + @backstage/plugin-xyz @@ -251,10 +270,7 @@ - - - - + @@ -274,11 +290,14 @@ - + + + + -
+
@@ -288,18 +307,18 @@
- + Search API - - + + -
+
@@ -309,7 +328,7 @@
- + Components @@ -824,11 +843,11 @@ - + -
+
@@ -844,16 +863,19 @@
- + Individual frontend... - + + + + -
+
@@ -863,20 +885,80 @@
- + Search Cont... - - - - - - - - - + + + + + + + + + + + + + +
+
+
+ @backstage/plugin-search-react +
+
+
+
+ + @backstage/plugin-search-react + +
+
+ + + + + + + + +
+
+
+ + Components + +
+
+
+
+ + Components + +
+
+ + + + + +
+
+
+ + Search Client + +
+
+
+
+ + Search Client + +
+
diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 14ad523740..d0e13eda55 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -18,7 +18,7 @@ If you haven't setup Backstage already, start ```bash # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-search +yarn add --cwd packages/app @backstage/plugin-search @backstage/plugin-search-react ``` Create a new `packages/app/src/components/search/SearchPage.tsx` file in your @@ -33,7 +33,7 @@ import { SearchResult, DefaultResultListItem, SearchFilter, -} from '@backstage/plugin-search'; +} from '@backstage/plugin-search-react'; import { CatalogResultListItem } from '@backstage/plugin-catalog'; export const searchPage = ( @@ -213,7 +213,7 @@ apiRouter.use('/search', await search(searchEnv)); ### Frontend -The Search Plugin exposes several default filter types as static properties, +The Search Plugin web library (`@backstage/plugin-search-react`) exposes several default filter types as static properties, including `` and ``. These allow you to provide values relevant to your Backstage instance that, when selected, get passed to the backend. @@ -237,7 +237,7 @@ If you have advanced filter needs, you can specify your own filter component like this (although new core filter contributions are welcome): ```tsx -import { useSearch, SearchFilter } from '@backstage/plugin-search'; +import { useSearch, SearchFilter } from '@backstage/plugin-search-react'; const MyCustomFilter = () => { // Note: filters contain filter data from other filter components. Be sure diff --git a/plugins/search/README.md b/plugins/search/README.md index f4a32c9597..3803b71e43 100644 --- a/plugins/search/README.md +++ b/plugins/search/README.md @@ -13,14 +13,17 @@ Run `yarn dev` in the root directory, and then navigate to [/search](http://loca This search plugin is primarily responsible for the following: - Providing a `` routable extension. -- Exposing various search-related components (like ``, - ``, etc), which can be composed by a Backstage App or by +- Exposing various search-related components (like ``, + ``, etc), which can be composed by a Backstage App or by other Backstage Plugins to power search experiences of all kinds. -- Exposing a ``, which manages search state and API - communication with the Backstage backend. -Don't forget, a lot of functionality is available in backend plugins: +Don't forget, a lot of functionality is available in web libraries and backend plugins: +- `@backstage/plugin-search-react`, which is responsible for: + - Exposing a ``, which manages search state and API + communication with the Backstage backend. + - Exposing the `SearchApi` and its corresponding ref. + - Exposing reusable components, such as `` and ``, etc. - `@backstage/plugin-search-backend-node`, which is responsible for the search index management - `@backstage/plugin-search-backend`, which is responsible for query processing From 88091591482ddc17044d0a84700acb7457a72a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 13:31:00 +0200 Subject: [PATCH 11/18] changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .changeset/famous-walls-visit.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/famous-walls-visit.md diff --git a/.changeset/famous-walls-visit.md b/.changeset/famous-walls-visit.md new file mode 100644 index 0000000000..8fcaed4ab0 --- /dev/null +++ b/.changeset/famous-walls-visit.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search': minor +'@backstage/plugin-search-react': minor +--- + +Components ``, `` (including ``), `` (including `.Checkbox`, `.Select`, and `.Autocomplete` static prop components), ``, ``, and `` are now exported from `@backstage/plugin-search-react`. They are now deprecated in `@backstage/plugin-search` and will be removed in a future release. From 8c2b28708ffeeeed041073407f08764d441e6767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 13:58:06 +0200 Subject: [PATCH 12/18] update api-report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- plugins/search/api-report.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index c2f1e93bff..9e418dd08e 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -13,6 +13,7 @@ import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SearchAutocompleteFilterProps as SearchAutocompleteFilterProps_2 } from '@backstage/plugin-search-react'; +import { SearchBarBaseProps as SearchBarBaseProps_2 } from '@backstage/plugin-search-react'; import { SearchFilterComponentProps as SearchFilterComponentProps_2 } from '@backstage/plugin-search-react'; import { SearchResultProps } from '@backstage/plugin-search-react'; @@ -66,11 +67,11 @@ export type FiltersState = { // @public (undocumented) export const HomePageSearchBar: ({ ...props -}: Partial>) => JSX.Element; +}: Partial>) => JSX.Element; // @public export type HomePageSearchBarProps = Partial< - Omit + Omit >; // @public (undocumented) From 93089a9d063cc2c497904883e34a89e0a0009321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Fri, 3 Jun 2022 15:29:29 +0200 Subject: [PATCH 13/18] prettier? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .../packages/app/src/components/search/SearchPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx index 08166bb308..1b3bf5b77d 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx @@ -14,7 +14,7 @@ import { SearchBar, SearchFilter, SearchResult, - useSearch + useSearch, } from '@backstage/plugin-search-react'; import { CatalogIcon, From 30f04d14972fa3375a0675cc3be287d21ebc39fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Tue, 7 Jun 2022 10:16:46 +0200 Subject: [PATCH 14/18] changeset for create-app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .changeset/red-apes-tell.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .changeset/red-apes-tell.md diff --git a/.changeset/red-apes-tell.md b/.changeset/red-apes-tell.md new file mode 100644 index 0000000000..61c4a5077e --- /dev/null +++ b/.changeset/red-apes-tell.md @@ -0,0 +1,24 @@ +--- +'@backstage/create-app': patch +--- + +Components ``, ``, ``, and `` are now deprecated in `@backstage/plugin-search` and should be imported from `@backstage/plugin-search-react` instead. + +To upgrade your App, update the following in `packages/app/src/components/search/SearchPage.tsx`: + +```diff +import { +- SearchBar +- SearchFilter +- SearchResult +SearchType, +- DefaultResultListItem +} from `@backstage/plugin-search`; +import { ++ DefaultResultListItem ++ SearchBar ++ SearchFilter ++ SearchResult +useSearch, +} from `@backstage/plugin-search-react`; +``` From a33399ed59fc863bb789658cdd100bd2266dc1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Tue, 7 Jun 2022 10:31:01 +0200 Subject: [PATCH 15/18] fixup to arch diagram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- docs/assets/search/architecture.drawio.svg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/assets/search/architecture.drawio.svg b/docs/assets/search/architecture.drawio.svg index 5c7c98ebbf..b85541b804 100644 --- a/docs/assets/search/architecture.drawio.svg +++ b/docs/assets/search/architecture.drawio.svg @@ -1,4 +1,4 @@ - + @@ -939,7 +939,6 @@
- From 295eb30778b7892197050f8409001c3d836664e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Tue, 7 Jun 2022 16:28:08 +0200 Subject: [PATCH 16/18] remove unnecessary deprecation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .../SearchFilter.Autocomplete.tsx | 100 +----------------- 1 file changed, 2 insertions(+), 98 deletions(-) diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx index ac37e3bd2c..3db5112b88 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -14,110 +14,14 @@ * limitations under the License. */ -import React, { ChangeEvent, useState } from 'react'; -import { Chip, TextField } from '@material-ui/core'; -import { - Autocomplete, - AutocompleteGetTagProps, - AutocompleteRenderInputParams, -} from '@material-ui/lab'; -import { - SearchFilterComponentProps, - useSearch, - useAsyncFilterValues, - useDefaultFilterValue, -} from '@backstage/plugin-search-react'; +import { SearchFilterComponentProps } from '@backstage/plugin-search-react'; /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { filterSelectedOptions?: boolean; limitTags?: number; multiple?: boolean; }; - -/** - * @deprecated Moved to `@backstage/plugin-search-react`. - */ -export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => { - const { - className, - defaultValue, - name, - values: givenValues, - valuesDebounceMs, - label, - filterSelectedOptions, - limitTags, - multiple, - } = props; - const [inputValue, setInputValue] = useState(''); - useDefaultFilterValue(name, defaultValue); - const asyncValues = - typeof givenValues === 'function' ? givenValues : undefined; - const defaultValues = - typeof givenValues === 'function' ? undefined : givenValues; - const { value: values, loading } = useAsyncFilterValues( - asyncValues, - inputValue, - defaultValues, - valuesDebounceMs, - ); - const { filters, setFilters } = useSearch(); - const filterValue = - (filters[name] as string | string[] | undefined) || (multiple ? [] : null); - - // Set new filter values on input change. - const handleChange = ( - _: ChangeEvent<{}>, - newValue: string | string[] | null, - ) => { - setFilters(prevState => { - const { [name]: filter, ...others } = prevState; - - if (newValue) { - return { ...others, [name]: newValue }; - } - return { ...others }; - }); - }; - - // Provide the input field. - const renderInput = (params: AutocompleteRenderInputParams) => ( - - ); - - // Render tags as primary-colored chips. - const renderTags = ( - tagValue: string[], - getTagProps: AutocompleteGetTagProps, - ) => - tagValue.map((option: string, index: number) => ( - - )); - - return ( - setInputValue(newValue)} - renderInput={renderInput} - renderTags={renderTags} - /> - ); -}; From 2dc4818541f83cabb855d3d160e6691fff1a2f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20N=C3=A4sman?= Date: Tue, 7 Jun 2022 16:30:04 +0200 Subject: [PATCH 17/18] apply review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders Näsman --- .changeset/famous-walls-visit.md | 6 +- .changeset/red-apes-tell.md | 19 +-- .changeset/strange-flies-march.md | 5 + plugins/search-react/api-report.md | 7 - .../DefaultResultListItem.tsx | 2 - .../components/SearchResult/SearchResult.tsx | 2 - .../SearchTracker/SearchTracker.tsx | 2 - plugins/search-react/src/components/index.ts | 1 - plugins/search/api-report.md | 2 +- .../src/components/SearchBar/SearchBar.tsx | 121 ++---------------- .../components/SearchFilter/SearchFilter.tsx | 12 +- .../SearchResultPager/SearchResultPager.tsx | 2 +- scripts/api-extractor.ts | 1 + 13 files changed, 32 insertions(+), 150 deletions(-) create mode 100644 .changeset/strange-flies-march.md diff --git a/.changeset/famous-walls-visit.md b/.changeset/famous-walls-visit.md index 8fcaed4ab0..b57702a392 100644 --- a/.changeset/famous-walls-visit.md +++ b/.changeset/famous-walls-visit.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-search': minor -'@backstage/plugin-search-react': minor +'@backstage/plugin-search': patch +'@backstage/plugin-search-react': patch --- -Components ``, `` (including ``), `` (including `.Checkbox`, `.Select`, and `.Autocomplete` static prop components), ``, ``, and `` are now exported from `@backstage/plugin-search-react`. They are now deprecated in `@backstage/plugin-search` and will be removed in a future release. +Components ``, `` (including ``), `` (including `.Checkbox`, `.Select`, and `.Autocomplete` static prop components), ``, and `` are now exported from `@backstage/plugin-search-react`. They are now deprecated in `@backstage/plugin-search` and will be removed in a future release. diff --git a/.changeset/red-apes-tell.md b/.changeset/red-apes-tell.md index 61c4a5077e..ee0ace6a4c 100644 --- a/.changeset/red-apes-tell.md +++ b/.changeset/red-apes-tell.md @@ -8,17 +8,10 @@ To upgrade your App, update the following in `packages/app/src/components/search ```diff import { -- SearchBar -- SearchFilter -- SearchResult -SearchType, -- DefaultResultListItem -} from `@backstage/plugin-search`; -import { -+ DefaultResultListItem -+ SearchBar -+ SearchFilter -+ SearchResult -useSearch, -} from `@backstage/plugin-search-react`; + DefaultResultListItem + SearchBar + SearchFilter + SearchResult +- } from `@backstage/plugin-search`; ++ } from `@backstage/plugin-search-react`; ``` diff --git a/.changeset/strange-flies-march.md b/.changeset/strange-flies-march.md new file mode 100644 index 0000000000..70a15b41d9 --- /dev/null +++ b/.changeset/strange-flies-march.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': minor +--- + +The pre-alpha ``, ``, `etc...` components have been removed. In the unlikely event you were still using/referencing them, please update to using their non-`*Next` equivalents from either `@backstage/plugin-search-react` or `@backstage/plugin-search`. diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 93ed657d85..ce1fa53bb4 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -187,13 +187,6 @@ export type SearchResultProps = { // @public (undocumented) export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; -// @public -export const TrackSearch: ({ - children, -}: { - children: React_2.ReactChild; -}) => JSX.Element; - // @public export const useAsyncFilterValues: ( fn: ((partial: string) => Promise) | undefined, diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx index 48f12213e4..ff6f3d7fe1 100644 --- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -101,8 +101,6 @@ export const DefaultResultListItemComponent = ({ }; /** - * A higher order function providing AnalyticsContext to the DefaultResultListItemComponent. - * * @public */ const HigherOrderDefaultResultListItem = ( diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx index 54024d546a..46a2dde305 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -65,8 +65,6 @@ export const SearchResultComponent = ({ children }: SearchResultProps) => { }; /** - * A higher order function providing AnalyticsContext to the SearchResultComponent. - * * @public */ const HigherOrderSearchResult = (props: SearchResultProps) => { diff --git a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx index 8ca7373298..f900e45d45 100644 --- a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx +++ b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx @@ -20,8 +20,6 @@ import { useSearch } from '../../context'; /** * Capture search event on term change. - * - * @public */ export const TrackSearch = ({ children }: { children: React.ReactChild }) => { const analytics = useAnalytics(); diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 155a4c3b12..263ca4ad59 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -19,5 +19,4 @@ export * from './SearchFilter'; export * from './SearchResult'; export * from './SearchResultPager'; export * from './SearchBar'; -export * from './SearchTracker'; export * from './DefaultResultListItem'; diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 9e418dd08e..85d4b783e5 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -99,7 +99,7 @@ export const SearchBarBase: ({ inputProps: defaultInputProps, endAdornment: defaultEndAdornment, ...props -}: SearchBarBaseProps) => JSX.Element; +}: SearchBarBaseProps_2) => JSX.Element; // @public @deprecated export type SearchBarBaseProps = Omit & { diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index 587478ed46..a854869976 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -14,36 +14,20 @@ * limitations under the License. */ -import React, { - ChangeEvent, - KeyboardEvent, - useState, - useEffect, - useCallback, -} from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { - InputBase, - InputBaseProps, - InputAdornment, - IconButton, -} from '@material-ui/core'; -import SearchIcon from '@material-ui/icons/Search'; -import ClearButton from '@material-ui/icons/Clear'; +import React, { useCallback } from 'react'; + +import { InputBaseProps } from '@material-ui/core'; import { - SearchContextProvider, + SearchBarBase as RealSearchBarBase, useSearch, - useSearchContextCheck, - TrackSearch, } from '@backstage/plugin-search-react'; /** * Props for {@link SearchBarBase}. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchBarBaseProps = Omit & { debounceTime?: number; @@ -59,102 +43,15 @@ export type SearchBarBaseProps = Omit & { * Recommended if you don't use Search Provider or Search Context. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ -export const SearchBarBase = ({ - onChange, - onKeyDown, - onSubmit, - debounceTime = 200, - clearButton = true, - fullWidth = true, - value: defaultValue, - inputProps: defaultInputProps = {}, - endAdornment: defaultEndAdornment, - ...props -}: SearchBarBaseProps) => { - const configApi = useApi(configApiRef); - const [value, setValue] = useState(defaultValue as string); - const hasSearchContext = useSearchContextCheck(); - - useEffect(() => { - setValue(prevValue => - prevValue !== defaultValue ? (defaultValue as string) : prevValue, - ); - }, [defaultValue]); - - useDebounce(() => onChange(value), debounceTime, [value]); - - const handleChange = useCallback( - (e: ChangeEvent) => { - setValue(e.target.value); - }, - [setValue], - ); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (onKeyDown) onKeyDown(e); - if (onSubmit && e.key === 'Enter') { - onSubmit(); - } - }, - [onKeyDown, onSubmit], - ); - - const handleClear = useCallback(() => { - onChange(''); - }, [onChange]); - - const placeholder = `Search in ${ - configApi.getOptionalString('app.title') || 'Backstage' - }`; - - const startAdornment = ( - - - - - - ); - - const endAdornment = ( - - - - - - ); - - const searchBar = ( - - - - ); - - return hasSearchContext ? ( - searchBar - ) : ( - {searchBar} - ); -}; +export const SearchBarBase = RealSearchBarBase; /** * Props for {@link SearchBar}. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchBarProps = Partial; @@ -162,7 +59,7 @@ export type SearchBarProps = Partial; * Recommended search bar when you use the Search Provider or Search Context. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { const { term, setTerm } = useSearch(); diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.tsx index e7982ca5c4..e58bbd5c14 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.tsx @@ -25,7 +25,7 @@ import { /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchFilterComponentProps = { className?: string; @@ -48,7 +48,7 @@ export type SearchFilterComponentProps = { /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export type SearchFilterWrapperProps = SearchFilterComponentProps & { component: (props: SearchFilterComponentProps) => ReactElement; @@ -57,7 +57,7 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ const SearchFilter = ({ component: Element, @@ -65,7 +65,7 @@ const SearchFilter = ({ }: SearchFilterWrapperProps) => ; /** - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ SearchFilter.Checkbox = ( props: Omit & @@ -73,7 +73,7 @@ SearchFilter.Checkbox = ( ) => ; /** - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ SearchFilter.Select = ( props: Omit & @@ -86,7 +86,7 @@ SearchFilter.Select = ( * which returns values may be provided instead. * * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => ( diff --git a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx index f99b6cf29f..287e485dd1 100644 --- a/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx +++ b/plugins/search/src/components/SearchResultPager/SearchResultPager.tsx @@ -31,7 +31,7 @@ const useStyles = makeStyles(theme => ({ /** * @public - * @deprecated Moved to `@backstage/plugin-search-react`. + * @deprecated Import from `@backstage/plugin-search-react` instead. */ export const SearchResultPager = () => { const { fetchNextPage, fetchPreviousPage } = useSearch(); diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 15e43fb02d..619dafabd4 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -256,6 +256,7 @@ const NO_WARNING_PACKAGES = [ 'plugins/scaffolder-backend-module-rails', 'plugins/scaffolder-backend-module-yeoman', 'plugins/scaffolder-common', + 'plugins/search', 'plugins/search-backend', 'plugins/search-backend-node', 'plugins/search-backend-module-elasticsearch', From effb3b27a40931ea317b0fe2f426d6e4799563d7 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 10 Jun 2022 11:05:41 +0200 Subject: [PATCH 18/18] Further review feedback. Signed-off-by: Eric Peterson --- plugins/search-react/api-report.md | 37 +------------------ .../components/SearchBar/SearchBar.test.tsx | 8 ++-- .../src/components/SearchBar/SearchBar.tsx | 14 ++++++- .../src/components/SearchFilter/index.ts | 1 - plugins/search/api-report.md | 12 +++--- .../search/src/components/Filters/Filters.tsx | 8 ++++ .../src/components/Filters/FiltersButton.tsx | 3 ++ 7 files changed, 35 insertions(+), 48 deletions(-) diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index ce1fa53bb4..f9bb993fd6 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -27,7 +27,7 @@ export const AutocompleteFilter: ( // @public (undocumented) export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; -// @public +// @public (undocumented) export const DefaultResultListItem: ( props: DefaultResultListItemProps, ) => JSX.Element; @@ -168,7 +168,7 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; -// @public +// @public (undocumented) export const SearchResult: (props: SearchResultProps) => JSX.Element; // @public @@ -187,39 +187,6 @@ export type SearchResultProps = { // @public (undocumented) export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element; -// @public -export const useAsyncFilterValues: ( - fn: ((partial: string) => Promise) | undefined, - inputValue: string, - defaultValues?: string[], - debounce?: number, -) => - | { - loading: boolean; - error?: undefined; - value?: undefined; - } - | { - loading: false; - error: Error; - value?: undefined; - } - | { - loading: true; - error?: Error | undefined; - value?: string[] | undefined; - } - | { - loading: boolean; - value: string[]; - }; - -// @public -export const useDefaultFilterValue: ( - name: string, - defaultValue?: string | string[] | null | undefined, -) => void; - // @public export const useSearch: () => SearchContextValue; diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx index 4e2c4d7a5c..d65278f7ec 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -317,8 +317,8 @@ describe('SearchBar', () => { expect(analyticsApiSpy.getEvents()[0]).toEqual({ action: 'search', context: { - extension: 'App', - pluginId: 'root', + extension: 'SearchBar', + pluginId: 'search', routeRef: 'unknown', searchTypes: 'software-catalog,techdocs', }, @@ -340,8 +340,8 @@ describe('SearchBar', () => { expect(analyticsApiSpy.getEvents()[1]).toEqual({ action: 'search', context: { - extension: 'App', - pluginId: 'root', + extension: 'SearchBar', + pluginId: 'search', routeRef: 'unknown', searchTypes: 'software-catalog,techdocs', }, diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.tsx index b3c8ec9fa5..d3d25455ce 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.tsx @@ -31,7 +31,11 @@ import { import SearchIcon from '@material-ui/icons/Search'; import ClearButton from '@material-ui/icons/Clear'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { + AnalyticsContext, + configApiRef, + useApi, +} from '@backstage/core-plugin-api'; import { SearchContextProvider, @@ -175,5 +179,11 @@ export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { [onChange, setTerm], ); - return ; + return ( + + + + ); }; diff --git a/plugins/search-react/src/components/SearchFilter/index.ts b/plugins/search-react/src/components/SearchFilter/index.ts index e159cff93d..9f92c7c8e2 100644 --- a/plugins/search-react/src/components/SearchFilter/index.ts +++ b/plugins/search-react/src/components/SearchFilter/index.ts @@ -16,7 +16,6 @@ export { CheckboxFilter, SearchFilter, SelectFilter } from './SearchFilter'; export { AutocompleteFilter } from './SearchFilter.Autocomplete'; -export { useAsyncFilterValues, useDefaultFilterValue } from './hooks'; export type { SearchFilterComponentProps, SearchFilterWrapperProps, diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 85d4b783e5..800fa7e7fc 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -22,13 +22,13 @@ export const DefaultResultListItem: ( props: DefaultResultListItemProps, ) => JSX.Element; -// @public (undocumented) +// @public @deprecated (undocumented) export type FilterOptions = { kind: Array; lifecycle: Array; }; -// @public (undocumented) +// @public @deprecated (undocumented) export const Filters: ({ filters, filterOptions, @@ -37,19 +37,19 @@ export const Filters: ({ updateChecked, }: FiltersProps) => JSX.Element; -// @public (undocumented) +// @public @deprecated (undocumented) export const FiltersButton: ({ numberOfSelectedFilters, handleToggleFilters, }: FiltersButtonProps) => JSX.Element; -// @public +// @public @deprecated export type FiltersButtonProps = { numberOfSelectedFilters: number; handleToggleFilters: () => void; }; -// @public +// @public @deprecated export type FiltersProps = { filters: FiltersState; filterOptions: FilterOptions; @@ -58,7 +58,7 @@ export type FiltersProps = { updateChecked: (filter: string) => void; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type FiltersState = { selected: string; checked: Array; diff --git a/plugins/search/src/components/Filters/Filters.tsx b/plugins/search/src/components/Filters/Filters.tsx index 9bbe90d32f..9170961e18 100644 --- a/plugins/search/src/components/Filters/Filters.tsx +++ b/plugins/search/src/components/Filters/Filters.tsx @@ -46,6 +46,8 @@ const useStyles = makeStyles(theme => ({ /** * @public + * @deprecated This type and corresponding component will be removed in a + * future release. */ export type FiltersState = { selected: string; @@ -54,6 +56,8 @@ export type FiltersState = { /** * @public + * @deprecated This type and corresponding component will be removed in a + * future release. */ export type FilterOptions = { kind: Array; @@ -64,6 +68,8 @@ export type FilterOptions = { * Props for {@link Filters}. * * @public + * @deprecated This type and corresponding component will be removed in a + * future release. */ export type FiltersProps = { filters: FiltersState; @@ -75,6 +81,8 @@ export type FiltersProps = { /** * @public + * @deprecated This component will be removed in a future release. Use + * `SearchFilter` from `@backstage/plugin-search-react` instead. */ export const Filters = ({ filters, diff --git a/plugins/search/src/components/Filters/FiltersButton.tsx b/plugins/search/src/components/Filters/FiltersButton.tsx index 8d896bbf30..890c787689 100644 --- a/plugins/search/src/components/Filters/FiltersButton.tsx +++ b/plugins/search/src/components/Filters/FiltersButton.tsx @@ -32,6 +32,8 @@ const useStyles = makeStyles(theme => ({ * Props for {@link FiltersButton}. * * @public + * @deprecated This type and corresponding component will be removed in a + * future release. */ export type FiltersButtonProps = { numberOfSelectedFilters: number; @@ -40,6 +42,7 @@ export type FiltersButtonProps = { /** * @public + * @deprecated See `SearchFilter` in `@backstage/plugin-search-react` instead. */ export const FiltersButton = ({ numberOfSelectedFilters,