From c8284149c2591c8d8f38fdba11a34c6663cd0739 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 24 May 2021 12:35:07 +0200 Subject: [PATCH] composability refactoring Signed-off-by: Emma Indal Signed-off-by: Eric Peterson --- .../app/src/components/search/SearchPage.tsx | 60 ++---- .../SearchBarNext/SearchBarNext.tsx | 6 +- .../SearchContext/SearchContext.tsx | 7 +- .../SearchFilterNext/SearchFilterNext.tsx | 176 ++++++++++++++++++ .../src/components/SearchFilterNext/index.ts | 17 ++ .../SearchPageNext/SearchPageNext.tsx | 18 +- plugins/search/src/components/index.tsx | 1 + plugins/search/src/index.ts | 2 +- plugins/search/src/plugin.ts | 9 - 9 files changed, 234 insertions(+), 62 deletions(-) create mode 100644 plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx create mode 100644 plugins/search/src/components/SearchFilterNext/index.ts diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index d0f1a93e81..8480edf514 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -16,31 +16,15 @@ import React from 'react'; import { Content, Header, Lifecycle, Page } from '@backstage/core'; -import { Grid, List } from '@material-ui/core'; +import { Grid, List, Card, CardContent } from '@material-ui/core'; import { SearchBarNext, SearchResultNext, DefaultResultListItem, - SearchFiltersNext, - FilterType, - CheckBoxFilter, - SelectFilter, + SearchFilterNext, } from '@backstage/plugin-search'; import { CatalogResultListItem } from '@backstage/plugin-catalog'; -const filterDefinitions = [ - { - field: 'kind', - type: FilterType.SELECT, - values: ['Component', 'Template'], - }, - { - field: 'lifecycle', - type: FilterType.CHECKBOX, - values: ['experimental', 'production'], - }, -]; - export const searchPage = (
} /> @@ -50,32 +34,20 @@ export const searchPage = ( - - <> - {filterDefinitions.map(definition => { - switch (definition.type) { - case 'checkbox': - return ( - - ); - case 'select': - return ( - - ); - default: - return null; - } - })} - - + + + + + + + + diff --git a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx index bec296c42a..a36393a376 100644 --- a/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx +++ b/plugins/search/src/components/SearchBarNext/SearchBarNext.tsx @@ -36,11 +36,7 @@ export const SearchBarNext = () => { const classes = useStyles(); const { term, setTerm, setPageCursor } = useSearch(); - const [queryString, setQueryString] = useQueryParamState('query'); - - useEffect(() => { - setTerm(queryString ?? ''); - }, [queryString, setTerm]); + const [, setQueryString] = useQueryParamState('query'); useDebounce( () => { diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index b0bcff1f25..ac264ccf32 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -39,6 +39,11 @@ type SearchContextValue = { setPageCursor: React.Dispatch>; }; +type SettableSearchContext = Omit< + SearchContextValue, + 'result' | 'setTerm' | 'setTypes' | 'setFilters' | 'setPageCursor' +>; + const SearchContext = createContext({} as SearchContextValue); export const SearchContextProvider = ({ @@ -49,7 +54,7 @@ export const SearchContextProvider = ({ types: ['*'], }, children, -}: PropsWithChildren<{ initialState?: any }>) => { +}: PropsWithChildren<{ initialState?: SettableSearchContext }>) => { const searchApi = useApi(searchApiRef); const [pageCursor, setPageCursor] = useState(initialState.pageCursor); const [filters, setFilters] = useState(initialState.filters); diff --git a/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx new file mode 100644 index 0000000000..02b625bd42 --- /dev/null +++ b/plugins/search/src/components/SearchFilterNext/SearchFilterNext.tsx @@ -0,0 +1,176 @@ +/* + * Copyright 2021 Spotify AB + * + * 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, useEffect } from 'react'; +import { + makeStyles, + FormControl, + FormControlLabel, + InputLabel, + Checkbox, + Select, + MenuItem, + FormLabel, +} from '@material-ui/core'; + +import { useSearch } from '../SearchContext'; + +const useStyles = makeStyles({ + select: { + width: '100%', + }, + subtitle: { + textTransform: 'capitalize', + }, +}); + +export type Component = Omit; + +export type Props = { + component: (props: Component) => ReactElement; + debug?: boolean; + name: string; + values?: string[]; + defaultValue?: string | null; +}; + +/** + * @param defaultValue - The default value. If more than one value should be defaulted to "checked," pass a comma-separated string. + */ +const CheckboxFilter = ({ name, defaultValue, values }: Component) => { + const { filters, setFilters } = useSearch(); + + const setCheckboxFilter = (filter: string) => { + const newFilters = filters; + const currentValues = newFilters[name] as string[]; + + if (!filter) return; + + if (!currentValues) { + setFilters({ ...filters, [name]: [filter] }); + } else if (!currentValues?.includes(filter)) { + setFilters({ + ...filters, + [name]: [...currentValues, filter], + }); + } else { + const filterToDelete = currentValues.find(value => value === filter); + if (filterToDelete) { + currentValues.splice(currentValues.indexOf(filterToDelete), 1); + + setFilters({ ...filters, [name]: currentValues }); + } + } + }; + + useEffect(() => { + if (defaultValue) { + setFilters(prevFilters => ({ + ...prevFilters, + [name]: defaultValue.split(','), + })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultValue, setFilters]); + + return ( + + {name} + {values && + values.map((v: string) => ( + setCheckboxFilter(v)} + checked={ + filters[name] + ? (filters[name] as string[]).includes(v) + : false + } + /> + } + label={v} + /> + ))} + + ); +}; + +const SelectFilter = ({ name, defaultValue, values }: Component) => { + const classes = useStyles(); + const { filters, setFilters } = useSearch(); + + const setSelectFilter = (filter: string) => { + const newFilters = filters; + if (newFilters[name] && filter === '') { + delete newFilters[name]; + setFilters({ newFilters }); + } else { + setFilters({ ...filters, [name]: filter as string }); + } + }; + + useEffect( + () => { + if (defaultValue) { + setFilters(prevFilters => ({ + ...prevFilters, + [name]: defaultValue, + })); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [setFilters, defaultValue], + ); + + return ( + + {name} + + + ); +}; + +const SearchFilterNext = ({ component: Element, ...props }: Props) => { + return ; +}; + +SearchFilterNext.Checkbox = (props: Omit) => ( + +); +SearchFilterNext.Select = (props: Omit) => ( + +); + +export { SearchFilterNext }; diff --git a/plugins/search/src/components/SearchFilterNext/index.ts b/plugins/search/src/components/SearchFilterNext/index.ts new file mode 100644 index 0000000000..322abe64d7 --- /dev/null +++ b/plugins/search/src/components/SearchFilterNext/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { SearchFilterNext } from './SearchFilterNext'; diff --git a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx index e86c887f59..f3a4efc32c 100644 --- a/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx +++ b/plugins/search/src/components/SearchPageNext/SearchPageNext.tsx @@ -15,12 +15,26 @@ */ import React from 'react'; -import { Outlet } from 'react-router'; +import qs from 'qs'; +import { Outlet, useLocation } from 'react-router'; +import { useQueryParamState } from '@backstage/core'; import { SearchContextProvider } from '../SearchContext'; +import { JsonObject } from '@backstage/config'; export const SearchPageNext = () => { + const location = useLocation(); + const [queryString] = useQueryParamState('query'); + const filters = (qs.parse(location.search.substring(1), { arrayLimit: 0 }) + .filters || {}) as JsonObject; + const initialState = { + term: queryString || '', + types: [], + pageCursor: '', + filters, + }; + return ( - + ); diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index 597bfabfb1..b02561c710 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -15,6 +15,7 @@ */ export * from './Filters'; +export * from './SearchFilterNext'; export * from './SearchFiltersNext'; export * from './SearchBar'; export * from './SearchBarNext'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 4d8d9c08a2..5ff047e986 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -22,7 +22,6 @@ export { SearchPageNext, SearchBarNext, SearchResultNext, - SearchFiltersNext, DefaultResultListItem, } from './plugin'; export { @@ -35,6 +34,7 @@ export { SearchContextProvider, useSearch, SearchPage as Router, + SearchFilterNext, SearchResult, SidebarSearch, } from './components'; diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index cff594b299..6ff40ebaf7 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -100,12 +100,3 @@ export const DefaultResultListItem = searchPlugin.provide( }, }), ); - -export const SearchFiltersNext = searchPlugin.provide( - createComponentExtension({ - component: { - lazy: () => - import('./components/SearchFiltersNext').then(m => m.SearchFiltersNext), - }, - }), -);