UsersPage: add FormDialog

This commit is contained in:
Dawid Wysokiński 2021-03-12 10:45:12 +01:00
parent ff3636542c
commit 2f73ef1b62
9 changed files with 274 additions and 23 deletions

View File

@ -29,6 +29,7 @@
"react-use": "^17.2.1",
"typescript": "^4.1.2",
"use-query-params": "^1.2.0",
"validator": "^13.5.2",
"web-vitals": "^1.0.1"
},
"scripts": {
@ -64,6 +65,7 @@
"@types/apollo-upload-client": "^14.1.0",
"@types/lodash": "^4.14.168",
"@types/react-router-dom": "^5.1.7",
"@types/validator": "^13.1.3",
"babel-plugin-transform-imports": "^2.0.0",
"babel-plugin-transform-modules": "^0.1.1",
"customize-cra": "^1.0.0",

View File

@ -1,20 +1,32 @@
import useUsers from './UsersPage.useUsers';
import { useState } from 'react';
import {
NumberParam,
StringParam,
useQueryParams,
withDefault,
StringParam,
} from 'use-query-params';
import SortParam, { decodeSort } from 'libs/serialize-query-params/SortParam';
import useUsers from './UsersPage.useUsers';
import { MutationCreateUserArgs, UserInput } from 'libs/graphql/types';
import { validateRowsPerPage } from 'common/Table/helpers';
import { DEFAULT_SORT, COLUMNS } from './constants';
import { COLUMNS, DEFAULT_SORT, DialogType } from './constants';
import { Container, IconButton, Paper } from '@material-ui/core';
import { Edit as EditIcon } from '@material-ui/icons';
import Table from 'common/Table/Table';
import TableToolbar from './components/TableToolbar/TableToolbar';
import FormDialog from './components/FormDialog/FormDialog';
import { ApolloError, useMutation } from '@apollo/client';
import { MUTATION_CREATE_USER } from './mutations';
import { useSnackbar } from 'material-ui-snackbar-provider';
const UsersPage = () => {
const [createUserMutation] = useMutation<any, MutationCreateUserArgs>(
MUTATION_CREATE_USER,
{ ignoreResults: true }
);
const [dialogType, setDialogType] = useState<DialogType>(DialogType.None);
const snackbar = useSnackbar();
const [{ page, sort, search, ...rest }, setQueryParams] = useQueryParams({
limit: NumberParam,
page: withDefault(NumberParam, 0),
@ -22,19 +34,34 @@ const UsersPage = () => {
search: withDefault(StringParam, ''),
});
const limit = validateRowsPerPage(rest.limit);
const { users, total, loading } = useUsers(
const { users, total, loading, refetch } = useUsers(
page,
limit,
sort.toString(),
search
);
console.log(users);
const handleCreateUser = async (input: UserInput) => {
try {
await createUserMutation({ variables: { input } });
await refetch();
return true;
} catch (e) {
snackbar.showMessage(
e instanceof ApolloError && e.graphQLErrors.length > 0
? e.graphQLErrors[0].message
: e.message
);
}
return false;
};
return (
<Container>
<Paper>
<TableToolbar
search={search}
onClickCreateUser={() => setDialogType(DialogType.Create)}
onChangeSearchValue={val => {
setQueryParams({ page: 0, search: val });
}}
@ -77,6 +104,13 @@ const UsersPage = () => {
}}
/>
</Paper>
<FormDialog
open={
dialogType === DialogType.Create || dialogType === DialogType.Edit
}
onSubmit={handleCreateUser}
onClose={() => setDialogType(DialogType.None)}
/>
</Container>
);
};

View File

@ -8,23 +8,23 @@ const useUsers = (
sort: string,
search: string
) => {
const { data, loading } = useQuery<Pick<Query, 'users'>, QueryUsersArgs>(
QUERY_USERS,
{
fetchPolicy: 'cache-and-network',
variables: {
offset: page * limit,
sort: [sort],
limit,
filter: {
or: {
displayNameIEQ: '%' + search + '%',
emailIEQ: '%' + search + '%',
},
const { data, loading, refetch } = useQuery<
Pick<Query, 'users'>,
QueryUsersArgs
>(QUERY_USERS, {
fetchPolicy: 'cache-and-network',
variables: {
offset: page * limit,
sort: [sort],
limit,
filter: {
or: {
displayNameIEQ: '%' + search + '%',
emailIEQ: '%' + search + '%',
},
},
}
);
},
});
return {
users: data?.users.items ?? [],
@ -32,6 +32,7 @@ const useUsers = (
return this.users.length === 0 && loading;
},
total: data?.users.total ?? 0,
refetch,
};
};

View File

@ -0,0 +1,181 @@
import { useForm } from 'react-hook-form';
import { pick } from 'lodash';
import isEmail from 'validator/es/lib/isEmail';
import { formatRole } from '../../utils';
import {
MAX_DISPLAY_NAME_LENGTH,
MAX_PASSWORD_LENGTH,
MIN_DISPLAY_NAME_LENGTH,
MIN_PASSWORD_LENGTH,
} from './constants';
import { Role, UserInput } from 'libs/graphql/types';
import { makeStyles } from '@material-ui/core/styles';
import {
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogProps,
DialogTitle,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
Radio,
RadioGroup,
TextField,
} from '@material-ui/core';
import { useState } from 'react';
export interface FormDialogProps extends Pick<DialogProps, 'open'> {
user?: UserInput;
onClose: () => void;
onSubmit: (input: UserInput) => Promise<boolean> | boolean;
}
const FormDialog = ({ open, onClose, user, onSubmit }: FormDialogProps) => {
const editMode = Boolean(user);
const { register, handleSubmit, errors } = useForm<UserInput>({
defaultValues: user,
});
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const classes = useStyles();
const _onSubmit = async (data: UserInput) => {
setIsSubmitting(true);
const filtered = editMode
? pick(
data,
Object.keys(data).filter(key => data[key as keyof UserInput])
)
: data;
const success = await onSubmit(filtered);
if (success) {
onClose();
} else {
setIsSubmitting(false);
}
};
return (
<Dialog
open={open}
onClose={isSubmitting ? undefined : onClose}
fullWidth
maxWidth="xs"
>
<form onSubmit={handleSubmit(_onSubmit)}>
<DialogTitle>
{editMode ? 'Edycja użytkownika' : 'Tworzenie użytkownika'}
</DialogTitle>
<DialogContent className={classes.dialogContent}>
<TextField
fullWidth
label="Nazwa użytkownika"
name="displayName"
inputRef={register({
required: 'Te pole jest wymagane.',
minLength: {
value: MIN_DISPLAY_NAME_LENGTH,
message: 'Te pole jest wymagane.',
},
maxLength: {
value: MAX_DISPLAY_NAME_LENGTH,
message: `Maksymalna długość nazwy użytkownika to ${MAX_DISPLAY_NAME_LENGTH} znaki.`,
},
})}
error={!!errors.displayName}
helperText={errors.displayName ? errors.displayName.message : ''}
/>
<TextField
fullWidth
label="Adres e-mail"
name="email"
inputRef={register({
required: 'Te pole jest wymagane.',
validate: (email: string) => {
return isEmail(email ?? '')
? true
: 'Niepoprawny adres e-mail.';
},
})}
error={!!errors.email}
helperText={errors.email ? errors.email.message : ''}
/>
<TextField
fullWidth
label="Hasło"
type="password"
name="password"
inputRef={register({
required: 'Te pole jest wymagane.',
minLength: {
value: MIN_PASSWORD_LENGTH,
message: `Hasło musi zawierać co najmniej ${MIN_PASSWORD_LENGTH} znaków.`,
},
maxLength: {
value: MAX_PASSWORD_LENGTH,
message: `Hasło może zawierać co najwyżej ${MAX_PASSWORD_LENGTH} znaki.`,
},
})}
error={!!errors.password}
helperText={errors.password ? errors.password.message : ''}
/>
<FormControl>
<FormLabel>Rola</FormLabel>
<RadioGroup name="role" defaultValue={Role.User}>
{[Role.Admin, Role.User].map(role => {
return (
<FormControlLabel
value={role}
key={role}
name="role"
control={<Radio inputRef={register} />}
label={formatRole(role)}
/>
);
})}
</RadioGroup>
</FormControl>
<FormGroup>
<FormControlLabel
control={<Checkbox inputRef={register} name="activated" />}
label="Aktywowany"
/>
</FormGroup>
</DialogContent>
<DialogActions>
<Button
color="secondary"
type="button"
variant="contained"
onClick={onClose}
disabled={isSubmitting}
>
Anuluj
</Button>
<Button
type="submit"
color="primary"
variant="contained"
disabled={isSubmitting}
>
{editMode ? 'Zapisz' : 'Utwórz'}
</Button>
</DialogActions>
</form>
</Dialog>
);
};
const useStyles = makeStyles(theme => ({
dialogContent: {
'& > *:not(:last-child)': {
marginBottom: theme.spacing(1),
},
},
}));
export default FormDialog;

View File

@ -0,0 +1,4 @@
export const MIN_DISPLAY_NAME_LENGTH = 2;
export const MAX_DISPLAY_NAME_LENGTH = 32;
export const MIN_PASSWORD_LENGTH = 6;
export const MAX_PASSWORD_LENGTH = 64;

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { useDebounce } from 'react-use';
import { makeStyles } from '@material-ui/core/styles';
@ -10,9 +10,14 @@ import SearchInput from 'common/Form/SearchInput';
export interface TableToolbarProps {
search: string;
onChangeSearchValue: (search: string) => void;
onClickCreateUser: () => void;
}
const TableToolbar = ({ search, onChangeSearchValue }: TableToolbarProps) => {
const TableToolbar = ({
search,
onChangeSearchValue,
onClickCreateUser,
}: TableToolbarProps) => {
const classes = useStyles();
const [_search, setSearch] = useState<string>(search);
useDebounce(
@ -35,7 +40,7 @@ const TableToolbar = ({ search, onChangeSearchValue }: TableToolbarProps) => {
}}
/>
<Tooltip title="Utwórz użykownika">
<IconButton>
<IconButton onClick={onClickCreateUser}>
<AddIcon />
</IconButton>
</Tooltip>

View File

@ -37,3 +37,8 @@ export const COLUMNS: Column<User>[] = [
type: 'datetime',
},
];
export enum DialogType {
Create = 'create',
Edit = 'edit',
None = '',
}

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const MUTATION_CREATE_USER = gql`
mutation createUser($input: UserInput!) {
createUser(input: $input) {
id
}
}
`;

View File

@ -2700,6 +2700,11 @@
resolved "https://registry.yarnpkg.com/@types/ungap__global-this/-/ungap__global-this-0.3.1.tgz#18ce9f657da556037a29d50604335614ce703f4c"
integrity sha512-+/DsiV4CxXl6ZWefwHZDXSe1Slitz21tom38qPCaG0DYCS1NnDPIQDTKcmQ/tvK/edJUKkmuIDBJbmKDiB0r/g==
"@types/validator@^13.1.3":
version "13.1.3"
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.1.3.tgz#366b394aa3fbeed2392bf0a20ded606fa4a3d35e"
integrity sha512-DaOWN1zf7j+8nHhqXhIgNmS+ltAC53NXqGxYuBhWqWgqolRhddKzfZU814lkHQSTG0IUfQxU7Cg0gb8fFWo2mA==
"@types/webpack-sources@*":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-2.1.0.tgz#8882b0bd62d1e0ce62f183d0d01b72e6e82e8c10"
@ -13557,6 +13562,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
validator@^13.5.2:
version "13.5.2"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.5.2.tgz#c97ae63ed4224999fb6f42c91eaca9567fe69a46"
integrity sha512-mD45p0rvHVBlY2Zuy3F3ESIe1h5X58GPfAtslBjY7EtTqGquZTj+VX/J4RnHWN8FKq0C9WRVt1oWAcytWRuYLQ==
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"