add users page

This commit is contained in:
2021-06-24 23:45:36 +03:00
parent 5d2ecf2f0c
commit 40ac760857
17 changed files with 227 additions and 97 deletions

View File

@ -57,7 +57,9 @@
"react/jsx-fragments": 0, "react/jsx-fragments": 0,
"react/jsx-key": "warn", "react/jsx-key": "warn",
"react/no-array-index-key": 0, "react/no-array-index-key": 0,
"react/display-name": 0,
"react/destructuring-assignment": 0, "react/destructuring-assignment": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"no-console": [ "no-console": [
"warn", "warn",
{ {

View File

@ -18,10 +18,10 @@ type RequestEntities<T> = {
offset?: number; offset?: number;
}; };
type EntityWithId<T> = T & { export type EntityWithId<T> = T & {
id: string; id: string;
}; };
type EntityWithoutId<T> = Omit<T, 'id'>; export type EntityWithoutId<T> = Omit<T, 'id'>;
type ResponseEntities<T> = { type ResponseEntities<T> = {
data: EntityWithId<T>[]; data: EntityWithId<T>[];
@ -68,8 +68,8 @@ export class CrudAPI<T> {
return http.post<never, EntityWithoutId<T>, EntityWithId<T>>(this.endpoint, undefined, entity); return http.post<never, EntityWithoutId<T>, EntityWithId<T>>(this.endpoint, undefined, entity);
} }
update = (id: string, entity: T): Promise<EntityWithId<T>> => { update = (id: string, entity: EntityWithoutId<T>): Promise<EntityWithId<T>> => {
return http.patch<never, T, EntityWithId<T>>(`${this.endpoint}/${id}`, undefined, entity); return http.patch<never, EntityWithoutId<T>, EntityWithId<T>>(`${this.endpoint}/${id}`, undefined, entity);
} }
replace = (id: string, entity: T): Promise<EntityWithId<T>> => { replace = (id: string, entity: T): Promise<EntityWithId<T>> => {

View File

@ -8,6 +8,7 @@ export const ROUTES = {
}; };
export const ENDPOINT = { export const ENDPOINT = {
AUTH: 'https://localhost:3189/api/auth',
USERS: 'https://localhost:3189/api/users', USERS: 'https://localhost:3189/api/users',
ACTIONS: 'https://localhost:3189/api/bot/actions', ACTIONS: 'https://localhost:3189/api/bot/actions',
CONDITIONS: 'https://localhost:3189/api/bot/conditions', CONDITIONS: 'https://localhost:3189/api/bot/conditions',

View File

@ -11,28 +11,28 @@ export type FieldData = {
}; };
const INIT_USERS: User[] = []; const INIT_USERS: User[] = [];
export const INIT_USER: FieldData[] = [ export const INIT_USER: User = {
{name: 'id', value: ''}, id: '',
{name: 'login', value: ''}, login: '',
{name: 'password', value: ''}, password: '',
]; };
export const loadUsersAction = declareAction<User[]>(); export const loadUsersAction = declareAction<User[]>();
export const usersAtom = declareAtom(INIT_USERS, on => [ export const usersAtom = declareAtom(INIT_USERS, on => [
on(loadUsersAction, (_state, payload) => payload), on(loadUsersAction, (_state, payload) => payload),
]); ]);
export const loadUserForm = declareAction<FieldData[]>(); export const loadUserForm = declareAction<User>();
export const userFormAtom = declareAtom(INIT_USER, on => [ export const userFormAtom = declareAtom(INIT_USER, on => [
on(loadUserForm, (_state, payload) => payload), on(loadUserForm, (state, payload) => payload),
]); ]);
export const bindedActions = { export const bindedActions = {
loadUsersAction: (users: User[]) => { loadUsersAction: (users: User[]) => {
store.dispatch(loadUsersAction(users)); store.dispatch(loadUsersAction(users));
}, },
loadUserForm: (fieldData: FieldData[]) => { loadUserForm: (user: User) => {
store.dispatch(loadUserForm(fieldData)); store.dispatch(loadUserForm(user));
} }
}; };

View File

@ -1,5 +1,25 @@
import {ENDPOINT} from '_consts/common'; import {ENDPOINT} from '_consts/common';
import {CrudAPI} from '../../../core/api/CrudAPI'; import {CrudAPI, EntityWithId, EntityWithoutId} from '../../../core/api/CrudAPI';
import {http} from '../../../core/infrastructure/Http';
import {User} from '../types'; import {User} from '../types';
import {ChangePasswordRequest} from './types';
export const usersAPI = new CrudAPI<User>(ENDPOINT.USERS); class UsersAPI<T> extends CrudAPI<T> {
create = (entity: EntityWithoutId<T>): Promise<EntityWithId<T>> => {
return http.post<never, EntityWithoutId<T>, EntityWithId<T>>(
`${ENDPOINT.AUTH}/register-user`,
undefined,
entity,
);
}
changePassword = (id: string, password: string): Promise<string> => {
return http.post<never, ChangePasswordRequest, string>(
`${ENDPOINT.AUTH}/admin-change-password`,
undefined,
{id, password},
);
}
}
export const usersAPI = new UsersAPI<User>(ENDPOINT.USERS);

View File

@ -0,0 +1,4 @@
export type ChangePasswordRequest = {
id: string;
password: string;
};

View File

@ -0,0 +1,44 @@
import {Input, Modal} from 'antd';
import React, {FC, memo, useCallback, useState} from 'react';
import {ROUTES} from '../../../../core/consts/common';
import {useQuery} from '../../../../core/hooks/useQuery';
import {routerService} from '../../../../core/services/RouterService';
import {LABELS} from '../../consts';
import {ModalType} from '../../enums';
import {usersService} from '../../services/UsersServices';
import {queryParsers} from '../../utils';
const handleClose = () => {
routerService.pushWithQuery(ROUTES.USERS, {}, {reset: true});
};
const ChangePasswordModal: FC = () => {
const {id, modalType} = useQuery(queryParsers);
const isOpen = modalType === ModalType.ChangePassword;
const [password, setForm] = useState('');
const handleChange = useCallback((event: React.SyntheticEvent<HTMLInputElement>) => {
setForm(event.currentTarget.value);
}, [setForm]);
const handleChangePassword = useCallback(() => {
usersService.changePassword(id, password);
}, [id, password]);
return (
<Modal
title={LABELS.CHANGE_PASSWORD}
visible={isOpen}
onOk={handleChangePassword}
onCancel={handleClose}
>
<form>
<label>Password:</label>
<Input onChange={handleChange} value={password} />
</form>
</Modal>
);
};
export default memo(ChangePasswordModal);

View File

@ -0,0 +1 @@
export * from './ChangePasswordModal';

View File

@ -1,9 +1,10 @@
import {Button, Layout} from 'antd'; import {Button, Layout} from 'antd';
import React, {FC, memo} from 'react'; import React, {FC, Fragment, memo} from 'react';
import {createUseStyles} from 'react-jss'; import {createUseStyles} from 'react-jss';
import {ROUTES} from '../../../../core/consts/common'; import {ROUTES} from '../../../../core/consts/common';
import {routerService} from '../../../../core/services/RouterService'; import {routerService} from '../../../../core/services/RouterService';
import {EntityMode} from '../../../../core/types/EntityModes'; import {EntityMode} from '../../../../core/types/EntityModes';
import ChangePasswordModal from '../change-password-modal/ChangePasswordModal';
import UserSidebar from '../user-sidebar/UserSidebar'; import UserSidebar from '../user-sidebar/UserSidebar';
import UsersTable from '../users-table/UsersTable'; import UsersTable from '../users-table/UsersTable';
@ -22,20 +23,23 @@ const handleClickNewUser = () => {
const Page: FC = () => { const Page: FC = () => {
const classes = useStyles(); const classes = useStyles();
return ( return (
<Layout> <Fragment>
<Layout.Header className={classes.header}> <Layout>
<Button <Layout.Header className={classes.header}>
type="primary" <Button
onClick={handleClickNewUser} type="primary"
> onClick={handleClickNewUser}
New user >
</Button> New user
</Layout.Header> </Button>
<Layout.Content> </Layout.Header>
<UsersTable /> <Layout.Content>
<UserSidebar /> <UsersTable />
</Layout.Content> <UserSidebar />
</Layout> </Layout.Content>
</Layout>
<ChangePasswordModal />
</Fragment>
); );
}; };

View File

@ -1,59 +1,59 @@
import {useAtom} from '@reatom/react'; import {useAtom} from '@reatom/react';
import {Button, Drawer, Form, Input} from 'antd'; import {Button, Drawer, Input} from 'antd';
import React, {FC, Fragment, memo, useCallback, useEffect, useMemo} from 'react'; import React, {FC, Fragment, memo, SyntheticEvent, useCallback, useEffect, useMemo} from 'react';
import {createUseStyles} from 'react-jss'; import {createUseStyles} from 'react-jss';
import {ROUTES} from '../../../../core/consts/common'; import {ROUTES} from '../../../../core/consts/common';
import {useQuery} from '../../../../core/hooks/useQuery'; import {useQuery} from '../../../../core/hooks/useQuery';
import {bindedActions, FieldData, userFormAtom} from '../../../../core/infrastructure/atom/usersAtom'; import {bindedActions, userFormAtom} from '../../../../core/infrastructure/atom/usersAtom';
import {isNotEmpty} from '../../../../core/referers/common';
import {routerService} from '../../../../core/services/RouterService'; import {routerService} from '../../../../core/services/RouterService';
import {EntityMode} from '../../../../core/types/EntityModes'; import {EntityMode} from '../../../../core/types/EntityModes';
import {usersService} from '../../services/UsersServices'; import {usersService} from '../../services/UsersServices';
import {User} from '../../types';
import {queryParsers} from '../../utils'; import {queryParsers} from '../../utils';
const AVAILABLE_CLOSE_MODES = [EntityMode.Show]; const AVAILABLE_CLOSE_MODES = [EntityMode.Show];
const DISABLED_FORM_MODES = [EntityMode.Show]; const DISABLED_FORM_MODES = [EntityMode.Show];
const SHOW_ID_MODES = [EntityMode.Show]; const SHOW_PASSWORD_MODES = [EntityMode.Create, EntityMode.Copy];
const useStyles = createUseStyles({ const useStyles = createUseStyles({
button: { button: {
marginRight: '8px', marginRight: '8px',
} },
input: {
marginBottom: '16px',
},
}); });
const handleClose = () => { const handleClose = () => {
routerService.push(ROUTES.USERS); routerService.push(ROUTES.USERS);
}; };
const onFieldsChange = (_: FieldData[], allFields: FieldData[]) => {
bindedActions.loadUserForm(allFields);
};
const UserSidebar: FC = () => { const UserSidebar: FC = () => {
const {mode, id} = useQuery(queryParsers); const {mode, id} = useQuery(queryParsers);
const fields = useAtom(userFormAtom); const form = useAtom(userFormAtom);
const classes = useStyles(); const classes = useStyles();
useEffect(() => { useEffect(() => {
usersService.loadUser(id); usersService.loadUser(id);
}, [id, mode]); }, [id, mode]);
const onChange = (event: SyntheticEvent<HTMLInputElement>) => {
const {name, value} = event.currentTarget;
bindedActions.loadUserForm({
...form,
[name]: value,
});
};
const disabled = useMemo(() => !mode || DISABLED_FORM_MODES.includes(mode), [mode]); const disabled = useMemo(() => !mode || DISABLED_FORM_MODES.includes(mode), [mode]);
const handleCreate = useCallback(() => { const handleCreateUser = useCallback(() => {
const user = fields.reduce<User>((acc, {name, value}) => { const {login, password} = form;
if (Array.isArray(name)) { usersService.createUser({login, password});
if (isNotEmpty(name[0])) { }, [form]);
acc[name[0]] = value;
} const handleSaveUser = useCallback(() => {
} else { usersService.updateUser(form);
acc[name] = value; }, [form]);
}
return acc;
}, {id: '', login: '', password: ''});
usersService.createUser(user);
}, [fields]);
const handleCopy = useCallback(() => { const handleCopy = useCallback(() => {
if (id) { if (id) {
@ -105,7 +105,7 @@ const UserSidebar: FC = () => {
return ( return (
<Button <Button
className={classes.button} className={classes.button}
onClick={handleCreate} onClick={handleCreateUser}
type="primary" type="primary"
> >
Create Create
@ -115,6 +115,7 @@ const UserSidebar: FC = () => {
return ( return (
<Button <Button
className={classes.button} className={classes.button}
onClick={handleSaveUser}
type="primary" type="primary"
> >
Save Save
@ -133,7 +134,7 @@ const UserSidebar: FC = () => {
default: default:
return null; return null;
} }
}, [mode, classes, handleEdit, handleCreate]); }, [mode, classes, handleEdit, handleCreateUser, handleSaveUser]);
const renderFooter = useMemo(() => { const renderFooter = useMemo(() => {
return ( return (
@ -169,31 +170,30 @@ const UserSidebar: FC = () => {
title={title} title={title}
footer={renderFooter} footer={renderFooter}
> >
<Form <form>
fields={fields} <div>
onFieldsChange={onFieldsChange as any} <label>Логин:</label>
> <Input
<Form.Item name="login"
label="Login" className={classes.input}
name="login" disabled={disabled}
> value={form.login}
<Input disabled={disabled} /> onChange={onChange}
</Form.Item> />
<Form.Item </div>
label="Password" {mode && SHOW_PASSWORD_MODES.includes(mode) && (
name="password" <div>
> <label>Пароль:</label>
<Input disabled={disabled} /> <Input
</Form.Item> name="password"
{mode && SHOW_ID_MODES.includes(mode) && ( className={classes.input}
<Form.Item disabled={disabled}
label="ID" value={form.password}
name="id" onChange={onChange}
> />
<Input disabled={disabled} /> </div>
</Form.Item>
)} )}
</Form> </form>
</Drawer> </Drawer>
); );
}; };

View File

@ -1,5 +1,6 @@
import {useAtom} from '@reatom/react'; import {useAtom} from '@reatom/react';
import {Table} from 'antd'; import {Button, Table} from 'antd';
import {ColumnsType} from 'antd/lib/table';
import {head} from 'lodash'; import {head} from 'lodash';
import React, {FC, memo, useEffect, useMemo} from 'react'; import React, {FC, memo, useEffect, useMemo} from 'react';
import {ROUTES} from '../../../../core/consts/common'; import {ROUTES} from '../../../../core/consts/common';
@ -7,6 +8,7 @@ import {usersAtom} from '../../../../core/infrastructure/atom/usersAtom';
import {routerService} from '../../../../core/services/RouterService'; import {routerService} from '../../../../core/services/RouterService';
import {EntityMode} from '../../../../core/types/EntityModes'; import {EntityMode} from '../../../../core/types/EntityModes';
import {objectKeys} from '../../../../core/utils/objectKeys'; import {objectKeys} from '../../../../core/utils/objectKeys';
import {ModalType} from '../../enums';
import {usersService} from '../../services/UsersServices'; import {usersService} from '../../services/UsersServices';
import {User} from '../../types'; import {User} from '../../types';
@ -19,6 +21,14 @@ const onRow = (user: User) => ({
}, },
}); });
const onClick = (id: string) => (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation();
routerService.pushWithQuery(ROUTES.USERS, {
id,
modalType: ModalType.ChangePassword,
});
};
const UsersTable: FC = () => { const UsersTable: FC = () => {
const users = useAtom(usersAtom); const users = useAtom(usersAtom);
@ -26,8 +36,18 @@ const UsersTable: FC = () => {
usersService.loadUsers(); usersService.loadUsers();
}, []); }, []);
const columns = useMemo(() => { const columns: ColumnsType<User> = useMemo(() => {
return objectKeys(head(users) ?? {}).map(field => { const user = head(users) ?? {} as User;
return objectKeys(user).map(field => {
if (field === 'password') {
return {
title: field,
key: field,
render: () => (
<Button onClick={onClick(user.id)}>Change password</Button>
),
};
}
return { return {
title: field, title: field,
dataIndex: field, dataIndex: field,

View File

@ -0,0 +1,3 @@
export const LABELS = {
CHANGE_PASSWORD: 'Change password',
};

3
src/pages/users/enums.ts Normal file
View File

@ -0,0 +1,3 @@
export enum ModalType {
ChangePassword = 'changePassword',
}

View File

@ -1,7 +1,7 @@
import {EntityWithoutId} from '../../../core/api/CrudAPI';
import {ROUTES} from '../../../core/consts/common'; import {ROUTES} from '../../../core/consts/common';
import {bindedActions, FieldData, INIT_USER} from '../../../core/infrastructure/atom/usersAtom'; import {bindedActions, INIT_USER} from '../../../core/infrastructure/atom/usersAtom';
import {routerService} from '../../../core/services/RouterService'; import {routerService} from '../../../core/services/RouterService';
import {objectEntries} from '../../../core/utils/objectEntries';
import {usersAPI} from '../api/UsersAPI'; import {usersAPI} from '../api/UsersAPI';
import {User} from '../types'; import {User} from '../types';
@ -19,18 +19,16 @@ class UsersService {
usersAPI usersAPI
.find(id) .find(id)
.then(user => { .then(user => {
const fieldData = objectEntries(user).reduce<FieldData[]>((acc, [name, value]) => { bindedActions.loadUserForm({
acc.push({name, value}); ...user,
return acc; password: '',
}, []); });
bindedActions.loadUserForm(fieldData);
}); });
} }
bindedActions.loadUserForm(INIT_USER); bindedActions.loadUserForm(INIT_USER);
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars createUser(user: EntityWithoutId<User>) {
createUser({id, ...user}: User) {
usersAPI usersAPI
.create(user) .create(user)
.then(() => { .then(() => {
@ -40,6 +38,28 @@ class UsersService {
}); });
} }
changePassword(id?: string, password?: string) {
if (id && password) {
usersAPI
.changePassword(id, password)
.then(() => {
this.loadUsers().then(() => {
routerService.push(ROUTES.USERS);
});
});
}
}
updateUser({id, ...user}: User) {
usersAPI
.update(id, user)
.then(() => {
this.loadUsers().then(() => {
routerService.push(ROUTES.USERS);
});
});
}
removeUser(id?: string) { removeUser(id?: string) {
if (id) { if (id) {
usersAPI usersAPI

View File

@ -1,4 +1,5 @@
import {EntityMode} from '../../core/types/EntityModes'; import {EntityMode} from '../../core/types/EntityModes';
import {ModalType} from './enums';
export type User = { export type User = {
id: string; id: string;
@ -9,4 +10,5 @@ export type User = {
export type QueryParams = { export type QueryParams = {
id?: string; id?: string;
mode?: EntityMode; mode?: EntityMode;
modalType?: ModalType;
}; };

View File

@ -1,8 +1,9 @@
import {QueryParsers} from '../../core/utils/getQueryFromUrl'; import {EntityMode} from '../../core/types/EntityModes';
import {stringParser} from '../../core/utils/queryParsers'; import {stringParser} from '../../core/utils/queryParsers';
import {QueryParams} from './types'; import {ModalType} from './enums';
export const queryParsers: QueryParsers<QueryParams> = { export const queryParsers = {
id: stringParser(), id: stringParser(),
mode: stringParser(), mode: stringParser<EntityMode>(),
modalType: stringParser<ModalType>(),
}; };

View File

@ -33,6 +33,11 @@ module.exports = {
pathRewrite: { '^/api': '' }, pathRewrite: { '^/api': '' },
secure: false, secure: false,
}, },
'/api/auth': {
target: 'http://vigdorov.ru:3013',
pathRewrite: { '^/api': '' },
secure: false,
},
'/api/bot': { '/api/bot': {
target: 'http://vigdorov.ru:3012', target: 'http://vigdorov.ru:3012',
pathRewrite: { '^/api/bot': '' }, pathRewrite: { '^/api/bot': '' },