feat(user-list): add pagination (#1219)

* feat(user-list): add pagination

* feat: add actual total count in getUsers

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
This commit is contained in:
Rıdvan Akca
2023-08-21 22:15:07 +03:00
committed by GitHub
parent cb06d3b0ae
commit 163aca6179
8 changed files with 235 additions and 82 deletions

View File

@@ -10,15 +10,14 @@ type Params = {
const getUsers = async (_parent: unknown, params: Params, context: Context) => { const getUsers = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('read', 'User'); context.currentUser.can('read', 'User');
const usersQuery = User const usersQuery = User.query()
.query()
.leftJoinRelated({ .leftJoinRelated({
role: true role: true,
}) })
.withGraphFetched({ .withGraphFetched({
role: true role: true,
}) })
.orderBy('full_name', 'desc'); .orderBy('full_name', 'asc');
return paginate(usersQuery, params.limit, params.offset); return paginate(usersQuery, params.limit, params.offset);
}; };

View File

@@ -341,6 +341,7 @@ type SamlAuthProvidersRoleMapping {
type UserConnection { type UserConnection {
edges: [UserEdge] edges: [UserEdge]
pageInfo: PageInfo pageInfo: PageInfo
totalCount: Int
} }
type UserEdge { type UserEdge {

View File

@@ -21,6 +21,7 @@ const paginate = async (
currentPage: Math.ceil(offset / limit + 1), currentPage: Math.ceil(offset / limit + 1),
totalPages: Math.ceil(count / limit), totalPages: Math.ceil(count / limit),
}, },
totalCount: count,
edges: records.map((record: Base) => ({ edges: records.map((record: Base) => ({
node: record, node: record,
})), })),

View File

@@ -0,0 +1,89 @@
import { useTheme } from '@mui/material';
import IconButton from '@mui/material/IconButton';
import FirstPageIcon from '@mui/icons-material/FirstPage';
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight';
import LastPageIcon from '@mui/icons-material/LastPage';
import Box from '@mui/material/Box';
interface TablePaginationActionsProps {
count: number;
page: number;
rowsPerPage: number;
onPageChange: (
event: React.MouseEvent<HTMLButtonElement>,
newPage: number
) => void;
}
export default function TablePaginationActions(
props: TablePaginationActionsProps
) {
const theme = useTheme();
const { count, page, rowsPerPage, onPageChange } = props;
const handleFirstPageButtonClick = (
event: React.MouseEvent<HTMLButtonElement>
) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (
event: React.MouseEvent<HTMLButtonElement>
) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (
event: React.MouseEvent<HTMLButtonElement>
) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (
event: React.MouseEvent<HTMLButtonElement>
) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<Box sx={{ flexShrink: 0, ml: 2.5 }}>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
{theme.direction === 'rtl' ? <LastPageIcon /> : <FirstPageIcon />}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
{theme.direction === 'rtl' ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
{theme.direction === 'rtl' ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
{theme.direction === 'rtl' ? <FirstPageIcon /> : <LastPageIcon />}
</IconButton>
</Box>
);
}

View File

@@ -11,89 +11,132 @@ import Paper from '@mui/material/Paper';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import TableFooter from '@mui/material/TableFooter';
import DeleteUserButton from 'components/DeleteUserButton/index.ee'; import DeleteUserButton from 'components/DeleteUserButton/index.ee';
import ListLoader from 'components/ListLoader'; import ListLoader from 'components/ListLoader';
import useUsers from 'hooks/useUsers'; import useUsers from 'hooks/useUsers';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import TablePaginationActions from './TablePaginationActions';
import { TablePagination } from './style';
export default function UserList(): React.ReactElement { export default function UserList(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { users, loading } = useUsers(); const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const {
users,
pageInfo,
totalCount,
loading,
} = useUsers(page, rowsPerPage);
const handleChangePage = (
event: React.MouseEvent<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setRowsPerPage(+event.target.value);
setPage(0);
};
return ( return (
<TableContainer component={Paper}> <>
<Table> <TableContainer component={Paper}>
<TableHead> <Table>
<TableRow> <TableHead>
<TableCell component="th"> <TableRow>
<Typography <TableCell component="th">
variant="subtitle1" <Typography
sx={{ color: 'text.secondary', fontWeight: 700 }} variant="subtitle1"
> sx={{ color: 'text.secondary', fontWeight: 700 }}
{formatMessage('userList.fullName')} >
</Typography> {formatMessage('userList.fullName')}
</TableCell> </Typography>
</TableCell>
<TableCell component="th"> <TableCell component="th">
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ color: 'text.secondary', fontWeight: 700 }} sx={{ color: 'text.secondary', fontWeight: 700 }}
> >
{formatMessage('userList.email')} {formatMessage('userList.email')}
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell component="th"> <TableCell component="th">
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ color: 'text.secondary', fontWeight: 700 }} sx={{ color: 'text.secondary', fontWeight: 700 }}
> >
{formatMessage('userList.role')} {formatMessage('userList.role')}
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell component="th" /> <TableCell component="th" />
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{loading && <ListLoader rowsNumber={3} columnsNumber={2} />} {loading && <ListLoader rowsNumber={3} columnsNumber={2} />}
{!loading && {!loading &&
users.map((user) => ( users.map((user) => (
<TableRow <TableRow
key={user.id} key={user.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
> >
<TableCell scope="row"> <TableCell scope="row">
<Typography variant="subtitle2">{user.fullName}</Typography> <Typography variant="subtitle2">{user.fullName}</Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Typography variant="subtitle2">{user.email}</Typography> <Typography variant="subtitle2">{user.email}</Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Typography variant="subtitle2">{user.role.name}</Typography> <Typography variant="subtitle2">
</TableCell> {user.role.name}
</Typography>
</TableCell>
<TableCell> <TableCell>
<Stack direction="row" gap={1} justifyContent="right"> <Stack direction="row" gap={1} justifyContent="right">
<IconButton <IconButton
size="small" size="small"
component={Link} component={Link}
to={URLS.USER(user.id)} to={URLS.USER(user.id)}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<DeleteUserButton userId={user.id} /> <DeleteUserButton userId={user.id} />
</Stack> </Stack>
</TableCell> </TableCell>
</TableRow>
))}
</TableBody>
{totalCount && (
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
page={page}
count={totalCount}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
/>
</TableRow> </TableRow>
))} </TableFooter>
</TableBody> )}
</Table> </Table>
</TableContainer> </TableContainer>
</>
); );
} }

View File

@@ -0,0 +1,12 @@
import { styled } from '@mui/material/styles';
import MuiTablePagination, {
tablePaginationClasses,
} from '@mui/material/TablePagination';
export const TablePagination = styled(MuiTablePagination)(() => ({
[`& .${tablePaginationClasses.selectLabel}, & .${tablePaginationClasses.displayedRows}`]:
{
fontWeight: 400,
fontSize: 14,
},
}));

View File

@@ -13,6 +13,7 @@ export const GET_USERS = gql`
currentPage currentPage
totalPages totalPages
} }
totalCount
edges { edges {
node { node {
id id

View File

@@ -4,30 +4,37 @@ import { IUser } from '@automatisch/types';
import { GET_USERS } from 'graphql/queries/get-users'; import { GET_USERS } from 'graphql/queries/get-users';
type Edge = { type Edge = {
node: IUser node: IUser;
} };
type QueryResponse = { type QueryResponse = {
getUsers: { getUsers: {
pageInfo: { pageInfo: {
currentPage: number; currentPage: number;
totalPages: number; totalPages: number;
} };
edges: Edge[] totalCount: number;
} edges: Edge[];
} };
};
export default function useUsers() { const getLimitAndOffset = (page: number, rowsPerPage: number) => ({
limit: rowsPerPage,
offset: page * rowsPerPage,
});
export default function useUsers(page: number, rowsPerPage: number) {
const { data, loading } = useQuery<QueryResponse>(GET_USERS, { const { data, loading } = useQuery<QueryResponse>(GET_USERS, {
variables: { variables: getLimitAndOffset(page, rowsPerPage),
limit: 100,
offset: 0
}
}); });
const users = data?.getUsers.edges.map(({ node }) => node) || []; const users = data?.getUsers.edges.map(({ node }) => node) || [];
const pageInfo = data?.getUsers.pageInfo;
const totalCount = data?.getUsers.totalCount;
return { return {
users, users,
loading pageInfo,
totalCount,
loading,
}; };
} }