add users page
This commit is contained in:
@ -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",
|
||||
{
|
||||
|
||||
@ -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>> => {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
4
src/pages/users/api/types.ts
Normal file
4
src/pages/users/api/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type ChangePasswordRequest = {
|
||||
id: string;
|
||||
password: string;
|
||||
};
|
||||
@ -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);
|
||||
@ -0,0 +1 @@
|
||||
export * from './ChangePasswordModal';
|
||||
@ -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,20 +23,23 @@ const handleClickNewUser = () => {
|
||||
const Page: FC = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Layout>
|
||||
<Layout.Header className={classes.header}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleClickNewUser}
|
||||
>
|
||||
New user
|
||||
</Button>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<UsersTable />
|
||||
<UserSidebar />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
<Fragment>
|
||||
<Layout>
|
||||
<Layout.Header className={classes.header}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleClickNewUser}
|
||||
>
|
||||
New user
|
||||
</Button>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<UsersTable />
|
||||
<UserSidebar />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
<ChangePasswordModal />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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"
|
||||
name="login"
|
||||
>
|
||||
<Input disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Password"
|
||||
name="password"
|
||||
>
|
||||
<Input disabled={disabled} />
|
||||
</Form.Item>
|
||||
{mode && SHOW_ID_MODES.includes(mode) && (
|
||||
<Form.Item
|
||||
label="ID"
|
||||
name="id"
|
||||
>
|
||||
<Input disabled={disabled} />
|
||||
</Form.Item>
|
||||
<form>
|
||||
<div>
|
||||
<label>Логин:</label>
|
||||
<Input
|
||||
name="login"
|
||||
className={classes.input}
|
||||
disabled={disabled}
|
||||
value={form.login}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
{mode && SHOW_PASSWORD_MODES.includes(mode) && (
|
||||
<div>
|
||||
<label>Пароль:</label>
|
||||
<Input
|
||||
name="password"
|
||||
className={classes.input}
|
||||
disabled={disabled}
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</form>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
3
src/pages/users/consts.ts
Normal file
3
src/pages/users/consts.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const LABELS = {
|
||||
CHANGE_PASSWORD: 'Change password',
|
||||
};
|
||||
3
src/pages/users/enums.ts
Normal file
3
src/pages/users/enums.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export enum ModalType {
|
||||
ChangePassword = 'changePassword',
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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>(),
|
||||
};
|
||||
|
||||
@ -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': '' },
|
||||
|
||||
Reference in New Issue
Block a user