Create a page limit component
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': minor
|
||||
---
|
||||
|
||||
A `<SearchResultLimiter />` component was created for limiting the number of results shown per search page.
|
||||
Use this new component to give users a combination of options to define how many search results they want to display per page.
|
||||
The default options are 10, 25, 50, 100.
|
||||
|
||||
See examples below:
|
||||
|
||||
_Basic_
|
||||
|
||||
```jsx
|
||||
import React, { useState } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { Page, Header, Content, Lifecycle } from '@backstage/core-components';
|
||||
import {
|
||||
SearchBarBase,
|
||||
SearchResultLimiterBase,
|
||||
SearchResultList,
|
||||
} from '@backstage/plugin-search-react';
|
||||
|
||||
const SearchPage = () => {
|
||||
const [term, setTerm] = useState('');
|
||||
const [pageLimit, setPageLimit] = useState(25);
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header title="Search" subtitle={<Lifecycle alpha />} />
|
||||
<Content>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<SearchBarBase value={term} onChange={setTerm} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SearchResultLimiterBase
|
||||
value={pageLimit}
|
||||
onChange={setPageLimit}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SearchResultList query={{ term, pageLimit }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
_With context_
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { Page, Header, Content, Lifecycle } from '@backstage/core-components';
|
||||
import {
|
||||
SearchBar,
|
||||
SearchResult,
|
||||
SearchResultLimiter,
|
||||
SearchResultListLayout,
|
||||
SearchContextProvider,
|
||||
DefaultResultListItem,
|
||||
} from '@backstage/plugin-search-react';
|
||||
|
||||
const SearchPage = () => (
|
||||
<SearchContextProvider>
|
||||
<Page themeId="home">
|
||||
<Header title="Search" subtitle={<Lifecycle alpha />} />
|
||||
<Content>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<SearchBar />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SearchResultLimiter />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SearchResult>
|
||||
{({ results }) => (
|
||||
<SearchResultListLayout
|
||||
resultItems={results}
|
||||
renderResultItem={({ document }) => (
|
||||
<DefaultResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</SearchResult>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
</SearchContextProvider>
|
||||
);
|
||||
```
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
SearchBar,
|
||||
SearchFilter,
|
||||
SearchResult,
|
||||
SearchResultLimiter,
|
||||
SearchResultPager,
|
||||
useSearch,
|
||||
} from '@backstage/plugin-search-react';
|
||||
@@ -55,6 +56,10 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||
padding: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
limiter: {
|
||||
width: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
}));
|
||||
|
||||
const SearchPage = () => {
|
||||
@@ -129,6 +134,7 @@ const SearchPage = () => {
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs>
|
||||
<SearchResultLimiter className={classes.limiter} />
|
||||
<SearchResult>
|
||||
{({ results }) => (
|
||||
<List>
|
||||
|
||||
@@ -320,6 +320,32 @@ export const SearchResultGroupTextFilterField: (
|
||||
export type SearchResultGroupTextFilterFieldProps =
|
||||
SearchResultGroupFilterFieldPropsWith<{}>;
|
||||
|
||||
// @public
|
||||
export const SearchResultLimiter: (
|
||||
props: SearchResultLimiterProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const SearchResultLimiterBase: (
|
||||
props: SearchResultLimiterBaseProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type SearchResultLimiterBaseProps = {
|
||||
id?: string;
|
||||
className?: string;
|
||||
label?: ReactNode;
|
||||
options?: number[];
|
||||
value?: number;
|
||||
onChange?: (value: number) => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type SearchResultLimiterProps = Omit<
|
||||
SearchResultLimiterBaseProps,
|
||||
'value' | 'onChange'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const SearchResultList: (props: SearchResultListProps) => JSX.Element;
|
||||
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2022 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, { ComponentType } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
|
||||
import { SearchResultLimiter } from './SearchResultLimiter';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Search/SearchResultLimiter',
|
||||
component: SearchResultLimiter,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<Story />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return <SearchResultLimiter />;
|
||||
};
|
||||
|
||||
export const CustomLabel = () => {
|
||||
return <SearchResultLimiter label="Results limit:" />;
|
||||
};
|
||||
|
||||
export const CustomOptions = () => {
|
||||
return <SearchResultLimiter options={[5, 10, 20]} />;
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2022 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 from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { renderWithEffects, TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { searchApiRef } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
|
||||
import { SearchResultLimiter } from './SearchResultLimiter';
|
||||
|
||||
const query = jest.fn().mockResolvedValue({ results: [] });
|
||||
|
||||
describe('SearchResultLimiter', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Renders without exploding', async () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Results per page:')).toBeInTheDocument();
|
||||
expect(screen.getByText('25')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Define default options', async () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('25'));
|
||||
|
||||
const options = screen.getAllByRole('option');
|
||||
expect(options).toHaveLength(4);
|
||||
expect(options[0]).toHaveTextContent('10');
|
||||
expect(options[1]).toHaveTextContent('25');
|
||||
expect(options[2]).toHaveTextContent('50');
|
||||
expect(options[3]).toHaveTextContent('100');
|
||||
});
|
||||
|
||||
it('Set page limit in the context', async () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('25'));
|
||||
|
||||
await userEvent.click(screen.getByText('10'));
|
||||
|
||||
expect(query).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pageLimit: 10,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('Accept custom label', async () => {
|
||||
const label = 'Custom label';
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter label={label} />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Accept custom options', async () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter options={[5, 10, 20, 25]} />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('25'));
|
||||
|
||||
const options = screen.getAllByRole('option');
|
||||
expect(options).toHaveLength(4);
|
||||
expect(options[0]).toHaveTextContent('5');
|
||||
expect(options[1]).toHaveTextContent('10');
|
||||
expect(options[2]).toHaveTextContent('20');
|
||||
expect(options[3]).toHaveTextContent('25');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2022 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, { ReactNode, ChangeEvent, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
InputBase,
|
||||
MenuItem,
|
||||
Select,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@material-ui/core';
|
||||
import { useSearch } from '../../context';
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultLimiterBase}.
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultLimiterBaseProps = {
|
||||
id?: string;
|
||||
className?: string;
|
||||
/**
|
||||
* A label for the combobox.
|
||||
*/
|
||||
label?: ReactNode;
|
||||
/**
|
||||
* The combobox labels, defaults to 10, 25, 50 and 100.
|
||||
*/
|
||||
options?: number[];
|
||||
/**
|
||||
* Combobox selected option, defaults to 25;
|
||||
*/
|
||||
value?: number;
|
||||
/**
|
||||
* The callback handler called when the selected option changed.
|
||||
*/
|
||||
onChange?: (value: number) => void;
|
||||
};
|
||||
|
||||
const DEFAULT_PAGE_LIMIT = 25;
|
||||
|
||||
/**
|
||||
* A component for selecting the number of results per page.
|
||||
* @param props - See {@link SearchResultLimiterBaseProps}.
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultLimiterBase = (
|
||||
props: SearchResultLimiterBaseProps,
|
||||
) => {
|
||||
const {
|
||||
id = 'search-result-limiter',
|
||||
className,
|
||||
label = 'Results per page:',
|
||||
options = [10, 50, 100],
|
||||
value = DEFAULT_PAGE_LIMIT,
|
||||
onChange = () => {},
|
||||
} = props;
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<{ value: unknown }>) => {
|
||||
const newValue = e.target.value;
|
||||
if (typeof newValue === 'number') {
|
||||
onChange(newValue);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={className}
|
||||
display="inline-grid"
|
||||
gridGap={theme.spacing(0.5)}
|
||||
gridAutoFlow="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography id={`${id}-label`} variant="body2">
|
||||
{label}
|
||||
</Typography>
|
||||
<Select
|
||||
id={`${id}-select`}
|
||||
labelId={`${id}-label`}
|
||||
variant="standard"
|
||||
input={<InputBase />}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{[...new Set([DEFAULT_PAGE_LIMIT, ...options])]
|
||||
.sort((a, b) => a - b)
|
||||
.map(option => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultLimiter}.
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultLimiterProps = Omit<
|
||||
SearchResultLimiterBaseProps,
|
||||
'value' | 'onChange'
|
||||
>;
|
||||
|
||||
/**
|
||||
* A component for setting the search context page limit.
|
||||
* @param props - See {@link SearchResultLimiterProps}.
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultLimiter = (props: SearchResultLimiterProps) => {
|
||||
const { pageLimit, setPageLimit } = useSearch();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newPageLimit: number) => {
|
||||
setPageLimit(newPageLimit);
|
||||
},
|
||||
[setPageLimit],
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchResultLimiterBase
|
||||
{...props}
|
||||
value={pageLimit}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
export * from './SearchResultLimiter';
|
||||
@@ -20,6 +20,7 @@ export * from './SearchAutocomplete';
|
||||
export * from './SearchFilter';
|
||||
export * from './SearchResult';
|
||||
export * from './SearchResultPager';
|
||||
export * from './SearchResultLimiter';
|
||||
export * from './SearchResultList';
|
||||
export * from './SearchResultGroup';
|
||||
export * from './DefaultResultListItem';
|
||||
|
||||
Reference in New Issue
Block a user