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:
@@ -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.
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user