Commit 98073696 authored by Alex Kakiris's avatar Alex Kakiris
Browse files

Merge branch 'feat/provider-list-table' into 'dev'

Show user providers in a table

See merge request !6
parents d921522b f40458d4
Loading
Loading
Loading
Loading
+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
@@ -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'
@@ -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 (
@@ -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)}`}
    />
  )
}

+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