Skip to content

Commit

Permalink
Limiting pagination buttons (#5)
Browse files Browse the repository at this point in the history
* Limited pagination using `maxButton` prop

- Also made it fully customizable via direct props for the ButtonGroup,
  or via buttonProps for all the page button props
- Now we don't use `0` in the URL for paginating results, despite the
  API requiring it

I'd still move this to ui-components at some point, because we're gonna
need it in ui-scaffold sooner than later.

* Remove unnecessary else

* Simplify ellipsis creation with a custom component

Also fixed some issues related to old pagination flow (removing
unnecessary adds and substracts).

And passed missing arguments to both ellipsis buttons and ellipsis
inputs.

* Hard limit min number of maxButtons to 5

* Fix keys and pages issue

* Fix pages position after latest changes
  • Loading branch information
elboletaire authored Jun 7, 2024
1 parent 198d5ca commit 3019238
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 45 deletions.
20 changes: 10 additions & 10 deletions src/components/Organizations/OrganizationsList.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { InputSearch } from '~src/layout/Inputs'
import { useOrganizationCount, useOrganizationList } from '~queries/organizations'
import { debounce } from '~utils/debounce'
import { ChangeEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { generatePath, useNavigate, useParams } from 'react-router-dom'
import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider'
import OrganizationCard from '~components/Organizations/Card'
import { RoutedPagination } from '~components/Pagination/Pagination'
import LoadingError from '~src/layout/LoadingError'
import { useTranslation } from 'react-i18next'
import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider'
import { useOrganizationCount, useOrganizationList } from '~queries/organizations'
import { InputSearch } from '~src/layout/Inputs'
import { LoadingCards } from '~src/layout/Loading'
import LoadingError from '~src/layout/LoadingError'
import { organizationsListPath } from '~src/router'
import { ChangeEvent } from 'react'
import { debounce } from '~utils/debounce'

export const OrganizationsFilter = () => {
const { t } = useTranslation()
const navigate = useNavigate()

const debouncedSearch = debounce((value) => {
navigate(generatePath(organizationsListPath, { page: '0', query: value as string }))
navigate(generatePath(organizationsListPath, { page: '1', query: value as string }))
}, 1000)

return (
Expand All @@ -41,7 +41,7 @@ export const PaginatedOrganizationsList = () => {
isError,
error,
} = useOrganizationList({
page: Number(page || 0),
page: Number(page || 1),
organizationId: query,
})

Expand All @@ -56,7 +56,7 @@ export const PaginatedOrganizationsList = () => {
}

return (
<RoutedPaginationProvider totalPages={Math.ceil(count / 10)} path={organizationsListPath}>
<RoutedPaginationProvider totalPages={!query ? Math.ceil(count / 10) : undefined} path={organizationsListPath}>
{orgs?.organizations.map((org) => (
<OrganizationCard key={org.organizationID} id={org.organizationID} electionCount={org.electionCount} />
))}
Expand Down
215 changes: 183 additions & 32 deletions src/components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,196 @@
import { Button, ButtonGroup } from '@chakra-ui/react'
import { ReactElement, useMemo } from 'react'
import { generatePath, Link as RouterLink, useParams } from 'react-router-dom'
import { Button, ButtonGroup, ButtonGroupProps, ButtonProps, Input, InputProps } from '@chakra-ui/react'
import { ReactElement, useMemo, useState } from 'react'
import { generatePath, Link as RouterLink, useNavigate, useParams } from 'react-router-dom'
import { usePagination, useRoutedPagination } from './PaginationProvider'

export const Pagination = () => {
const { page, setPage, totalPages } = usePagination()
export type PaginationProps = ButtonGroupProps & {
maxButtons?: number | false
buttonProps?: ButtonProps
inputProps?: InputProps
}

const createButton = (page: number, currentPage: number, props: ButtonProps) => (
<Button key={page} isActive={currentPage === page} {...props}>
{page + 1}
</Button>
)

type EllipsisButtonProps = ButtonProps & {
gotoPage: (page: number) => void
inputProps?: InputProps
}

const EllipsisButton = ({ gotoPage, inputProps, ...rest }: EllipsisButtonProps) => {
const [ellipsisInput, setEllipsisInput] = useState(false)

const pages: ReactElement[] = useMemo(() => {
const pages: ReactElement[] = []
if (ellipsisInput) {
return (
<Input
placeholder='Page #'
width='50px'
{...inputProps}
onKeyDown={(e) => {
if (e.target instanceof HTMLInputElement && e.key === 'Enter') {
const pageNumber = Number(e.target.value)
gotoPage(pageNumber)
setEllipsisInput(false)
}
}}
onBlur={() => setEllipsisInput(false)}
autoFocus
/>
)
}

return (
<Button
as='a'
href='#goto-page'
{...rest}
onClick={(e) => {
e.preventDefault()
setEllipsisInput(true)
}}
>
...
</Button>
)
}

const usePaginationPages = (
currentPage: number,
totalPages: number | undefined,
maxButtons: number | undefined | false,
gotoPage: (page: number) => void,
createPageButton: (i: number) => ReactElement,
inputProps?: InputProps,
buttonProps?: ButtonProps
) => {
return useMemo(() => {
if (totalPages === undefined) return []

let pages: ReactElement[] = []

// Create an array of all page buttons
for (let i = 0; i < totalPages; i++) {
pages.push(
<Button key={i} onClick={() => setPage(i)} isActive={page === i}>
{i + 1}
</Button>
)
pages.push(createPageButton(i))
}
return pages
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, totalPages])

return <ButtonGroup isAttached>{pages.map((page) => page)}</ButtonGroup>
if (!maxButtons || totalPages <= maxButtons) {
return pages
}

const startEllipsis = (
<EllipsisButton key='start-ellipsis' gotoPage={gotoPage} inputProps={inputProps} {...buttonProps} />
)
const endEllipsis = (
<EllipsisButton key='end-ellipsis' gotoPage={gotoPage} inputProps={inputProps} {...buttonProps} />
)

// Add ellipsis and slice the array accordingly
const sideButtons = 2 // First and last page
const availableButtons = maxButtons - sideButtons // Buttons we can distribute around the current page

if (currentPage <= availableButtons / 2) {
// Near the start
return [...pages.slice(0, availableButtons), endEllipsis, pages[totalPages - 1]]
} else if (currentPage >= totalPages - 1 - availableButtons / 2) {
// Near the end
return [pages[0], startEllipsis, ...pages.slice(totalPages - availableButtons, totalPages)]
} else {
// In the middle
const startPage = currentPage - Math.floor((availableButtons - 1) / 2)
const endPage = currentPage + Math.floor(availableButtons / 2)
return [pages[0], startEllipsis, ...pages.slice(startPage, endPage - 1), endEllipsis, pages[totalPages - 1]]
}
}, [currentPage, totalPages, maxButtons, gotoPage])
}

export const RoutedPagination = () => {
export const Pagination = ({ maxButtons = 10, buttonProps, inputProps, ...rest }: PaginationProps) => {
const { page, setPage, totalPages } = usePagination()

const pages = usePaginationPages(
page,
totalPages,
maxButtons ? Math.max(5, maxButtons) : false,
(page) => {
if (page >= 0 && totalPages && page < totalPages) {
setPage(page)
}
},
(i) => createButton(i, page, { onClick: () => setPage(i), ...buttonProps })
)

return (
<ButtonGroup {...rest}>
{totalPages === undefined ? (
<>
<Button key='previous' onClick={() => setPage(page - 1)} isDisabled={page === 0} {...buttonProps}>
Previous
</Button>
<Button key='next' onClick={() => setPage(page + 1)} {...buttonProps}>
Next
</Button>
</>
) : (
pages
)}
</ButtonGroup>
)
}

export const RoutedPagination = ({ maxButtons = 10, buttonProps, ...rest }: PaginationProps) => {
const { path, totalPages } = useRoutedPagination()
const { page }: { page?: number } = useParams()
const { page, ...extraParams }: { page?: number } = useParams()
const navigate = useNavigate()

const p = Number(page) || 0
const p = Number(page) || 1

const pages: ReactElement[] = useMemo(() => {
const pages: ReactElement[] = []
for (let i = 0; i < totalPages; i++) {
pages.push(
<Button as={RouterLink} key={i} to={generatePath(path, { page: i })} isActive={p === i}>
{i + 1}
</Button>
)
}
return pages
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [p, totalPages])
const pages = usePaginationPages(
p,
totalPages,
maxButtons ? Math.max(5, maxButtons) : false,
(page) => {
if (page >= 0 && totalPages && page < totalPages) {
navigate(generatePath(path, { page: page, ...extraParams }))
}
},
(i) => (
<Button
as={RouterLink}
key={i}
to={generatePath(path, { page: i + 1, ...extraParams })}
isActive={p - 1 === i}
{...buttonProps}
>
{i + 1}
</Button>
)
)

return <ButtonGroup isAttached>{pages.map((page) => page)}</ButtonGroup>
return (
<ButtonGroup {...rest}>
{totalPages === undefined ? (
<>
<Button
key='previous'
onClick={() => navigate(generatePath(path, { page: p, ...extraParams }))}
isDisabled={p === 0}
{...buttonProps}
>
Previous
</Button>
<Button
key='next'
onClick={() => navigate(generatePath(path, { page: p + 2, ...extraParams }))}
{...buttonProps}
>
Next
</Button>
</>
) : (
pages
)}
</ButtonGroup>
)
}
2 changes: 1 addition & 1 deletion src/components/Pagination/PaginationProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createContext, PropsWithChildren, useContext, useState } from 'react'
export type PaginationContextProps = {
page: number
setPage: (page: number) => void
totalPages: number
totalPages?: number
}

export type RoutedPaginationContextProps = Omit<PaginationContextProps, 'setPage' | 'page'> & {
Expand Down
4 changes: 2 additions & 2 deletions src/queries/organizations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { useClient } from '@vocdoni/react-providers'
import { ExtendedSDKClient } from '@vocdoni/extended-sdk'
import { useClient } from '@vocdoni/react-providers'
import { IChainOrganizationCountResponse, IChainOrganizationListResponse } from '@vocdoni/sdk'

export const useOrganizationList = ({
Expand All @@ -14,7 +14,7 @@ export const useOrganizationList = ({
const { client } = useClient<ExtendedSDKClient>()
return useQuery({
queryKey: ['organizations', 'list', page, organizationId],
queryFn: () => client.organizationList(page, organizationId),
queryFn: () => client.organizationList(page - 1, organizationId),
...options,
})
}
Expand Down

2 comments on commit 3019238

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.