feat(auth): add loading state for user and role management

This commit is contained in:
Rıdvan Akca
2023-07-31 17:56:31 +03:00
parent 9e64af4793
commit d3bc3a796b
7 changed files with 204 additions and 145 deletions

View File

@@ -9,7 +9,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
type DeleteRoleButtonProps = { type DeleteRoleButtonProps = {
roleId: string; roleId: string;
} };
export default function DeleteRoleButton(props: DeleteRoleButtonProps) { export default function DeleteRoleButton(props: DeleteRoleButtonProps) {
const { roleId } = props; const { roleId } = props;

View File

@@ -9,7 +9,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
type DeleteUserButtonProps = { type DeleteUserButtonProps = {
userId: string; userId: string;
} };
export default function DeleteUserButton(props: DeleteUserButtonProps) { export default function DeleteUserButton(props: DeleteUserButtonProps) {
const { userId } = props; const { userId } = props;

View File

@@ -0,0 +1,47 @@
import {
IconButton,
Skeleton,
Stack,
TableCell,
TableRow,
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
type ListLoaderProps = {
rowsNumber: number;
cellNumber: number;
};
const ListLoader = ({ rowsNumber, cellNumber }: ListLoaderProps) => {
return (
<>
{[...Array(rowsNumber)].map((row, index) => (
<TableRow
key={index}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
{[...Array(cellNumber)].map((cell, index) => (
<TableCell key={index} scope="row">
<Skeleton />
</TableCell>
))}
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton size="small">
<EditIcon />
</IconButton>
<IconButton size="small">
<DeleteIcon />
</IconButton>
</Stack>
</TableCell>
</TableRow>
))}
</>
);
};
export default ListLoader;

View File

@@ -13,15 +13,15 @@ import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteRoleButton from 'components/DeleteRoleButton/index.ee'; import DeleteRoleButton from 'components/DeleteRoleButton/index.ee';
import ListLoader from 'components/ListLoader';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useRoles from 'hooks/useRoles.ee'; import useRoles from 'hooks/useRoles.ee';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
// TODO: introduce interaction feedback upon deletion (successful + failure) // TODO: introduce interaction feedback upon deletion (successful + failure)
// TODO: introduce loading bar
export default function RoleList(): React.ReactElement { export default function RoleList(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { roles } = useRoles(); const { roles, loading } = useRoles();
return ( return (
<TableContainer component={Paper}> <TableContainer component={Paper}>
@@ -50,42 +50,40 @@ export default function RoleList(): React.ReactElement {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{roles.map((role) => ( {loading ? (
<TableRow <ListLoader rowsNumber={3} cellNumber={2} />
key={role.id} ) : (
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} roles.map((role) => (
> <TableRow
<TableCell scope="row"> key={role.id}
<Typography sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
variant="subtitle2" >
> <TableCell scope="row">
{role.name} <Typography variant="subtitle2">{role.name}</Typography>
</Typography> </TableCell>
</TableCell>
<TableCell scope="row"> <TableCell scope="row">
<Typography <Typography variant="subtitle2">
variant="subtitle2" {role.description}
> </Typography>
{role.description} </TableCell>
</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.ROLE(role.id)} to={URLS.ROLE(role.id)}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<DeleteRoleButton roleId={role.id} /> <DeleteRoleButton roleId={role.id} />
</Stack> </Stack>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>

View File

@@ -13,13 +13,13 @@ import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteUserButton from 'components/DeleteUserButton/index.ee'; import DeleteUserButton from 'components/DeleteUserButton/index.ee';
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';
// TODO: introduce translation entries // TODO: introduce translation entries
// TODO: introduce interaction feedback upon deletion (successful + failure) // TODO: introduce interaction feedback upon deletion (successful + failure)
// TODO: introduce loading bar
export default function UserList(): React.ReactElement { export default function UserList(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { users, loading } = useUsers(); const { users, loading } = useUsers();
@@ -51,42 +51,38 @@ export default function UserList(): React.ReactElement {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{users.map((user) => ( {loading ? (
<TableRow <ListLoader rowsNumber={3} cellNumber={2} />
key={user.id} ) : (
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} users.map((user) => (
> <TableRow
<TableCell scope="row"> key={user.id}
<Typography sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
variant="subtitle2" >
> <TableCell scope="row">
{user.fullName} <Typography variant="subtitle2">{user.fullName}</Typography>
</Typography> </TableCell>
</TableCell>
<TableCell> <TableCell>
<Typography <Typography variant="subtitle2">{user.email}</Typography>
variant="subtitle2" </TableCell>
>
{user.email}
</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> </TableRow>
))} ))
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>

View File

@@ -13,13 +13,13 @@ import PageTitle from 'components/PageTitle';
import Form from 'components/Form'; import Form from 'components/Form';
import TextField from 'components/TextField'; import TextField from 'components/TextField';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import { Skeleton } from '@mui/material';
type EditRoleParams = { type EditRoleParams = {
roleId: string; roleId: string;
} };
// TODO: introduce interaction feedback upon deletion (successful + failure) // TODO: introduce interaction feedback upon deletion (successful + failure)
// TODO: introduce loading bar
export default function EditRole(): React.ReactElement { export default function EditRole(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [updateRole, { loading }] = useMutation(UPDATE_ROLE); const [updateRole, { loading }] = useMutation(UPDATE_ROLE);
@@ -33,13 +33,11 @@ export default function EditRole(): React.ReactElement {
id: roleId, id: roleId,
name: roleData.name, name: roleData.name,
description: roleData.description, description: roleData.description,
} },
} },
}); });
}; };
if (roleLoading) return <React.Fragment />;
return ( return (
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}> <Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
<Grid container item xs={12} sm={9} md={8} lg={6}> <Grid container item xs={12} sm={9} md={8} lg={6}>
@@ -48,33 +46,41 @@ export default function EditRole(): React.ReactElement {
</Grid> </Grid>
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}> <Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
<Form defaultValues={role} onSubmit={handleRoleUpdate}> {roleLoading ? (
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
<TextField <Skeleton variant="rounded" height={55} />
required={true} <Skeleton variant="rounded" height={55} />
name="name" <Skeleton variant="rounded" height={45} />
label={formatMessage('roleForm.name')}
fullWidth
/>
<TextField
required={true}
name="description"
label={formatMessage('roleForm.description')}
fullWidth
/>
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={loading}
>
{formatMessage('editRole.submit')}
</LoadingButton>
</Stack> </Stack>
</Form> ) : (
<Form defaultValues={role} onSubmit={handleRoleUpdate}>
<Stack direction="column" gap={2}>
<TextField
required={true}
name="name"
label={formatMessage('roleForm.name')}
fullWidth
/>
<TextField
required={true}
name="description"
label={formatMessage('roleForm.description')}
fullWidth
/>
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={loading}
>
{formatMessage('editRole.submit')}
</LoadingButton>
</Stack>
</Form>
)}
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>

View File

@@ -16,17 +16,17 @@ import Form from 'components/Form';
import ControlledAutocomplete from 'components/ControlledAutocomplete'; import ControlledAutocomplete from 'components/ControlledAutocomplete';
import TextField from 'components/TextField'; import TextField from 'components/TextField';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import { Skeleton } from '@mui/material';
type EditUserParams = { type EditUserParams = {
userId: string; userId: string;
} };
function generateRoleOptions(roles: IRole[]) { function generateRoleOptions(roles: IRole[]) {
return roles?.map(({ name: label, id: value }) => ({ label, value })); return roles?.map(({ name: label, id: value }) => ({ label, value }));
} }
// TODO: introduce interaction feedback upon deletion (successful + failure) // TODO: introduce interaction feedback upon deletion (successful + failure)
// TODO: introduce loading bar
export default function EditUser(): React.ReactElement { export default function EditUser(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [updateUser, { loading }] = useMutation(UPDATE_USER); const [updateUser, { loading }] = useMutation(UPDATE_USER);
@@ -42,15 +42,13 @@ export default function EditUser(): React.ReactElement {
fullName: userDataToUpdate.fullName, fullName: userDataToUpdate.fullName,
email: userDataToUpdate.email, email: userDataToUpdate.email,
role: { role: {
id: userDataToUpdate.role?.id id: userDataToUpdate.role?.id,
} },
} },
} },
}); });
}; };
if (userLoading) return <React.Fragment />;
return ( return (
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}> <Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
<Grid container item xs={12} sm={9} md={8} lg={6}> <Grid container item xs={12} sm={9} md={8} lg={6}>
@@ -59,43 +57,57 @@ export default function EditUser(): React.ReactElement {
</Grid> </Grid>
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}> <Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
<Form defaultValues={user} onSubmit={handleUserUpdate}> {userLoading ? (
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
<TextField <Skeleton variant="rounded" height={55} />
required={true} <Skeleton variant="rounded" height={55} />
name="fullName" <Skeleton variant="rounded" height={55} />
label={formatMessage('userForm.fullName')} <Skeleton variant="rounded" height={45} />
fullWidth
/>
<TextField
required={true}
name="email"
label={formatMessage('userForm.email')}
fullWidth
/>
<ControlledAutocomplete
name="role.id"
fullWidth
disablePortal
disableClearable={true}
options={generateRoleOptions(roles)}
renderInput={(params) => <MuiTextField {...params} label={formatMessage('userForm.role')} />}
loading={rolesLoading}
/>
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={loading}
>
{formatMessage('editUser.submit')}
</LoadingButton>
</Stack> </Stack>
</Form> ) : (
<Form defaultValues={user} onSubmit={handleUserUpdate}>
<Stack direction="column" gap={2}>
<TextField
required={true}
name="fullName"
label={formatMessage('userForm.fullName')}
fullWidth
/>
<TextField
required={true}
name="email"
label={formatMessage('userForm.email')}
fullWidth
/>
<ControlledAutocomplete
name="role.id"
fullWidth
disablePortal
disableClearable={true}
options={generateRoleOptions(roles)}
renderInput={(params) => (
<MuiTextField
{...params}
label={formatMessage('userForm.role')}
/>
)}
loading={rolesLoading}
/>
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={loading}
>
{formatMessage('editUser.submit')}
</LoadingButton>
</Stack>
</Form>
)}
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>