diff --git a/src/components/Accounts/List.tsx b/src/components/Accounts/List.tsx index 3284463b..7d8aaf4b 100644 --- a/src/components/Accounts/List.tsx +++ b/src/components/Accounts/List.tsx @@ -1,63 +1,55 @@ import { keepPreviousData } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' -import { generatePath, useNavigate, useParams } from 'react-router-dom' import { InputSearch } from '~components/Layout/Inputs' import { LoadingCards } from '~components/Layout/Loading' import { AccountCard } from '~components/Accounts/Card' -import { RoutedPaginationProvider, useRoutedPagination } from '~components/Pagination/PaginationProvider' +import { useRoutedPagination } from '~components/Pagination/PaginationProvider' import { RoutedPagination } from '~components/Pagination/RoutedPagination' -import { RoutePath } from '~constants' -import { useOrganizationCount, useOrganizationList } from '~queries/accounts' +import { useOrganizationList } from '~queries/accounts' import { ContentError, NoResultsError } from '~components/Layout/ContentError' +import { useRoutedPaginationQueryParams } from '~src/router/use-query-params' + +type FilterQueryParams = { + accountId?: string +} export const AccountsFilter = () => { const { t } = useTranslation() - const navigate = useNavigate() - const { query } = useParams<{ query?: string }>() + const { queryParams, setQueryParams } = useRoutedPaginationQueryParams() return ( { - navigate(generatePath(RoutePath.AccountsList, { page: '0', query: value as string })) + setQueryParams({ accountId: value }) }} debounceTime={500} - initialValue={query} + initialValue={queryParams.accountId} /> ) } -export const PaginatedAccountsList = () => { - return ( - - - - ) -} - export const AccountsList = () => { const { page }: { page?: number } = useRoutedPagination() - const { query }: { query?: string } = useParams() - const { data: count, isLoading: isLoadingCount } = useOrganizationCount() + const { queryParams } = useRoutedPaginationQueryParams() + const accountId = queryParams.accountId const { data: orgs, - isLoading: isLoadingOrgs, + isLoading, isFetching, isError, error, } = useOrganizationList({ params: { page, - organizationId: query, + organizationId: accountId, }, placeholderData: keepPreviousData, }) - const isLoading = isLoadingCount || isLoadingOrgs - - if (isLoading || (query && isFetching)) { + if (isLoading || (accountId && isFetching)) { return } diff --git a/src/components/Layout/ListPageLayout.tsx b/src/components/Layout/ListPageLayout.tsx index d920a40b..cc53af21 100644 --- a/src/components/Layout/ListPageLayout.tsx +++ b/src/components/Layout/ListPageLayout.tsx @@ -13,7 +13,12 @@ const ListPageLayout = ({ } & PropsWithChildren) => { return ( - + {title} @@ -21,7 +26,7 @@ const ListPageLayout = ({ {subtitle && {subtitle}} {rightComponent && ( - + {rightComponent} )} diff --git a/src/components/Layout/TopBar.tsx b/src/components/Layout/TopBar.tsx index 177cf26c..1b75a426 100644 --- a/src/components/Layout/TopBar.tsx +++ b/src/components/Layout/TopBar.tsx @@ -49,7 +49,7 @@ export const TopBar = () => { const links: HeaderLink[] = [ { name: t('links.accounts', { defaultValue: 'Accounts' }), - url: generatePath(RoutePath.AccountsList, { page: null, query: null }), + url: generatePath(RoutePath.AccountsList, { page: null }), }, { name: t('links.processes', { defaultValue: 'Processes' }), diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index e77486dd..e982fc0f 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -1,6 +1,6 @@ import { Button, ButtonGroup, ButtonGroupProps, ButtonProps, Input, InputProps, Text } from '@chakra-ui/react' import { ReactElement, useMemo, useState } from 'react' -import { generatePath, Link as RouterLink, useLocation, useNavigate, useParams } from 'react-router-dom' +import { Link as RouterLink } from 'react-router-dom' import { usePagination, useRoutedPagination } from './PaginationProvider' import { PaginationResponse } from '@vocdoni/sdk' import { Trans } from 'react-i18next' @@ -203,22 +203,16 @@ export const Pagination = ({ maxButtons = 10, buttonProps, inputProps, paginatio } export const RoutedPagination = ({ maxButtons = 10, buttonProps, pagination, ...rest }: PaginationProps) => { - const { path } = useRoutedPagination() - const { search } = useLocation() - const { page, ...extraParams }: { page?: number } = useParams() - const navigate = useNavigate() + const { getPathForPage, setPage } = useRoutedPagination() const totalPages = pagination.lastPage + 1 - const currentPage = pagination.currentPage - const _generatePath = (page: number) => generatePath(path, { page, ...extraParams }) + search - return ( navigate(_generatePath(page))} + goToPage={(page) => setPage(page)} createPageButton={(i) => ( - + )} currentPage={currentPage} totalPages={totalPages} diff --git a/src/components/Pagination/PaginationProvider.tsx b/src/components/Pagination/PaginationProvider.tsx index 52b8d20c..02261f36 100644 --- a/src/components/Pagination/PaginationProvider.tsx +++ b/src/components/Pagination/PaginationProvider.tsx @@ -1,5 +1,5 @@ -import { createContext, PropsWithChildren, useContext, useState } from 'react' -import { useParams } from 'react-router-dom' +import { createContext, PropsWithChildren, useCallback, useContext, useState } from 'react' +import { generatePath, useLocation, useNavigate, useParams } from 'react-router-dom' export type PaginationContextProps = { page: number @@ -8,6 +8,10 @@ export type PaginationContextProps = { export type RoutedPaginationContextProps = Omit & { path: string + // Util function that generates the path for a given page + // (it return the actual path with queryParams and other route params but changing the page) + getPathForPage: (page: number, queryParams?: string) => string + setPage: (page: number, queryParams?: string) => void } const PaginationContext = createContext(undefined) @@ -36,10 +40,22 @@ export type RoutedPaginationProviderProps = PaginationProviderProps & { } export const RoutedPaginationProvider = ({ path, ...rest }: PropsWithChildren) => { - const { page }: { page?: number } = useParams() + const { search } = useLocation() + const { page, ...extraParams }: { page?: number } = useParams() const p = page && page > 0 ? page - 1 : 0 - return + const navigate = useNavigate() + + const getPathForPage = (page: number, queryParams?: string) => { + const p = queryParams || search + return generatePath(path, { page, ...extraParams }) + p + } + + const setPage = (page: number, queryParams?: string) => { + navigate(getPathForPage(page, queryParams)) + } + + return } export const PaginationProvider = ({ ...rest }: PropsWithChildren) => { diff --git a/src/components/Process/ProcessList.tsx b/src/components/Process/ProcessList.tsx index c650c3dc..5dc23365 100644 --- a/src/components/Process/ProcessList.tsx +++ b/src/components/Process/ProcessList.tsx @@ -1,7 +1,10 @@ import { + Box, Button, Checkbox, Flex, + FormControl, + FormErrorMessage, IconButton, Popover, PopoverBody, @@ -14,21 +17,22 @@ import { Trans, useTranslation } from 'react-i18next' import { InputSearch } from '~components/Layout/Inputs' import { LoadingCards } from '~components/Layout/Loading' import { ContentError, NoResultsError } from '~components/Layout/ContentError' -import { RoutedPaginationProvider, useRoutedPagination } from '~components/Pagination/PaginationProvider' +import { useRoutedPagination } from '~components/Pagination/PaginationProvider' import { RoutedPagination } from '~components/Pagination/RoutedPagination' -import { RoutePath } from '~constants' import { useProcessList } from '~queries/processes' -import useQueryParams from '~src/router/use-query-params' import { isEmpty } from '~utils/objects' import { ElectionCard } from './Card' import { LuListFilter } from 'react-icons/lu' +import { isValidPartialProcessId } from '~utils/strings' +import { useState } from 'react' +import { useRoutedPaginationQueryParams } from '~src/router/use-query-params' type FilterQueryParams = { [K in keyof Omit]: string } const PopoverFilter = () => { - const { queryParams, setQueryParams } = useQueryParams() + const { queryParams, setQueryParams } = useRoutedPaginationQueryParams() return ( @@ -65,29 +69,38 @@ const PopoverFilter = () => { export const ProcessSearchBox = () => { const { t } = useTranslation() - const { queryParams, setQueryParams } = useQueryParams() + const [isInvalid, setIsInvalid] = useState(false) + const { queryParams, setQueryParams } = useRoutedPaginationQueryParams() return ( - - - - { - setQueryParams({ ...queryParams, electionId: value }) - }} - initialValue={queryParams.electionId} - debounceTime={500} - /> + + + + + { + const isInvalid = value !== '' && !isValidPartialProcessId(value) + setIsInvalid(isInvalid) + if (!isInvalid) { + setQueryParams({ ...queryParams, electionId: value }) + } + }} + initialValue={queryParams.electionId} + debounceTime={500} + isInvalid={isInvalid} + /> + {isInvalid && Not valid partial process id} + - + ) } export const ProcessByTypeFilter = () => { const { t } = useTranslation() - const { queryParams, setQueryParams } = useQueryParams() + const { queryParams, setQueryParams } = useRoutedPaginationQueryParams() const currentStatus = queryParams.status @@ -126,17 +139,9 @@ export const ProcessByTypeFilter = () => { ) } -export const PaginatedProcessList = () => { - return ( - - - - ) -} - -const ProcessList = () => { +export const ProcessList = () => { const { page }: { page?: number } = useRoutedPagination() - const { queryParams: processFilters } = useQueryParams() + const { queryParams: processFilters } = useRoutedPaginationQueryParams() const { data, isLoading, isFetching, isError, error } = useProcessList({ filters: { diff --git a/src/constants.ts b/src/constants.ts index 18fd800a..44f793c6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -24,7 +24,7 @@ export enum RoutePath { BlocksList = '/blocks/:page?', Envelope = '/envelope/:verifier/:tab?', Account = '/account/:pid/:tab?/:page?', - AccountsList = '/accounts/:page?/:query?', + AccountsList = '/accounts/:page?', Process = '/process/:pid/:tab?', ProcessesList = '/processes/:page?', Transaction = '/transactions/:block/:index/:tab?', @@ -42,7 +42,7 @@ export enum OldRoutePath { OrganizationDetails = '/organizations/show/#/', ProcessDetails = '/processes/show/#/', TransactionDetails = '/transactions/show/#/', - Verify = '/verify', + Verify = '/verify/#/', Stats = '/stats', OrganizationsList = '/organizations', Organization = '/organization', diff --git a/src/pages/accounts.tsx b/src/pages/accounts.tsx index 1af1d9de..5b30eb2d 100644 --- a/src/pages/accounts.tsx +++ b/src/pages/accounts.tsx @@ -1,8 +1,9 @@ -import { AccountsFilter, PaginatedAccountsList } from '~components/Accounts/List' +import { AccountsFilter, AccountsList } from '~components/Accounts/List' import ListPageLayout from '~components/Layout/ListPageLayout' import { useOrganizationCount } from '~queries/accounts' import { useTranslation } from 'react-i18next' -import { RefreshIntervalPagination } from '~constants' +import { RefreshIntervalPagination, RoutePath } from '~constants' +import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider' const OrganizationList = () => { const { t } = useTranslation() @@ -13,9 +14,11 @@ const OrganizationList = () => { const subtitle = !isLoading ? t('accounts.accounts_count', { count: orgsCount || 0 }) : '' return ( - }> - - + + }> + + + ) } diff --git a/src/pages/processes.tsx b/src/pages/processes.tsx index 038661a0..fc11a036 100644 --- a/src/pages/processes.tsx +++ b/src/pages/processes.tsx @@ -1,8 +1,13 @@ import ListPageLayout from '~components/Layout/ListPageLayout' import { useTranslation } from 'react-i18next' import { useProcessesCount } from '~queries/processes' -import { PaginatedProcessList, ProcessByTypeFilter, ProcessSearchBox } from '~components/Process/ProcessList' -import { RefreshIntervalPagination } from '~constants' +import { + ProcessList as PaginatedProcessList, + ProcessByTypeFilter, + ProcessSearchBox, +} from '~components/Process/ProcessList' +import { RefreshIntervalPagination, RoutePath } from '~constants' +import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider' const ProcessList = () => { const { t } = useTranslation() @@ -13,10 +18,12 @@ const ProcessList = () => { const subtitle = !isLoading ? t('process.process_count', { count: data || 0 }) : '' return ( - }> - - - + + }> + + + + ) } diff --git a/src/router/use-query-params.ts b/src/router/use-query-params.ts index 64cf2022..fe6d7669 100644 --- a/src/router/use-query-params.ts +++ b/src/router/use-query-params.ts @@ -1,7 +1,10 @@ import { useMemo } from 'react' import { useLocation, useNavigate } from 'react-router-dom' +import { useRoutedPagination } from '~components/Pagination/PaginationProvider' -const useQueryParams = >() => { +export type QueryParamsType = Record + +const useQueryParams = () => { const { search } = useLocation() const navigate = useNavigate() const location = useLocation() @@ -16,7 +19,7 @@ const useQueryParams = >() => { return queryObj }, [search]) - const setQueryParams = (newParams: Partial) => { + const getNewParams = (newParams: Partial) => { const params = new URLSearchParams(search) for (const key in newParams) { if (newParams[key]) { @@ -25,7 +28,25 @@ const useQueryParams = >() => { params.delete(key) } } - navigate(`${location.pathname}?${params.toString()}`) + return params + } + + const setQueryParams = (newParams: Partial) => { + navigate(`${location.pathname}?${getNewParams(newParams).toString()}`) + } + + return { queryParams, setQueryParams, getNewParams } +} + +/** + * Hook to manage query params and pagination. + * When a query param is set, the page is reset to 1. + */ +export const useRoutedPaginationQueryParams = () => { + const { queryParams, getNewParams } = useQueryParams() + const { setPage } = useRoutedPagination() + const setQueryParams = (filters: Partial) => { + setPage(1, `?${getNewParams(filters).toString()}`) } return { queryParams, setQueryParams }