feat(auth): add loading state for user and role management
This commit is contained in:
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
47
packages/web/src/components/ListLoader/index.tsx
Normal file
47
packages/web/src/components/ListLoader/index.tsx
Normal 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;
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user