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:
@@ -10,15 +10,14 @@ type Params = {
|
||||
const getUsers = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'User');
|
||||
|
||||
const usersQuery = User
|
||||
.query()
|
||||
const usersQuery = User.query()
|
||||
.leftJoinRelated({
|
||||
role: true
|
||||
role: true,
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true
|
||||
role: true,
|
||||
})
|
||||
.orderBy('full_name', 'desc');
|
||||
.orderBy('full_name', 'asc');
|
||||
|
||||
return paginate(usersQuery, params.limit, params.offset);
|
||||
};
|
||||
|
@@ -341,6 +341,7 @@ type SamlAuthProvidersRoleMapping {
|
||||
type UserConnection {
|
||||
edges: [UserEdge]
|
||||
pageInfo: PageInfo
|
||||
totalCount: Int
|
||||
}
|
||||
|
||||
type UserEdge {
|
||||
|
@@ -21,6 +21,7 @@ const paginate = async (
|
||||
currentPage: Math.ceil(offset / limit + 1),
|
||||
totalPages: Math.ceil(count / limit),
|
||||
},
|
||||
totalCount: count,
|
||||
edges: records.map((record: Base) => ({
|
||||
node: record,
|
||||
})),
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -11,89 +11,132 @@ import Paper from '@mui/material/Paper';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import TableFooter from '@mui/material/TableFooter';
|
||||
|
||||
import DeleteUserButton from 'components/DeleteUserButton/index.ee';
|
||||
import ListLoader from 'components/ListLoader';
|
||||
import useUsers from 'hooks/useUsers';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import * as URLS from 'config/urls';
|
||||
import TablePaginationActions from './TablePaginationActions';
|
||||
import { TablePagination } from './style';
|
||||
|
||||
export default function UserList(): React.ReactElement {
|
||||
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 (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.fullName')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.fullName')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.email')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.email')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.role')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.role')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading && <ListLoader rowsNumber={3} columnsNumber={2} />}
|
||||
{!loading &&
|
||||
users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{user.fullName}</Typography>
|
||||
</TableCell>
|
||||
<TableCell component="th" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading && <ListLoader rowsNumber={3} columnsNumber={2} />}
|
||||
{!loading &&
|
||||
users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{user.fullName}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{user.email}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{user.email}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{user.role.name}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">
|
||||
{user.role.name}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.USER(user.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.USER(user.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
|
||||
<DeleteUserButton userId={user.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<DeleteUserButton userId={user.id} />
|
||||
</Stack>
|
||||
</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>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableFooter>
|
||||
)}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
12
packages/web/src/components/UserList/style.ts
Normal file
12
packages/web/src/components/UserList/style.ts
Normal 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,
|
||||
},
|
||||
}));
|
@@ -13,6 +13,7 @@ export const GET_USERS = gql`
|
||||
currentPage
|
||||
totalPages
|
||||
}
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
|
@@ -4,30 +4,37 @@ import { IUser } from '@automatisch/types';
|
||||
import { GET_USERS } from 'graphql/queries/get-users';
|
||||
|
||||
type Edge = {
|
||||
node: IUser
|
||||
}
|
||||
node: IUser;
|
||||
};
|
||||
|
||||
type QueryResponse = {
|
||||
getUsers: {
|
||||
pageInfo: {
|
||||
currentPage: 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, {
|
||||
variables: {
|
||||
limit: 100,
|
||||
offset: 0
|
||||
}
|
||||
variables: getLimitAndOffset(page, rowsPerPage),
|
||||
});
|
||||
const users = data?.getUsers.edges.map(({ node }) => node) || [];
|
||||
const pageInfo = data?.getUsers.pageInfo;
|
||||
const totalCount = data?.getUsers.totalCount;
|
||||
|
||||
return {
|
||||
users,
|
||||
loading
|
||||
pageInfo,
|
||||
totalCount,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user