diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx new file mode 100644 index 0000000000..e8e62b1929 --- /dev/null +++ b/plugins/search/src/components/SearchFilter/SearchFilter.Autocomplete.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ChangeEvent, useState } from 'react'; +import { Chip, TextField } from '@material-ui/core'; +import { + Autocomplete, + AutocompleteGetTagProps, + AutocompleteRenderInputParams, +} from '@material-ui/lab'; +import { useSearch } from '../SearchContext'; +import { useAsyncFilterValues } from './hooks'; +import { SearchFilterComponentProps } from './SearchFilter'; + +/** + * @public + */ +export type SearchAutocompleteFilterProps = SearchFilterComponentProps & { + multiple?: boolean; + limitTags?: number; +}; + +export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => { + const { + asyncValues, + asyncDebounce, + className, + multiple, + name, + values: givenValues, + label, + limitTags, + } = props; + const [inputValue, setInputValue] = useState(''); + const { value: values, loading } = useAsyncFilterValues( + asyncValues, + inputValue, + givenValues, + asyncDebounce, + ); + const { setFilters } = useSearch(); + + // 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/src/components/SearchFilter/SearchFilter.stories.tsx index dbedff8c63..e2a73a42bc 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx @@ -72,3 +72,49 @@ export const AsyncSelectFilter = () => { ); }; + +export const Autocomplete = () => { + return ( + + + + ); +}; + +export const MultiSelectAutocomplete = () => { + return ( + + + + ); +}; + +export const AsyncMultiSelectAutocomplete = () => { + return ( + + { + if (partial === '') return []; + const response = await fetch( + `https://swapi.dev/api/people?search=${partial}`, + ); + const json: { results: Array<{ name: string }> } = + await response.json(); + return json.results.map(r => r.name); + }} + /> + + ); +}; diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.tsx index 93d0bd2fd7..23761ee4a9 100644 --- a/plugins/search/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search/src/components/SearchFilter/SearchFilter.tsx @@ -26,6 +26,10 @@ import { FormLabel, } from '@material-ui/core'; +import { + AutocompleteFilter, + SearchAutocompleteFilterProps, +} from './SearchFilter.Autocomplete'; import { useSearch } from '../SearchContext'; import { useAsyncFilterValues } from './hooks'; @@ -196,6 +200,16 @@ SearchFilter.Select = ( 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) => ( + +); + /** * @deprecated This component was used for rapid prototyping of the Backstage * Search platform. Now that the API has stabilized, you should use the diff --git a/plugins/search/src/components/SearchFilter/index.ts b/plugins/search/src/components/SearchFilter/index.ts index af5fca182c..86cd66ff6b 100644 --- a/plugins/search/src/components/SearchFilter/index.ts +++ b/plugins/search/src/components/SearchFilter/index.ts @@ -19,3 +19,4 @@ export type { SearchFilterComponentProps, SearchFilterWrapperProps, } from './SearchFilter'; +export type { SearchAutocompleteFilterProps } from './SearchFilter.Autocomplete'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 7a63ccd471..99c3f5177b 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -34,6 +34,7 @@ export type { export { SearchContextProvider, useSearch } from './components/SearchContext'; export { SearchFilter, SearchFilterNext } from './components/SearchFilter'; export type { + SearchAutocompleteFilterProps, SearchFilterComponentProps, SearchFilterWrapperProps, } from './components/SearchFilter';