Merge pull request #5581 from SDA-SE/feat/apidocssearch

Fix setting the state with `useQueryParamState` makes inputs lose their focus
This commit is contained in:
Ben Lambert
2021-05-07 15:42:09 +02:00
committed by GitHub
4 changed files with 98 additions and 69 deletions
+7
View File
@@ -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.
+79 -57
View File
@@ -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<BackstageTheme>(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<BackstageTheme>(theme => ({
const StyledMTableToolbar = withStyles(theme => ({
root: {
padding: theme.spacing(3, 0, 2.5, 2.5),
},
@@ -124,7 +127,7 @@ const useToolbarStyles = makeStyles<BackstageTheme>(theme => ({
searchField: {
paddingRight: theme.spacing(2),
},
}));
}))(MTableToolbar);
const useFilterStyles = makeStyles<BackstageTheme>(() => ({
root: {
@@ -208,6 +211,59 @@ export interface TableProps<T extends object = {}>
onStateChange?: (state: TableState) => any;
}
export function TableToolbar(toolbarProps: {
toolbarRef: MutableRefObject<any>;
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 (
<div className={filtersClasses.root}>
<div className={filtersClasses.root}>
<IconButton onClick={toggleFilters} aria-label="filter list">
<FilterList />
</IconButton>
<Typography className={filtersClasses.title}>
Filters ({selectedFiltersLength})
</Typography>
</div>
<StyledMTableToolbar
{...toolbarProps}
ref={toolbarRef}
onSearchChanged={onSearchChanged}
/>
</div>
);
}
return (
<StyledMTableToolbar
{...toolbarProps}
ref={toolbarRef}
onSearchChanged={onSearchChanged}
/>
);
}
export function Table<T extends object = {}>({
columns,
options,
@@ -219,10 +275,7 @@ export function Table<T extends object = {}>({
onStateChange,
...props
}: TableProps<T>) {
const headerClasses = useHeaderStyles();
const toolbarClasses = useToolbarStyles();
const tableClasses = useTableStyles();
const filtersClasses = useFilterStyles();
const { data, ...propsWithoutData } = props;
@@ -230,9 +283,12 @@ export function Table<T extends object = {}>({
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<T extends object = {}>({
}));
};
const hasFilters = !!filters?.length;
const Toolbar = useCallback(
toolbarProps => {
const onSearchChanged = (searchText: string) => {
toolbarProps.onSearchChanged(searchText);
setSearch(searchText);
};
if (filters?.length) {
return (
<div className={filtersClasses.root}>
<div className={filtersClasses.root}>
<IconButton
onClick={() => toggleFilters(el => !el)}
aria-label="filter list"
>
<FilterList />
</IconButton>
<Typography className={filtersClasses.title}>
Filters ({selectedFiltersLength})
</Typography>
</div>
<MTableToolbar
classes={toolbarClasses}
{...toolbarProps}
ref={toolbarRef}
onSearchChanged={onSearchChanged}
/>
</div>
);
}
return (
<MTableToolbar
classes={toolbarClasses}
<TableToolbar
toolbarRef={toolbarRef}
setSearch={setSearch}
hasFilters={hasFilters}
selectedFiltersLength={selectedFiltersLength}
toggleFilters={toggleFilters}
{...toolbarProps}
ref={toolbarRef}
onSearchChanged={onSearchChanged}
/>
);
},
[
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 (
<tbody>
<tr>
<td colSpan={columns.length}>{emptyContent}</td>
<td colSpan={columnCount}>{emptyContent}</td>
</tr>
</tbody>
);
@@ -444,7 +468,7 @@ export function Table<T extends object = {}>({
return <MTableBody {...bodyProps} />;
},
[data, emptyContent, columns],
[hasNoRows, emptyContent, columnCount],
);
return (
@@ -458,9 +482,7 @@ export function Table<T extends object = {}>({
)}
<MTable<T>
components={{
Header: headerProps => (
<MTableHeader classes={headerClasses} {...headerProps} />
),
Header: StyledMTableHeader,
Toolbar,
Body,
}}
+12 -11
View File
@@ -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<T> = (params: T) => void;
export function useQueryParamState<T>(
stateName: string,
debounceTime: number = 100,
/** @deprecated Don't configure a custom debouceTime */
debounceTime: number = 250,
): [T | undefined, SetQueryParams<T>] {
const navigate = useNavigate();
const location = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
const searchParamsString = searchParams.toString();
const [queryParamState, setQueryParamState] = useState<T>(
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];
@@ -156,7 +156,6 @@ export const ApiExplorerTable = ({
}: ExplorerTableProps) => {
const [queryParamState, setQueryParamState] = useQueryParamState<TableState>(
'apiTable',
500,
);
if (error) {