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-key": "warn",
"react/no-array-index-key": 0,
"react/display-name": 0,
"react/destructuring-assignment": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"no-console": [
"warn",
{

View File

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

View File

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

View File

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

View File

@ -1,5 +1,25 @@
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 {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 React, {FC, memo} from 'react';
import React, {FC, Fragment, memo} from 'react';
import {createUseStyles} from 'react-jss';
import {ROUTES} from '../../../../core/consts/common';
import {routerService} from '../../../../core/services/RouterService';
import {EntityMode} from '../../../../core/types/EntityModes';
import ChangePasswordModal from '../change-password-modal/ChangePasswordModal';
import UserSidebar from '../user-sidebar/UserSidebar';
import UsersTable from '../users-table/UsersTable';
@ -22,6 +23,7 @@ const handleClickNewUser = () => {
const Page: FC = () => {
const classes = useStyles();
return (
<Fragment>
<Layout>
<Layout.Header className={classes.header}>
<Button
@ -36,6 +38,8 @@ const Page: FC = () => {
<UserSidebar />
</Layout.Content>
</Layout>
<ChangePasswordModal />
</Fragment>
);
};

View File

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

View File

@ -1,5 +1,6 @@
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 React, {FC, memo, useEffect, useMemo} from 'react';
import {ROUTES} from '../../../../core/consts/common';
@ -7,6 +8,7 @@ import {usersAtom} from '../../../../core/infrastructure/atom/usersAtom';
import {routerService} from '../../../../core/services/RouterService';
import {EntityMode} from '../../../../core/types/EntityModes';
import {objectKeys} from '../../../../core/utils/objectKeys';
import {ModalType} from '../../enums';
import {usersService} from '../../services/UsersServices';
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 users = useAtom(usersAtom);
@ -26,8 +36,18 @@ const UsersTable: FC = () => {
usersService.loadUsers();
}, []);
const columns = useMemo(() => {
return objectKeys(head(users) ?? {}).map(field => {
const columns: ColumnsType<User> = useMemo(() => {
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 {
title: 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 {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 {objectEntries} from '../../../core/utils/objectEntries';
import {usersAPI} from '../api/UsersAPI';
import {User} from '../types';
@ -19,18 +19,16 @@ class UsersService {
usersAPI
.find(id)
.then(user => {
const fieldData = objectEntries(user).reduce<FieldData[]>((acc, [name, value]) => {
acc.push({name, value});
return acc;
}, []);
bindedActions.loadUserForm(fieldData);
bindedActions.loadUserForm({
...user,
password: '',
});
});
}
bindedActions.loadUserForm(INIT_USER);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
createUser({id, ...user}: User) {
createUser(user: EntityWithoutId<User>) {
usersAPI
.create(user)
.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) {
if (id) {
usersAPI

View File

@ -1,4 +1,5 @@
import {EntityMode} from '../../core/types/EntityModes';
import {ModalType} from './enums';
export type User = {
id: string;
@ -9,4 +10,5 @@ export type User = {
export type QueryParams = {
id?: string;
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 {QueryParams} from './types';
import {ModalType} from './enums';
export const queryParsers: QueryParsers<QueryParams> = {
export const queryParsers = {
id: stringParser(),
mode: stringParser(),
mode: stringParser<EntityMode>(),
modalType: stringParser<ModalType>(),
};

View File

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