From 3de4bd4f19670c7c784fa5ed234e76ab06d64d6c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 6 Oct 2022 09:00:59 +0200 Subject: [PATCH 1/7] Create a page limit component Signed-off-by: Camila Belo --- .changeset/search-days-pull.md | 98 ++++++++++++ .../app/src/components/search/SearchPage.tsx | 6 + plugins/search-react/api-report.md | 26 ++++ .../SearchResultLimiter.stories.tsx | 55 +++++++ .../SearchResultLimiter.test.tsx | 118 ++++++++++++++ .../SearchResultLimiter.tsx | 146 ++++++++++++++++++ .../components/SearchResultLimiter/index.ts | 17 ++ plugins/search-react/src/components/index.ts | 1 + 8 files changed, 467 insertions(+) create mode 100644 .changeset/search-days-pull.md create mode 100644 plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx create mode 100644 plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx create mode 100644 plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx create mode 100644 plugins/search-react/src/components/SearchResultLimiter/index.ts diff --git a/.changeset/search-days-pull.md b/.changeset/search-days-pull.md new file mode 100644 index 0000000000..72fb9974a4 --- /dev/null +++ b/.changeset/search-days-pull.md @@ -0,0 +1,98 @@ +--- +'@backstage/plugin-search-react': minor +--- + +A `` 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 ( + +
} /> + + + + + + + + + + + + + + + ); +}; +``` + +_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 = () => ( + + +
} /> + + + + + + + + + + + {({ results }) => ( + ( + + )} + /> + )} + + + + + + +); +``` diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index 99814130c2..b97ac99558 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -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 = () => { )} + {({ results }) => ( diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index ddfe5d5b39..744f45065f 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -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; diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx new file mode 100644 index 0000000000..f098f1ab68 --- /dev/null +++ b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx @@ -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<{}>) => ( + + + + + + + + + + ), + ], +}; + +export const Default = () => { + return ; +}; + +export const CustomLabel = () => { + return ; +}; + +export const CustomOptions = () => { + return ; +}; diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx new file mode 100644 index 0000000000..a6f9f5b1e9 --- /dev/null +++ b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx @@ -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( + + + + + , + ); + + expect(screen.getByText('Results per page:')).toBeInTheDocument(); + expect(screen.getByText('25')).toBeInTheDocument(); + }); + + it('Define default options', async () => { + await renderWithEffects( + + + + + , + ); + + 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( + + + + + , + ); + + 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( + + + + + , + ); + + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + it('Accept custom options', async () => { + await renderWithEffects( + + + + + , + ); + + 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'); + }); +}); diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx new file mode 100644 index 0000000000..0d4b8381dd --- /dev/null +++ b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx @@ -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 ( + + + {label} + + + + ); +}; + +/** + * 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 ( + + ); +}; diff --git a/plugins/search-react/src/components/SearchResultLimiter/index.ts b/plugins/search-react/src/components/SearchResultLimiter/index.ts new file mode 100644 index 0000000000..411e48b5f5 --- /dev/null +++ b/plugins/search-react/src/components/SearchResultLimiter/index.ts @@ -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'; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 62d8b611a4..524b0f9194 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -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'; From 5eb598babb013462a7fc20719d82037574282de2 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 7 Oct 2022 17:38:42 +0200 Subject: [PATCH 2/7] refactor(search-react): restrict range of valid options type Signed-off-by: Camila Belo --- plugins/search-react/api-report.md | 10 ++++++++- .../SearchResultLimiter.tsx | 22 +++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 744f45065f..24b70240a0 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -335,11 +335,19 @@ export type SearchResultLimiterBaseProps = { id?: string; className?: string; label?: ReactNode; - options?: number[]; + options?: SearchResultLimiterOption[]; value?: number; onChange?: (value: number) => void; }; +// @public +export type SearchResultLimiterOption< + Current extends number = 101, + Accumulator extends number[] = [], +> = Accumulator['length'] extends Current + ? Accumulator[number] + : SearchResultLimiterOption; + // @public export type SearchResultLimiterProps = Omit< SearchResultLimiterBaseProps, diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx index 0d4b8381dd..7f2d1ffc58 100644 --- a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx +++ b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx @@ -25,6 +25,17 @@ import { } from '@material-ui/core'; import { useSearch } from '../../context'; +/** + * A page limit option, this value must not be greater than 100. + * @public + */ +export type SearchResultLimiterOption< + Current extends number = 101, + Accumulator extends number[] = [], +> = Accumulator['length'] extends Current + ? Accumulator[number] + : SearchResultLimiterOption; + /** * Props for {@link SearchResultLimiterBase}. * @public @@ -39,7 +50,7 @@ export type SearchResultLimiterBaseProps = { /** * The combobox labels, defaults to 10, 25, 50 and 100. */ - options?: number[]; + options?: SearchResultLimiterOption[]; /** * Combobox selected option, defaults to 25; */ @@ -129,18 +140,11 @@ export type SearchResultLimiterProps = Omit< export const SearchResultLimiter = (props: SearchResultLimiterProps) => { const { pageLimit, setPageLimit } = useSearch(); - const handleChange = useCallback( - (newPageLimit: number) => { - setPageLimit(newPageLimit); - }, - [setPageLimit], - ); - return ( ); }; From c9198c9fce457788db93a3d809310c84025aea06 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 8 Oct 2022 15:03:43 +0200 Subject: [PATCH 3/7] refactor(search-react): use mui table pagination component Signed-off-by: Camila Belo --- .changeset/search-days-pull.md | 19 +- .../app/src/components/search/SearchPage.tsx | 8 +- plugins/search-react/api-report.md | 78 ++++---- .../SearchPagination.stories.tsx} | 20 +- .../SearchPagination.test.tsx} | 110 ++++++++--- .../SearchPagination/SearchPagination.tsx | 178 ++++++++++++++++++ .../index.ts | 2 +- .../SearchResultLimiter.tsx | 150 --------------- plugins/search-react/src/components/index.ts | 2 +- 9 files changed, 331 insertions(+), 236 deletions(-) rename plugins/search-react/src/components/{SearchResultLimiter/SearchResultLimiter.stories.tsx => SearchPagination/SearchPagination.stories.tsx} (72%) rename plugins/search-react/src/components/{SearchResultLimiter/SearchResultLimiter.test.tsx => SearchPagination/SearchPagination.test.tsx} (58%) create mode 100644 plugins/search-react/src/components/SearchPagination/SearchPagination.tsx rename plugins/search-react/src/components/{SearchResultLimiter => SearchPagination}/index.ts (93%) delete mode 100644 plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx diff --git a/.changeset/search-days-pull.md b/.changeset/search-days-pull.md index 72fb9974a4..03000e4870 100644 --- a/.changeset/search-days-pull.md +++ b/.changeset/search-days-pull.md @@ -2,9 +2,7 @@ '@backstage/plugin-search-react': minor --- -A `` 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. +A `` component was created for limiting the number of results shown per search page. Use this new component to give users options to select how many search results they want to display per page. The default options are 10, 25, 50, 100. See examples below: @@ -16,13 +14,14 @@ import { Grid } from '@material-ui/core'; import { Page, Header, Content, Lifecycle } from '@backstage/core-components'; import { SearchBarBase, - SearchResultLimiterBase, + SearchPaginationBase, SearchResultList, } from '@backstage/plugin-search-react'; const SearchPage = () => { const [term, setTerm] = useState(''); const [pageLimit, setPageLimit] = useState(25); + const [pageCursor, setPageCursor] = useState(); return ( @@ -33,9 +32,11 @@ const SearchPage = () => { - @@ -57,7 +58,7 @@ import { Page, Header, Content, Lifecycle } from '@backstage/core-components'; import { SearchBar, SearchResult, - SearchResultLimiter, + SearchPagination, SearchResultListLayout, SearchContextProvider, DefaultResultListItem, @@ -73,7 +74,7 @@ const SearchPage = () => ( - + diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index b97ac99558..d324b683d8 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -35,7 +35,7 @@ import { SearchBar, SearchFilter, SearchResult, - SearchResultLimiter, + SearchPagination, SearchResultPager, useSearch, } from '@backstage/plugin-search-react'; @@ -56,10 +56,6 @@ const useStyles = makeStyles((theme: Theme) => ({ padding: theme.spacing(2), marginTop: theme.spacing(2), }, - limiter: { - width: '100%', - justifyContent: 'flex-end', - }, })); const SearchPage = () => { @@ -134,7 +130,7 @@ const SearchPage = () => { )} - + {({ results }) => ( diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 24b70240a0..57c1791d9a 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -211,6 +211,50 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { debug?: boolean; }; +// @public +export const SearchPagination: (props: SearchPaginationProps) => JSX.Element; + +// @public +export const SearchPaginationBase: ( + props: SearchPaginationBaseProps, +) => JSX.Element; + +// @public +export type SearchPaginationBaseProps = { + className?: string; + pageCursor?: string; + onPageCursorChange?: (pageCursor: string) => void; + pageLimit?: number; + pageLimitLabel?: ReactNode; + pageLimitText?: SearchPaginationLimitText; + pageLimitOptions?: SearchPaginationLimitOption[]; + onPageLimitChange?: (value: number) => void; +}; + +// @public +export type SearchPaginationLimitOption< + Current extends number = 101, + Accumulator extends number[] = [], +> = Accumulator['length'] extends Current + ? Accumulator[number] + : SearchPaginationLimitOption< + Current, + [...Accumulator, Accumulator['length']] + >; + +// @public +export type SearchPaginationLimitText = (params: { + from: number; + to: number; + page: number; +}) => ReactNode; + +// @public +export type SearchPaginationProps = Omit< + SearchPaginationBaseProps, + 'pageLimit' | 'onPageLimitChange' | 'pageCursor' | 'onPageCursorChange' +>; + // @public export const SearchResult: (props: SearchResultProps) => JSX.Element; @@ -320,40 +364,6 @@ 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?: SearchResultLimiterOption[]; - value?: number; - onChange?: (value: number) => void; -}; - -// @public -export type SearchResultLimiterOption< - Current extends number = 101, - Accumulator extends number[] = [], -> = Accumulator['length'] extends Current - ? Accumulator[number] - : SearchResultLimiterOption; - -// @public -export type SearchResultLimiterProps = Omit< - SearchResultLimiterBaseProps, - 'value' | 'onChange' ->; - // @public export const SearchResultList: (props: SearchResultListProps) => JSX.Element; diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx similarity index 72% rename from plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx rename to plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx index f098f1ab68..eca668d165 100644 --- a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx @@ -22,11 +22,11 @@ import { TestApiProvider } from '@backstage/test-utils'; import { searchApiRef, MockSearchApi } from '../../api'; import { SearchContextProvider } from '../../context'; -import { SearchResultLimiter } from './SearchResultLimiter'; +import { SearchPagination } from './SearchPagination'; export default { - title: 'Plugins/Search/SearchResultLimiter', - component: SearchResultLimiter, + title: 'Plugins/Search/SearchPagination', + component: SearchPagination, decorators: [ (Story: ComponentType<{}>) => ( @@ -43,13 +43,17 @@ export default { }; export const Default = () => { - return ; + return ; }; -export const CustomLabel = () => { - return ; +export const CustomPageLimitLabel = () => { + return ; }; -export const CustomOptions = () => { - return ; +export const CustomPageLimitText = () => { + return `${from}-${to}`} />; +}; + +export const CustomPageLimitOptions = () => { + return ; }; diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx similarity index 58% rename from plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx rename to plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx index a6f9f5b1e9..723a8a0031 100644 --- a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx @@ -23,11 +23,15 @@ import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; import { searchApiRef } from '../../api'; import { SearchContextProvider } from '../../context'; -import { SearchResultLimiter } from './SearchResultLimiter'; +import { SearchPagination } from './SearchPagination'; -const query = jest.fn().mockResolvedValue({ results: [] }); +const query = jest.fn().mockResolvedValue({ + results: [], + nextPageCursor: 'Mg==', + previousPageCursor: 'MA==', +}); -describe('SearchResultLimiter', () => { +describe('SearchPagination', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -36,20 +40,23 @@ describe('SearchResultLimiter', () => { await renderWithEffects( - + , ); expect(screen.getByText('Results per page:')).toBeInTheDocument(); expect(screen.getByText('25')).toBeInTheDocument(); + expect(screen.getByText('1-25 of more than 25')).toBeInTheDocument(); + expect(screen.getByLabelText('Next page')).toBeEnabled(); + expect(screen.getByLabelText('Previous page')).toBeDisabled(); }); - it('Define default options', async () => { + it('Define default page limit options', async () => { await renderWithEffects( - + , ); @@ -64,11 +71,55 @@ describe('SearchResultLimiter', () => { expect(options[3]).toHaveTextContent('100'); }); + it('Accept custom page limit label', async () => { + const label = 'Page limit:'; + await renderWithEffects( + + + + + , + ); + + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + it('Accept custom page limit text', async () => { + await renderWithEffects( + + + `${from}-${to}`} /> + + , + ); + + expect(screen.getByText('1-25')).toBeInTheDocument(); + }); + + it('Accept custom page limit options', async () => { + await renderWithEffects( + + + + + , + ); + + 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'); + }); + it('Set page limit in the context', async () => { await renderWithEffects( - + , ); @@ -84,35 +135,40 @@ describe('SearchResultLimiter', () => { ); }); - it('Accept custom label', async () => { - const label = 'Custom label'; + it('Set page cursor in the context', async () => { + const initialState = { + term: '', + types: [], + filters: {}, + pageCursor: 'MQ==', // page: 1 + }; + await renderWithEffects( - - + + , ); - expect(screen.getByText(label)).toBeInTheDocument(); - }); + await userEvent.click(screen.getByLabelText('Next page')); - it('Accept custom options', async () => { - await renderWithEffects( - - - - - , + expect(screen.getByText('51-75 of more than 75')).toBeInTheDocument(); + + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + pageCursor: 'Mg==', // page: 2 + }), ); - await userEvent.click(screen.getByText('25')); + await userEvent.click(screen.getByLabelText('Previous page')); - 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'); + expect(screen.getByText('26-50 of more than 50')).toBeInTheDocument(); + + expect(query).toHaveBeenLastCalledWith( + expect.objectContaining({ + pageCursor: 'MQ==', // page: 1 + }), + ); }); }); diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx new file mode 100644 index 0000000000..c0d1c127c8 --- /dev/null +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx @@ -0,0 +1,178 @@ +/* + * 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, + MouseEvent, + useCallback, + useMemo, +} from 'react'; +import { TablePagination } from '@material-ui/core'; +import { useSearch } from '../../context'; + +const encodePageCursor = (pageCursor: number): string => { + return Buffer.from(pageCursor.toString(), 'utf-8').toString('base64'); +}; + +const decodePageCursor = (pageCursor?: string): number => { + if (!pageCursor) return 0; + return Number(Buffer.from(pageCursor, 'base64').toString('utf-8')); +}; + +/** + * A page limit option, this value must not be greater than 100. + * @public + */ +export type SearchPaginationLimitOption< + Current extends number = 101, + Accumulator extends number[] = [], +> = Accumulator['length'] extends Current + ? Accumulator[number] + : SearchPaginationLimitOption< + Current, + [...Accumulator, Accumulator['length']] + >; + +/** + * A page limit text, this function is called with a "\{ from, to, page \}" object. + * @public + */ +export type SearchPaginationLimitText = (params: { + from: number; + to: number; + page: number; +}) => ReactNode; + +/** + * Props for {@link SearchPaginationBase}. + * @public + */ +export type SearchPaginationBaseProps = { + /** + * The component class name. + */ + className?: string; + /** + * The cursor for the current page. + */ + pageCursor?: string; + /** + * Callback fired when the current page cursor is changed. + */ + onPageCursorChange?: (pageCursor: string) => void; + /** + * The limit of results per page. + * Set -1 to display all the results. + */ + pageLimit?: number; + /** + * Customize the results per page label. + */ + pageLimitLabel?: ReactNode; + /** + * Customize the results per page text. + */ + pageLimitText?: SearchPaginationLimitText; + /** + * Options for setting how many results show per page. + * If less than two options are available, no select field will be displayed. + * Use -1 for the value with a custom label to show all the results. + */ + pageLimitOptions?: SearchPaginationLimitOption[]; + /** + * Callback fired when the number of results per page is changed. + */ + onPageLimitChange?: (value: number) => void; +}; + +/** + * A component with controls for search results pagination. + * @param props - See {@link SearchPaginationBaseProps}. + * @public + */ +export const SearchPaginationBase = (props: SearchPaginationBaseProps) => { + const { + pageCursor, + onPageCursorChange, + pageLimit: rowsPerPage = 25, + pageLimitLabel: labelRowsPerPage = 'Results per page:', + pageLimitText: labelDisplayedRows, + pageLimitOptions: rowsPerPageOptions, + onPageLimitChange, + ...rest + } = props; + + const page = useMemo(() => decodePageCursor(pageCursor), [pageCursor]); + + const handlePageChange = useCallback( + (_: MouseEvent | null, newValue: number) => { + onPageCursorChange?.(encodePageCursor(newValue)); + }, + [onPageCursorChange], + ); + + const handleRowsPerPageChange = useCallback( + (e: ChangeEvent) => { + const newValue = e.target.value; + onPageLimitChange?.(parseInt(newValue, 10)); + }, + [onPageLimitChange], + ); + + return ( + + ); +}; + +/** + * Props for {@link SearchPagination}. + * @public + */ +export type SearchPaginationProps = Omit< + SearchPaginationBaseProps, + 'pageLimit' | 'onPageLimitChange' | 'pageCursor' | 'onPageCursorChange' +>; + +/** + * A component for setting the search context page limit and cursor. + * @param props - See {@link SearchPaginationProps}. + * @public + */ +export const SearchPagination = (props: SearchPaginationProps) => { + const { pageLimit, setPageLimit, pageCursor, setPageCursor } = useSearch(); + + return ( + + ); +}; diff --git a/plugins/search-react/src/components/SearchResultLimiter/index.ts b/plugins/search-react/src/components/SearchPagination/index.ts similarity index 93% rename from plugins/search-react/src/components/SearchResultLimiter/index.ts rename to plugins/search-react/src/components/SearchPagination/index.ts index 411e48b5f5..74053f0e11 100644 --- a/plugins/search-react/src/components/SearchResultLimiter/index.ts +++ b/plugins/search-react/src/components/SearchPagination/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './SearchResultLimiter'; +export * from './SearchPagination'; diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx deleted file mode 100644 index 7f2d1ffc58..0000000000 --- a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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'; - -/** - * A page limit option, this value must not be greater than 100. - * @public - */ -export type SearchResultLimiterOption< - Current extends number = 101, - Accumulator extends number[] = [], -> = Accumulator['length'] extends Current - ? Accumulator[number] - : SearchResultLimiterOption; - -/** - * 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?: SearchResultLimiterOption[]; - /** - * 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 ( - - - {label} - - - - ); -}; - -/** - * 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(); - - return ( - - ); -}; diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts index 524b0f9194..5f68905407 100644 --- a/plugins/search-react/src/components/index.ts +++ b/plugins/search-react/src/components/index.ts @@ -20,7 +20,7 @@ export * from './SearchAutocomplete'; export * from './SearchFilter'; export * from './SearchResult'; export * from './SearchResultPager'; -export * from './SearchResultLimiter'; +export * from './SearchPagination'; export * from './SearchResultList'; export * from './SearchResultGroup'; export * from './DefaultResultListItem'; From 90616bcaa6f9a1f5b34ae0e5f1a42dd62f3b32cd Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 8 Oct 2022 15:04:49 +0200 Subject: [PATCH 4/7] refactor(search-react): add to create-app search page template Signed-off-by: Camila Belo --- .changeset/silent-mice-brake.md | 5 +++++ .../packages/app/src/components/search/SearchPage.tsx | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/silent-mice-brake.md diff --git a/.changeset/silent-mice-brake.md b/.changeset/silent-mice-brake.md new file mode 100644 index 0000000000..c8ebb28f40 --- /dev/null +++ b/.changeset/silent-mice-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +Add the new search pagination component to the search page template. diff --git a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx index 928b8201ea..9f11d0c80c 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx @@ -14,6 +14,7 @@ import { SearchBar, SearchFilter, SearchResult, + SearchPagination, useSearch, } from '@backstage/plugin-search-react'; import { @@ -109,6 +110,7 @@ const SearchPage = () => { + {({ results }) => ( From 270ee83613e4a31599b6da14713a1d2ff60aee50 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 10 Oct 2022 12:08:16 +0200 Subject: [PATCH 5/7] refactor: change default page limit text Signed-off-by: Camila Belo --- .../SearchPagination/SearchPagination.stories.tsx | 8 ++++++-- .../SearchPagination/SearchPagination.test.tsx | 12 +++++++----- .../components/SearchPagination/SearchPagination.tsx | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx index eca668d165..c23fb1e863 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx @@ -47,11 +47,15 @@ export const Default = () => { }; export const CustomPageLimitLabel = () => { - return ; + return ; }; export const CustomPageLimitText = () => { - return `${from}-${to}`} />; + return ( + `${from}-${to} of more than ${to}`} + /> + ); }; export const CustomPageLimitOptions = () => { diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx index 723a8a0031..6e4abcdbda 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx @@ -47,7 +47,7 @@ describe('SearchPagination', () => { expect(screen.getByText('Results per page:')).toBeInTheDocument(); expect(screen.getByText('25')).toBeInTheDocument(); - expect(screen.getByText('1-25 of more than 25')).toBeInTheDocument(); + expect(screen.getByText('1-25')).toBeInTheDocument(); expect(screen.getByLabelText('Next page')).toBeEnabled(); expect(screen.getByLabelText('Previous page')).toBeDisabled(); }); @@ -88,12 +88,14 @@ describe('SearchPagination', () => { await renderWithEffects( - `${from}-${to}`} /> + `${from}-${to} of more than ${to}`} + /> , ); - expect(screen.getByText('1-25')).toBeInTheDocument(); + expect(screen.getByText('1-25 of more than 25')).toBeInTheDocument(); }); it('Accept custom page limit options', async () => { @@ -153,7 +155,7 @@ describe('SearchPagination', () => { await userEvent.click(screen.getByLabelText('Next page')); - expect(screen.getByText('51-75 of more than 75')).toBeInTheDocument(); + expect(screen.getByText('51-75')).toBeInTheDocument(); expect(query).toHaveBeenLastCalledWith( expect.objectContaining({ @@ -163,7 +165,7 @@ describe('SearchPagination', () => { await userEvent.click(screen.getByLabelText('Previous page')); - expect(screen.getByText('26-50 of more than 50')).toBeInTheDocument(); + expect(screen.getByText('26-50')).toBeInTheDocument(); expect(query).toHaveBeenLastCalledWith( expect.objectContaining({ diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx index c0d1c127c8..e5807832e9 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx @@ -110,7 +110,7 @@ export const SearchPaginationBase = (props: SearchPaginationBaseProps) => { onPageCursorChange, pageLimit: rowsPerPage = 25, pageLimitLabel: labelRowsPerPage = 'Results per page:', - pageLimitText: labelDisplayedRows, + pageLimitText: labelDisplayedRows = ({ from, to }) => `${from}-${to}`, pageLimitOptions: rowsPerPageOptions, onPageLimitChange, ...rest From 77b9448291265464da86fc0802eceb71abbdbcf4 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 10 Oct 2022 12:12:26 +0200 Subject: [PATCH 6/7] chore: change create app changeset to patch Signed-off-by: Camila Belo --- .changeset/silent-mice-brake.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silent-mice-brake.md b/.changeset/silent-mice-brake.md index c8ebb28f40..fde02146c9 100644 --- a/.changeset/silent-mice-brake.md +++ b/.changeset/silent-mice-brake.md @@ -1,5 +1,5 @@ --- -'@backstage/create-app': minor +'@backstage/create-app': patch --- Add the new search pagination component to the search page template. From 098f487e59d5d000e4a8752a7bcc1b0fa071d448 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 11 Oct 2022 09:15:14 +0200 Subject: [PATCH 7/7] refactor: add results total prop Signed-off-by: Camila Belo --- .changeset/search-days-pull.md | 8 +-- plugins/search-react/api-report.md | 16 +++--- .../SearchPagination.stories.tsx | 6 +-- .../SearchPagination.test.tsx | 18 +++++-- .../SearchPagination/SearchPagination.tsx | 52 ++++++++++++------- 5 files changed, 63 insertions(+), 37 deletions(-) diff --git a/.changeset/search-days-pull.md b/.changeset/search-days-pull.md index 03000e4870..45d59be514 100644 --- a/.changeset/search-days-pull.md +++ b/.changeset/search-days-pull.md @@ -33,10 +33,10 @@ const SearchPage = () => { diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 57c1791d9a..ef3e8b19bd 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -222,13 +222,14 @@ export const SearchPaginationBase: ( // @public export type SearchPaginationBaseProps = { className?: string; - pageCursor?: string; - onPageCursorChange?: (pageCursor: string) => void; - pageLimit?: number; - pageLimitLabel?: ReactNode; - pageLimitText?: SearchPaginationLimitText; - pageLimitOptions?: SearchPaginationLimitOption[]; - onPageLimitChange?: (value: number) => void; + total?: number; + cursor?: string; + onCursorChange?: (pageCursor: string) => void; + limit?: number; + limitLabel?: ReactNode; + limitText?: SearchPaginationLimitText; + limitOptions?: SearchPaginationLimitOption[]; + onLimitChange?: (value: number) => void; }; // @public @@ -247,6 +248,7 @@ export type SearchPaginationLimitText = (params: { from: number; to: number; page: number; + count: number; }) => ReactNode; // @public diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx index c23fb1e863..4e72c0fa25 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.stories.tsx @@ -47,17 +47,17 @@ export const Default = () => { }; export const CustomPageLimitLabel = () => { - return ; + return ; }; export const CustomPageLimitText = () => { return ( `${from}-${to} of more than ${to}`} + limitText={({ from, to }) => `${from}-${to} of more than ${to}`} /> ); }; export const CustomPageLimitOptions = () => { - return ; + return ; }; diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx index 6e4abcdbda..d2ecc55d31 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx @@ -76,7 +76,7 @@ describe('SearchPagination', () => { await renderWithEffects( - + , ); @@ -84,12 +84,24 @@ describe('SearchPagination', () => { expect(screen.getByText(label)).toBeInTheDocument(); }); + it('Show the total in text', async () => { + await renderWithEffects( + + + + + , + ); + + expect(screen.getByText('of 100')).toBeInTheDocument(); + }); + it('Accept custom page limit text', async () => { await renderWithEffects( `${from}-${to} of more than ${to}`} + limitText={({ from, to }) => `${from}-${to} of more than ${to}`} /> , @@ -102,7 +114,7 @@ describe('SearchPagination', () => { await renderWithEffects( - + , ); diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx index e5807832e9..34b6deaff5 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx @@ -48,13 +48,14 @@ export type SearchPaginationLimitOption< >; /** - * A page limit text, this function is called with a "\{ from, to, page \}" object. + * A page limit text, this function is called with a "\{ from, to, page, count \}" object. * @public */ export type SearchPaginationLimitText = (params: { from: number; to: number; page: number; + count: number; }) => ReactNode; /** @@ -66,37 +67,46 @@ export type SearchPaginationBaseProps = { * The component class name. */ className?: string; + /** + * The total number of results. + * For an unknown number of items, provide -1. + * Defaults to -1. + */ + total?: number; /** * The cursor for the current page. */ - pageCursor?: string; + cursor?: string; /** * Callback fired when the current page cursor is changed. */ - onPageCursorChange?: (pageCursor: string) => void; + onCursorChange?: (pageCursor: string) => void; /** * The limit of results per page. * Set -1 to display all the results. */ - pageLimit?: number; + limit?: number; /** * Customize the results per page label. + * Defaults to "Results per page:". */ - pageLimitLabel?: ReactNode; + limitLabel?: ReactNode; /** * Customize the results per page text. + * Defaults to "(\{ from, to, count \}) =\> count \> 0 ? `of $\{count\}` : `$\{from\}-$\{to\}`". */ - pageLimitText?: SearchPaginationLimitText; + limitText?: SearchPaginationLimitText; /** * Options for setting how many results show per page. * If less than two options are available, no select field will be displayed. * Use -1 for the value with a custom label to show all the results. + * Defaults to [10, 25, 50, 100]. */ - pageLimitOptions?: SearchPaginationLimitOption[]; + limitOptions?: SearchPaginationLimitOption[]; /** * Callback fired when the number of results per page is changed. */ - onPageLimitChange?: (value: number) => void; + onLimitChange?: (value: number) => void; }; /** @@ -106,13 +116,15 @@ export type SearchPaginationBaseProps = { */ export const SearchPaginationBase = (props: SearchPaginationBaseProps) => { const { - pageCursor, - onPageCursorChange, - pageLimit: rowsPerPage = 25, - pageLimitLabel: labelRowsPerPage = 'Results per page:', - pageLimitText: labelDisplayedRows = ({ from, to }) => `${from}-${to}`, - pageLimitOptions: rowsPerPageOptions, - onPageLimitChange, + total: count = -1, + cursor: pageCursor, + onCursorChange: onPageCursorChange, + limit: rowsPerPage = 25, + limitLabel: labelRowsPerPage = 'Results per page:', + limitText: labelDisplayedRows = ({ from, to }) => + count > 0 ? `of ${count}` : `${from}-${to}`, + limitOptions: rowsPerPageOptions, + onLimitChange: onPageLimitChange, ...rest } = props; @@ -137,7 +149,7 @@ export const SearchPaginationBase = (props: SearchPaginationBaseProps) => { { return ( ); };