Loading capif_frontend/src/pages/me/providers/index.js +15 −19 Changes for capif_frontend/src/pages/me/providers/index.js: 15 added lines, 19 removed lines. Original line number Diff line number Diff line Loading @@ -5,9 +5,6 @@ import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' import CircularProgress from '@mui/material/CircularProgress' import Alert from '@mui/material/Alert' import List from '@mui/material/List' import ListItem from '@mui/material/ListItem' import ListItemText from '@mui/material/ListItemText' import Typography from '@mui/material/Typography' import Button from '@mui/material/Button' import Dialog from '@mui/material/Dialog' Loading @@ -16,24 +13,26 @@ import DialogContent from '@mui/material/DialogContent' import DialogActions from '@mui/material/DialogActions' import TextField from '@mui/material/TextField' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getMyProviders, onboardProvider } from 'src/configs/apiService' import { errorMessage, retryServerErrorsOnly } from 'src/configs/queryHelpers' import Link from 'next/link' import ListItemButton from '@mui/material/ListItemButton' import { useAuth } from 'src/hooks/useAuth' import EntityTable from 'src/views/me/EntityTable' const MY_PROVIDERS_KEY = ['my-providers'] const ProviderList = () => { const { user } = useAuth() const { data, isPending, isError, error } = useQuery({ queryKey: MY_PROVIDERS_KEY, queryFn: getMyProviders, retry: retryServerErrorsOnly }) // Derived during render: `data` is undefined until the first response arrives. const providers = data?.providers ?? [] const rows = (data?.providers ?? []).map(provider => ({ id: provider.apiProvDomId, date: provider.onboardedAt })) if (isPending) { return ( Loading @@ -47,20 +46,17 @@ const ProviderList = () => { return <Alert severity='error'>{errorMessage(error, 'Could not load your providers.')}</Alert> } if (providers.length === 0) { if (rows.length === 0) { return <Typography>{"You don't have a provider yet."}</Typography> } return ( <List> {providers.map(id => ( <ListItem key={id} disablePadding> <ListItemButton component={Link} href={`/me/providers/${encodeURIComponent(id)}`}> <ListItemText primary={id} /> </ListItemButton> </ListItem> ))} </List> <EntityTable rows={rows} idLabel='Provider ID' owner={user?.username} hrefFor={id => `/me/providers/${encodeURIComponent(id)}`} /> ) } Loading capif_frontend/src/views/me/EntityTable.js 0 → 100644 +124 −0 Changes for capif_frontend/src/views/me/EntityTable.js: 124 added lines, 0 removed lines. Original line number Diff line number Diff line import { useState } from 'react' import Link from 'next/link' import { styled } from '@mui/material/styles' import Box from '@mui/material/Box' import Table from '@mui/material/Table' import TableBody from '@mui/material/TableBody' import TableCell from '@mui/material/TableCell' import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import TableSortLabel from '@mui/material/TableSortLabel' import TextField from '@mui/material/TextField' import Pagination from '@mui/material/Pagination' import Select from '@mui/material/Select' import MenuItem from '@mui/material/MenuItem' const LinkStyled = styled(Link)(({ theme }) => ({ textDecoration: 'none', color: theme.palette.primary.main })) const PAGE_SIZES = [10, 20, 30] // Null dates go last whichever way we sort. const compareDates = (a, b, ascending) => { if (!a.date && !b.date) return 0 if (!a.date) return 1 if (!b.date) return -1 const diff = new Date(a.date) - new Date(b.date) return ascending ? diff : -diff } const formatDate = iso => (iso ? new Date(iso).toLocaleString() : '—') const EntityTable = ({ rows, idLabel, owner, hrefFor }) => { const [search, setSearch] = useState('') const [sortAscending, setSortAscending] = useState(false) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) // Everything below is derived during render; only the controls above are state. const term = search.trim().toLowerCase() const filtered = term ? rows.filter(row => row.id.toLowerCase().includes(term)) : rows const sorted = [...filtered].sort((a, b) => compareDates(a, b, sortAscending)) // copy: never sort the query's data const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize)) const currentPage = Math.min(page, pageCount) // clamped, so no effect is needed to reset it const pageRows = sorted.slice((currentPage - 1) * pageSize, currentPage * pageSize) const handleSearchChange = event => { setSearch(event.target.value) setPage(1) } const handlePageSizeChange = event => { setPageSize(event.target.value) setPage(1) } return ( <> <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 4 }}> <TextField label={`Search by ${idLabel}`} variant='outlined' value={search} onChange={handleSearchChange} sx={{ width: 300 }} /> </Box> <TableContainer> <Table aria-label={`${idLabel} table`}> <TableHead> <TableRow> <TableCell>{idLabel}</TableCell> <TableCell>Owner</TableCell> <TableCell sortDirection={sortAscending ? 'asc' : 'desc'}> <TableSortLabel active direction={sortAscending ? 'asc' : 'desc'} onClick={() => setSortAscending(previous => !previous)} > Date </TableSortLabel> </TableCell> </TableRow> </TableHead> <TableBody> {pageRows.length === 0 ? ( // Only reachable when a search matches nothing: the page handles "no entities at all". <TableRow> <TableCell colSpan={3}>No matches.</TableCell> </TableRow> ) : ( pageRows.map(row => ( <TableRow key={row.id}> <TableCell> <LinkStyled href={hrefFor(row.id)}>{row.id}</LinkStyled> </TableCell> <TableCell>{owner || '—'}</TableCell> <TableCell>{formatDate(row.date)}</TableCell> </TableRow> )) )} </TableBody> </Table> </TableContainer> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 4 }}> <Pagination count={pageCount} page={currentPage} onChange={(_, value) => setPage(value)} /> <Select value={pageSize} onChange={handlePageSizeChange}> {PAGE_SIZES.map(size => ( <MenuItem key={size} value={size}> {size} per page </MenuItem> ))} </Select> </Box> </> ) } export default EntityTable No newline at end of file Loading
capif_frontend/src/pages/me/providers/index.js +15 −19 Changes for capif_frontend/src/pages/me/providers/index.js: 15 added lines, 19 removed lines. Original line number Diff line number Diff line Loading @@ -5,9 +5,6 @@ import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' import CircularProgress from '@mui/material/CircularProgress' import Alert from '@mui/material/Alert' import List from '@mui/material/List' import ListItem from '@mui/material/ListItem' import ListItemText from '@mui/material/ListItemText' import Typography from '@mui/material/Typography' import Button from '@mui/material/Button' import Dialog from '@mui/material/Dialog' Loading @@ -16,24 +13,26 @@ import DialogContent from '@mui/material/DialogContent' import DialogActions from '@mui/material/DialogActions' import TextField from '@mui/material/TextField' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getMyProviders, onboardProvider } from 'src/configs/apiService' import { errorMessage, retryServerErrorsOnly } from 'src/configs/queryHelpers' import Link from 'next/link' import ListItemButton from '@mui/material/ListItemButton' import { useAuth } from 'src/hooks/useAuth' import EntityTable from 'src/views/me/EntityTable' const MY_PROVIDERS_KEY = ['my-providers'] const ProviderList = () => { const { user } = useAuth() const { data, isPending, isError, error } = useQuery({ queryKey: MY_PROVIDERS_KEY, queryFn: getMyProviders, retry: retryServerErrorsOnly }) // Derived during render: `data` is undefined until the first response arrives. const providers = data?.providers ?? [] const rows = (data?.providers ?? []).map(provider => ({ id: provider.apiProvDomId, date: provider.onboardedAt })) if (isPending) { return ( Loading @@ -47,20 +46,17 @@ const ProviderList = () => { return <Alert severity='error'>{errorMessage(error, 'Could not load your providers.')}</Alert> } if (providers.length === 0) { if (rows.length === 0) { return <Typography>{"You don't have a provider yet."}</Typography> } return ( <List> {providers.map(id => ( <ListItem key={id} disablePadding> <ListItemButton component={Link} href={`/me/providers/${encodeURIComponent(id)}`}> <ListItemText primary={id} /> </ListItemButton> </ListItem> ))} </List> <EntityTable rows={rows} idLabel='Provider ID' owner={user?.username} hrefFor={id => `/me/providers/${encodeURIComponent(id)}`} /> ) } Loading
capif_frontend/src/views/me/EntityTable.js 0 → 100644 +124 −0 Changes for capif_frontend/src/views/me/EntityTable.js: 124 added lines, 0 removed lines. Original line number Diff line number Diff line import { useState } from 'react' import Link from 'next/link' import { styled } from '@mui/material/styles' import Box from '@mui/material/Box' import Table from '@mui/material/Table' import TableBody from '@mui/material/TableBody' import TableCell from '@mui/material/TableCell' import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import TableSortLabel from '@mui/material/TableSortLabel' import TextField from '@mui/material/TextField' import Pagination from '@mui/material/Pagination' import Select from '@mui/material/Select' import MenuItem from '@mui/material/MenuItem' const LinkStyled = styled(Link)(({ theme }) => ({ textDecoration: 'none', color: theme.palette.primary.main })) const PAGE_SIZES = [10, 20, 30] // Null dates go last whichever way we sort. const compareDates = (a, b, ascending) => { if (!a.date && !b.date) return 0 if (!a.date) return 1 if (!b.date) return -1 const diff = new Date(a.date) - new Date(b.date) return ascending ? diff : -diff } const formatDate = iso => (iso ? new Date(iso).toLocaleString() : '—') const EntityTable = ({ rows, idLabel, owner, hrefFor }) => { const [search, setSearch] = useState('') const [sortAscending, setSortAscending] = useState(false) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) // Everything below is derived during render; only the controls above are state. const term = search.trim().toLowerCase() const filtered = term ? rows.filter(row => row.id.toLowerCase().includes(term)) : rows const sorted = [...filtered].sort((a, b) => compareDates(a, b, sortAscending)) // copy: never sort the query's data const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize)) const currentPage = Math.min(page, pageCount) // clamped, so no effect is needed to reset it const pageRows = sorted.slice((currentPage - 1) * pageSize, currentPage * pageSize) const handleSearchChange = event => { setSearch(event.target.value) setPage(1) } const handlePageSizeChange = event => { setPageSize(event.target.value) setPage(1) } return ( <> <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 4 }}> <TextField label={`Search by ${idLabel}`} variant='outlined' value={search} onChange={handleSearchChange} sx={{ width: 300 }} /> </Box> <TableContainer> <Table aria-label={`${idLabel} table`}> <TableHead> <TableRow> <TableCell>{idLabel}</TableCell> <TableCell>Owner</TableCell> <TableCell sortDirection={sortAscending ? 'asc' : 'desc'}> <TableSortLabel active direction={sortAscending ? 'asc' : 'desc'} onClick={() => setSortAscending(previous => !previous)} > Date </TableSortLabel> </TableCell> </TableRow> </TableHead> <TableBody> {pageRows.length === 0 ? ( // Only reachable when a search matches nothing: the page handles "no entities at all". <TableRow> <TableCell colSpan={3}>No matches.</TableCell> </TableRow> ) : ( pageRows.map(row => ( <TableRow key={row.id}> <TableCell> <LinkStyled href={hrefFor(row.id)}>{row.id}</LinkStyled> </TableCell> <TableCell>{owner || '—'}</TableCell> <TableCell>{formatDate(row.date)}</TableCell> </TableRow> )) )} </TableBody> </Table> </TableContainer> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 4 }}> <Pagination count={pageCount} page={currentPage} onChange={(_, value) => setPage(value)} /> <Select value={pageSize} onChange={handlePageSizeChange}> {PAGE_SIZES.map(size => ( <MenuItem key={size} value={size}> {size} per page </MenuItem> ))} </Select> </Box> </> ) } export default EntityTable No newline at end of file