UsersPage: add FormDialog
This commit is contained in:
parent
ff3636542c
commit
2f73ef1b62
|
@ -29,6 +29,7 @@
|
||||||
"react-use": "^17.2.1",
|
"react-use": "^17.2.1",
|
||||||
"typescript": "^4.1.2",
|
"typescript": "^4.1.2",
|
||||||
"use-query-params": "^1.2.0",
|
"use-query-params": "^1.2.0",
|
||||||
|
"validator": "^13.5.2",
|
||||||
"web-vitals": "^1.0.1"
|
"web-vitals": "^1.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
"@types/apollo-upload-client": "^14.1.0",
|
"@types/apollo-upload-client": "^14.1.0",
|
||||||
"@types/lodash": "^4.14.168",
|
"@types/lodash": "^4.14.168",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
|
"@types/validator": "^13.1.3",
|
||||||
"babel-plugin-transform-imports": "^2.0.0",
|
"babel-plugin-transform-imports": "^2.0.0",
|
||||||
"babel-plugin-transform-modules": "^0.1.1",
|
"babel-plugin-transform-modules": "^0.1.1",
|
||||||
"customize-cra": "^1.0.0",
|
"customize-cra": "^1.0.0",
|
||||||
|
|
|
@ -1,20 +1,32 @@
|
||||||
import useUsers from './UsersPage.useUsers';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
NumberParam,
|
NumberParam,
|
||||||
|
StringParam,
|
||||||
useQueryParams,
|
useQueryParams,
|
||||||
withDefault,
|
withDefault,
|
||||||
StringParam,
|
|
||||||
} from 'use-query-params';
|
} from 'use-query-params';
|
||||||
import SortParam, { decodeSort } from 'libs/serialize-query-params/SortParam';
|
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 { 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 { Container, IconButton, Paper } from '@material-ui/core';
|
||||||
import { Edit as EditIcon } from '@material-ui/icons';
|
import { Edit as EditIcon } from '@material-ui/icons';
|
||||||
import Table from 'common/Table/Table';
|
import Table from 'common/Table/Table';
|
||||||
import TableToolbar from './components/TableToolbar/TableToolbar';
|
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 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({
|
const [{ page, sort, search, ...rest }, setQueryParams] = useQueryParams({
|
||||||
limit: NumberParam,
|
limit: NumberParam,
|
||||||
page: withDefault(NumberParam, 0),
|
page: withDefault(NumberParam, 0),
|
||||||
|
@ -22,19 +34,34 @@ const UsersPage = () => {
|
||||||
search: withDefault(StringParam, ''),
|
search: withDefault(StringParam, ''),
|
||||||
});
|
});
|
||||||
const limit = validateRowsPerPage(rest.limit);
|
const limit = validateRowsPerPage(rest.limit);
|
||||||
const { users, total, loading } = useUsers(
|
const { users, total, loading, refetch } = useUsers(
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
sort.toString(),
|
sort.toString(),
|
||||||
search
|
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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Paper>
|
<Paper>
|
||||||
<TableToolbar
|
<TableToolbar
|
||||||
search={search}
|
search={search}
|
||||||
|
onClickCreateUser={() => setDialogType(DialogType.Create)}
|
||||||
onChangeSearchValue={val => {
|
onChangeSearchValue={val => {
|
||||||
setQueryParams({ page: 0, search: val });
|
setQueryParams({ page: 0, search: val });
|
||||||
}}
|
}}
|
||||||
|
@ -77,6 +104,13 @@ const UsersPage = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
<FormDialog
|
||||||
|
open={
|
||||||
|
dialogType === DialogType.Create || dialogType === DialogType.Edit
|
||||||
|
}
|
||||||
|
onSubmit={handleCreateUser}
|
||||||
|
onClose={() => setDialogType(DialogType.None)}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,23 +8,23 @@ const useUsers = (
|
||||||
sort: string,
|
sort: string,
|
||||||
search: string
|
search: string
|
||||||
) => {
|
) => {
|
||||||
const { data, loading } = useQuery<Pick<Query, 'users'>, QueryUsersArgs>(
|
const { data, loading, refetch } = useQuery<
|
||||||
QUERY_USERS,
|
Pick<Query, 'users'>,
|
||||||
{
|
QueryUsersArgs
|
||||||
fetchPolicy: 'cache-and-network',
|
>(QUERY_USERS, {
|
||||||
variables: {
|
fetchPolicy: 'cache-and-network',
|
||||||
offset: page * limit,
|
variables: {
|
||||||
sort: [sort],
|
offset: page * limit,
|
||||||
limit,
|
sort: [sort],
|
||||||
filter: {
|
limit,
|
||||||
or: {
|
filter: {
|
||||||
displayNameIEQ: '%' + search + '%',
|
or: {
|
||||||
emailIEQ: '%' + search + '%',
|
displayNameIEQ: '%' + search + '%',
|
||||||
},
|
emailIEQ: '%' + search + '%',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users: data?.users.items ?? [],
|
users: data?.users.items ?? [],
|
||||||
|
@ -32,6 +32,7 @@ const useUsers = (
|
||||||
return this.users.length === 0 && loading;
|
return this.users.length === 0 && loading;
|
||||||
},
|
},
|
||||||
total: data?.users.total ?? 0,
|
total: data?.users.total ?? 0,
|
||||||
|
refetch,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
181
src/features/UsersPage/components/FormDialog/FormDialog.tsx
Normal file
181
src/features/UsersPage/components/FormDialog/FormDialog.tsx
Normal 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;
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useDebounce } from 'react-use';
|
import { useDebounce } from 'react-use';
|
||||||
|
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
@ -10,9 +10,14 @@ import SearchInput from 'common/Form/SearchInput';
|
||||||
export interface TableToolbarProps {
|
export interface TableToolbarProps {
|
||||||
search: string;
|
search: string;
|
||||||
onChangeSearchValue: (search: string) => void;
|
onChangeSearchValue: (search: string) => void;
|
||||||
|
onClickCreateUser: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableToolbar = ({ search, onChangeSearchValue }: TableToolbarProps) => {
|
const TableToolbar = ({
|
||||||
|
search,
|
||||||
|
onChangeSearchValue,
|
||||||
|
onClickCreateUser,
|
||||||
|
}: TableToolbarProps) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [_search, setSearch] = useState<string>(search);
|
const [_search, setSearch] = useState<string>(search);
|
||||||
useDebounce(
|
useDebounce(
|
||||||
|
@ -35,7 +40,7 @@ const TableToolbar = ({ search, onChangeSearchValue }: TableToolbarProps) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tooltip title="Utwórz użykownika">
|
<Tooltip title="Utwórz użykownika">
|
||||||
<IconButton>
|
<IconButton onClick={onClickCreateUser}>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -37,3 +37,8 @@ export const COLUMNS: Column<User>[] = [
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
export enum DialogType {
|
||||||
|
Create = 'create',
|
||||||
|
Edit = 'edit',
|
||||||
|
None = '',
|
||||||
|
}
|
||||||
|
|
9
src/features/UsersPage/mutations.ts
Normal file
9
src/features/UsersPage/mutations.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const MUTATION_CREATE_USER = gql`
|
||||||
|
mutation createUser($input: UserInput!) {
|
||||||
|
createUser(input: $input) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
10
yarn.lock
10
yarn.lock
|
@ -2700,6 +2700,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/ungap__global-this/-/ungap__global-this-0.3.1.tgz#18ce9f657da556037a29d50604335614ce703f4c"
|
resolved "https://registry.yarnpkg.com/@types/ungap__global-this/-/ungap__global-this-0.3.1.tgz#18ce9f657da556037a29d50604335614ce703f4c"
|
||||||
integrity sha512-+/DsiV4CxXl6ZWefwHZDXSe1Slitz21tom38qPCaG0DYCS1NnDPIQDTKcmQ/tvK/edJUKkmuIDBJbmKDiB0r/g==
|
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@*":
|
"@types/webpack-sources@*":
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-2.1.0.tgz#8882b0bd62d1e0ce62f183d0d01b72e6e82e8c10"
|
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-correct "^3.0.0"
|
||||||
spdx-expression-parse "^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:
|
value-equal@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
||||||
|
|
Reference in New Issue
Block a user