diff --git a/.changeset/short-bobcats-mate.md b/.changeset/short-bobcats-mate.md new file mode 100644 index 0000000000..d63e7058a9 --- /dev/null +++ b/.changeset/short-bobcats-mate.md @@ -0,0 +1,7 @@ +--- +'@backstage/core': patch +'@backstage/plugin-api-docs': patch +--- + +Fix state persisted in the URL make search input in the table toolbar lose their +focus. diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index 2251b8304e..1dd09e7ab8 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -20,6 +20,7 @@ import { makeStyles, Typography, useTheme, + withStyles, } from '@material-ui/core'; // Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51 import AddBox from '@material-ui/icons/AddBox'; @@ -49,6 +50,7 @@ import MTable, { } from 'material-table'; import React, { forwardRef, + MutableRefObject, ReactNode, useCallback, useEffect, @@ -100,19 +102,20 @@ function extractValueByField(data: any, field: string): any | undefined { return value; } -const useHeaderStyles = makeStyles(theme => ({ +const StyledMTableHeader = withStyles(theme => ({ header: { padding: theme.spacing(1, 2, 1, 2.5), borderTop: `1px solid ${theme.palette.grey.A100}`, borderBottom: `1px solid ${theme.palette.grey.A100}`, - color: theme.palette.textSubtle, + // withStyles hasn't a generic overload for theme + color: (theme as BackstageTheme).palette.textSubtle, fontWeight: theme.typography.fontWeightBold, position: 'static', wordBreak: 'normal', }, -})); +}))(MTableHeader); -const useToolbarStyles = makeStyles(theme => ({ +const StyledMTableToolbar = withStyles(theme => ({ root: { padding: theme.spacing(3, 0, 2.5, 2.5), }, @@ -124,7 +127,7 @@ const useToolbarStyles = makeStyles(theme => ({ searchField: { paddingRight: theme.spacing(2), }, -})); +}))(MTableToolbar); const useFilterStyles = makeStyles(() => ({ root: { @@ -208,6 +211,59 @@ export interface TableProps onStateChange?: (state: TableState) => any; } +export function TableToolbar(toolbarProps: { + toolbarRef: MutableRefObject; + setSearch: (value: string) => void; + onSearchChanged: (value: string) => void; + toggleFilters: () => void; + hasFilters: boolean; + selectedFiltersLength: number; +}) { + const { + toolbarRef, + setSearch, + hasFilters, + selectedFiltersLength, + toggleFilters, + } = toolbarProps; + const filtersClasses = useFilterStyles(); + const onSearchChanged = useCallback( + (searchText: string) => { + toolbarProps.onSearchChanged(searchText); + setSearch(searchText); + }, + [toolbarProps, setSearch], + ); + + if (hasFilters) { + return ( +
+
+ + + + + Filters ({selectedFiltersLength}) + +
+ +
+ ); + } + + return ( + + ); +} + export function Table({ columns, options, @@ -219,10 +275,7 @@ export function Table({ onStateChange, ...props }: TableProps) { - const headerClasses = useHeaderStyles(); - const toolbarClasses = useToolbarStyles(); const tableClasses = useTableStyles(); - const filtersClasses = useFilterStyles(); const { data, ...propsWithoutData } = props; @@ -230,9 +283,12 @@ export function Table({ const calculatedInitialState = { ...defaultInitialState, ...initialState }; - const [filtersOpen, toggleFilters] = useState( + const [filtersOpen, setFiltersOpen] = useState( calculatedInitialState.filtersOpen, ); + const toggleFilters = useCallback(() => setFiltersOpen(v => !v), [ + setFiltersOpen, + ]); const [selectedFiltersLength, setSelectedFiltersLength] = useState(0); const [tableData, setTableData] = useState(data as any[]); const [selectedFilters, setSelectedFilters] = useState( @@ -379,64 +435,32 @@ export function Table({ })); }; + const hasFilters = !!filters?.length; const Toolbar = useCallback( toolbarProps => { - const onSearchChanged = (searchText: string) => { - toolbarProps.onSearchChanged(searchText); - setSearch(searchText); - }; - - if (filters?.length) { - return ( -
-
- toggleFilters(el => !el)} - aria-label="filter list" - > - - - - Filters ({selectedFiltersLength}) - -
- -
- ); - } - return ( - ); }, - [ - filters?.length, - selectedFiltersLength, - toggleFilters, - toolbarClasses, - filtersClasses, - setSearch, - toolbarRef, - ], + [toggleFilters, hasFilters, selectedFiltersLength, setSearch, toolbarRef], ); + const hasNoRows = typeof data !== 'function' && data.length === 0; + const columnCount = columns.length; const Body = useCallback( bodyProps => { - if (emptyContent && typeof data !== 'function' && data.length === 0) { + if (emptyContent && hasNoRows) { return ( - {emptyContent} + {emptyContent} ); @@ -444,7 +468,7 @@ export function Table({ return ; }, - [data, emptyContent, columns], + [hasNoRows, emptyContent, columnCount], ); return ( @@ -458,9 +482,7 @@ export function Table({ )} components={{ - Header: headerProps => ( - - ), + Header: StyledMTableHeader, Toolbar, Body, }} diff --git a/packages/core/src/hooks/useQueryParamState.ts b/packages/core/src/hooks/useQueryParamState.ts index da303234f5..173993a466 100644 --- a/packages/core/src/hooks/useQueryParamState.ts +++ b/packages/core/src/hooks/useQueryParamState.ts @@ -17,7 +17,7 @@ import { isEqual } from 'lodash'; import qs from 'qs'; import { useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { useDebounce } from 'react-use'; function stringify(queryParams: any): string { @@ -58,36 +58,37 @@ type SetQueryParams = (params: T) => void; export function useQueryParamState( stateName: string, - debounceTime: number = 100, + /** @deprecated Don't configure a custom debouceTime */ + debounceTime: number = 250, ): [T | undefined, SetQueryParams] { - const navigate = useNavigate(); - const location = useLocation(); + const [searchParams, setSearchParams] = useSearchParams(); + const searchParamsString = searchParams.toString(); const [queryParamState, setQueryParamState] = useState( - extractState(location.search, stateName), + extractState(searchParamsString, stateName), ); useEffect(() => { - const newState = extractState(location.search, stateName); + const newState = extractState(searchParamsString, stateName); setQueryParamState(oldState => isEqual(newState, oldState) ? oldState : newState, ); - }, [location, stateName]); + }, [searchParamsString, setQueryParamState, stateName]); useDebounce( () => { const queryString = joinQueryString( - location.search, + searchParamsString, stateName, queryParamState, ); - if (location.search !== queryString) { - navigate({ ...location, search: `?${queryString}` }, { replace: true }); + if (searchParamsString !== queryString) { + setSearchParams(queryString, { replace: true }); } }, debounceTime, - [queryParamState], + [setSearchParams, queryParamState, searchParamsString, stateName], ); return [queryParamState, setQueryParamState]; diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 39c2caaf1b..5b9808e26d 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -156,7 +156,6 @@ export const ApiExplorerTable = ({ }: ExplorerTableProps) => { const [queryParamState, setQueryParamState] = useQueryParamState( 'apiTable', - 500, ); if (error) {