GroupCreation, ErrorHandling, Database

This commit is contained in:
Maksim Skobaro 2025-02-07 09:48:50 +03:00
parent c2d19a7724
commit b1ea3a670e
31 changed files with 369 additions and 154 deletions

1
.gitignore vendored
View File

@ -31,3 +31,4 @@ build/
### VS Code ###
.vscode/
/logs/app.log
/configurations/Start RDBMS.run.xml

View File

@ -97,6 +97,7 @@ public class SecurityConfiguration {
httpAuthorization.requestMatchers("/api/v1/student/current").permitAll();
/* GroupController */
httpAuthorization.requestMatchers("/api/v1/group/get-all").permitAll();
httpAuthorization.requestMatchers("api/v1/group/create-group").hasAuthority("ROLE_ADMINISTRATOR");
/* deny all other api requests */
httpAuthorization.requestMatchers("/api/**").denyAll();
/* since api already blocked, all other requests are static resources */

View File

@ -1,14 +1,16 @@
package ru.tubryansk.tdms.controller;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import ru.tubryansk.tdms.controller.payload.GroupDTO;
import ru.tubryansk.tdms.controller.payload.GroupRegistrationDTO;
import ru.tubryansk.tdms.service.GroupService;
import java.util.Collection;
@RestController("/api/v1/group/")
@RestController
@RequestMapping("/api/v1/group")
public class GroupController {
@Autowired
private GroupService groupService;
@ -17,4 +19,9 @@ public class GroupController {
public Collection<GroupDTO> getAllGroups() {
return groupService.getAllGroups();
}
@PostMapping("/create-group")
public void createGroup(@RequestBody @Valid GroupRegistrationDTO groupRegistrationDTO) {
groupService.createGroup(groupRegistrationDTO.getName());
}
}

View File

@ -1,6 +1,5 @@
package ru.tubryansk.tdms.controller;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@ -13,9 +12,10 @@ public class SysInfoController {
@Autowired
private SysInfoService sysInfoService;
@SneakyThrows
@GetMapping("/version")
public String getVersion() {
return sysInfoService.getVersion();
}
// @GetMapping("/status")
}

View File

@ -9,6 +9,6 @@ import lombok.ToString;
@ToString
public class GroupDTO {
private String name;
private String principalName;
private Boolean isMePrincipal;
private String curatorName;
private Boolean iAmCurator;
}

View File

@ -0,0 +1,14 @@
package ru.tubryansk.tdms.controller.payload;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
@Getter
public class GroupRegistrationDTO {
@NotEmpty(message = "Имя группы не может быть пустым")
@Size(min = 3, max = 50, message = "Имя группы должно быть от 3 до 50 символов")
@Pattern(regexp = "^[а-яА-ЯёЁ0-9_-]*$", message = "Имя группы должно содержать только русские буквы, дефис, нижнее подчеркивание и цифры")
private String name;
}

View File

@ -5,13 +5,17 @@ import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.ZonedDateTime;
@Getter
@Setter
@ToString
@Entity
@Table(name = "group")
@Table(name = "`group`")
public class Group {
@Id
@Column(name = "id")
@ -20,6 +24,12 @@ public class Group {
@Column(name = "name")
private String name;
@ManyToOne
@JoinColumn(name = "curator_user_id")
private User groupCurator;
@JoinColumn(name = "curator_teacher_id")
private Teacher groupCurator;
@Column(name = "created_at")
@CreationTimestamp
private ZonedDateTime createdAt;
@Column(name = "updated_at")
@UpdateTimestamp
private ZonedDateTime updatedAt;
}

View File

@ -47,9 +47,10 @@ public class Student {
@ManyToOne
@JoinColumn(name = "diploma_topic_id")
private DiplomaTopic diplomaTopic;
// Научный руководитель
@ManyToOne
@JoinColumn(name = "mentor_user_id")
private User mentorUser;
@JoinColumn(name = "adviser_teacher_id")
private Teacher adviser;
@ManyToOne
@JoinColumn(name = "group_id")
private Group group;

View File

@ -0,0 +1,32 @@
package ru.tubryansk.tdms.entity;
import jakarta.persistence.*;
import lombok.Getter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.ZonedDateTime;
import java.util.List;
@Getter
@Entity
@Table(name = "teacher")
public class Teacher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "groupCurator")
private List<Group> curatingGroups;
@OneToMany(mappedBy = "adviser")
private List<Student> advisingStudents;
@Column(name = "created_at")
@CreationTimestamp
private ZonedDateTime createdAt;
@Column(name = "updated_at")
@UpdateTimestamp
private ZonedDateTime updatedAt;
}

View File

@ -2,6 +2,7 @@ package ru.tubryansk.tdms.entity;
import jakarta.persistence.*;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ -19,6 +20,7 @@ import java.util.List;
@Getter
@Setter
@ToString
@EqualsAndHashCode(of = "id")
@Entity
@Table(name = "`user`")
public class User implements UserDetails {

View File

@ -10,4 +10,6 @@ public interface GroupRepository extends JpaRepository<Group, Long> {
default Group findByIdThrow(Long id) {
return this.findById(id).orElseThrow(() -> new NotFoundException(Group.class, id));
}
boolean existsByName(String name);
}

View File

@ -9,8 +9,8 @@ import ru.tubryansk.tdms.exception.NotFoundException;
import java.util.Optional;
@Repository
public interface StudentRepository extends JpaRepository<Student, Integer> {
default Student findByIdThrow(Integer id) {
public interface StudentRepository extends JpaRepository<Student, Long> {
default Student findByIdThrow(Long id) {
return this.findById(id).orElseThrow(() -> new NotFoundException(Student.class, id));
}

View File

@ -0,0 +1,13 @@
package ru.tubryansk.tdms.entity.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.tubryansk.tdms.entity.Teacher;
import ru.tubryansk.tdms.exception.NotFoundException;
@Repository
public interface TeacherRepository extends JpaRepository<Teacher, Long> {
default Teacher findByIdThrow(Long id) {
return this.findById(id).orElseThrow(() -> new NotFoundException(Teacher.class, id));
}
}

View File

@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
import java.util.Random;
import java.util.stream.Collectors;
@RestControllerAdvice
@ -51,7 +52,9 @@ public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleUnexpectedException(Exception e) {
log.error("Unexpected exception.", e);
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.INTERNAL_ERROR);
Random random = new Random();
long errorInx = random.nextLong();
log.error("Unexpected exception. random: {}", errorInx, e);
return new ErrorResponse("Произошла непредвиденная ошибка, обратитесь к администратору. Номер ошибки: " + errorInx, ErrorResponse.ErrorCode.INTERNAL_ERROR);
}
}

View File

@ -1,15 +1,53 @@
package ru.tubryansk.tdms.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.tubryansk.tdms.controller.payload.GroupDTO;
import ru.tubryansk.tdms.entity.Group;
import ru.tubryansk.tdms.entity.User;
import ru.tubryansk.tdms.entity.repository.GroupRepository;
import ru.tubryansk.tdms.exception.BusinessException;
import java.util.Collection;
import java.util.List;
@Service
@Transactional
public class GroupService {
@Autowired
private GroupRepository groupRepository;
@Autowired
private CallerService callerService;
public Collection<GroupDTO> getAllGroups() {
return null;
List<Group> groups = groupRepository.findAll();
User callerUser = callerService.getCallerUser().orElse(null);
return groups.stream()
.map(g -> {
GroupDTO groupDTO = new GroupDTO();
groupDTO.setName(g.getName());
if (g.getGroupCurator() != null) {
groupDTO.setCuratorName(g.getGroupCurator().getUser().getFullName());
if (callerUser != null) {
groupDTO.setIAmCurator(g.getGroupCurator().getUser().equals(callerUser));
}
}
return groupDTO;
})
.toList();
}
public void createGroup(String groupName) {
boolean existsByName = groupRepository.existsByName(groupName);
if (existsByName) {
throw new BusinessException("Группа с именем " + groupName + " уже существует");
}
Group group = new Group();
group.setName(groupName);
groupRepository.save(group);
}
}

View File

@ -10,7 +10,6 @@ import ru.tubryansk.tdms.entity.repository.DiplomaTopicRepository;
import ru.tubryansk.tdms.entity.repository.StudentRepository;
import ru.tubryansk.tdms.exception.AccessDeniedException;
import java.util.Map;
import java.util.Optional;
@Service
@ -26,15 +25,15 @@ public class StudentService {
private CallerService callerService;
/** @param studentToDiplomaTopic Map of @{@link Student} id and @{@link DiplomaTopic} id */
public void changeDiplomaTopic(Map<Integer, Integer> studentToDiplomaTopic) {
studentToDiplomaTopic.forEach(this::changeDiplomaTopic);
}
// public void changeDiplomaTopic(Map<Integer, Integer> studentToDiplomaTopic) {
// studentToDiplomaTopic.forEach(this::changeDiplomaTopic);
// }
public void changeDiplomaTopic(Integer studentId, Integer diplomaTopicId) {
Student student = studentRepository.findByIdThrow(studentId);
DiplomaTopic diplomaTopic = diplomaTopicRepository.findByIdThrow(diplomaTopicId);
student.setDiplomaTopic(diplomaTopic);
}
// public void changeDiplomaTopic(Integer studentId, Integer diplomaTopicId) {
// Student student = studentRepository.findByIdThrow(studentId);
// DiplomaTopic diplomaTopic = diplomaTopicRepository.findByIdThrow(diplomaTopicId);
// student.setDiplomaTopic(diplomaTopic);
// }
public void changeCallerDiplomaTopic(Integer diplomaTopicId) {
DiplomaTopic diplomaTopic = diplomaTopicRepository.findByIdThrow(diplomaTopicId);

View File

@ -1,22 +0,0 @@
create table "group"
(
id bigserial primary key,
name text not null unique,
curator_user_id bigint,
created_at timestamptz not null,
updated_at timestamptz
);
-- FOREIGN KEY
alter table "group"
add constraint fk_group_curator_user_id
foreign key (curator_user_id) references "user" (id)
on delete set null on update cascade;
-- COMMENTS
comment on table "group" is 'Таблица групп студентов';
comment on column "group".name is 'Название группы';
comment on column "group".curator_user_id is 'Идентификатор куратора группы';

View File

@ -0,0 +1,19 @@
create table teacher
(
id bigserial primary key,
user_id bigint not null,
created_at timestamptz not null,
updated_at timestamptz
);
-- FOREIGN KEY
alter table teacher
add constraint fk_teacher_user_id
foreign key (user_id) references "user" (id)
on delete cascade on update cascade;
-- COMMENTS
comment on table teacher is 'Таблица преподавателей';
comment on column teacher.user_id is 'Идентификатор пользователя';

View File

@ -0,0 +1,22 @@
create table "group"
(
id bigserial primary key,
name text not null unique,
curator_teacher_id bigint,
created_at timestamptz not null,
updated_at timestamptz
);
-- FOREIGN KEY
alter table "group"
add constraint fk_group_curator_teacher_id
foreign key (curator_teacher_id) references teacher (id)
on delete set null on update cascade;
-- COMMENTS
comment on table "group" is 'Таблица групп студентов';
comment on column "group".name is 'Название группы';
comment on column "group".curator_teacher_id is 'Идентификатор куратора группы';

View File

@ -3,7 +3,7 @@ create table student
id bigserial primary key,
user_id bigint not null,
diploma_topic_id bigint not null,
mentor_user_id bigint not null,
adviser_teacher_id bigint not null,
group_id bigint not null,
form boolean,
@ -34,8 +34,8 @@ alter table student
foreign key (diploma_topic_id) references diploma_topic (id)
on delete set null on update cascade;
alter table student
add constraint fk_student_mentor_user_id
foreign key (mentor_user_id) references "user" (id)
add constraint fk_student_adviser_teacher_id
foreign key (adviser_teacher_id) references teacher (id)
on delete set null on update cascade;
alter table student
add constraint fk_student_group_id
@ -47,7 +47,7 @@ comment on table student is 'Таблица студентов';
comment on column student.user_id is 'Идентификатор пользователя';
comment on column student.diploma_topic_id is 'Идентификатор темы дипломной работы';
comment on column student.mentor_user_id is 'Идентификатор научного руководителя';
comment on column student.adviser_teacher_id is 'Идентификатор научного руководителя';
comment on column student.group_id is 'Идентификатор группы';
comment on column student.form is 'Форма обучения';

View File

@ -1,7 +1,7 @@
import {ComponentContext} from "../utils/ComponentContext";
import {observer} from "mobx-react";
import {Notification, NotificationType} from "../store/NotificationStore";
import {Card, CardBody, CardHeader, CardText, CardTitle, Col, Row} from "react-bootstrap";
import {Card, CardBody, CardHeader, CardText, CardTitle} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {action, makeObservable} from "mobx";
@ -14,7 +14,7 @@ export class NotificationContainer extends ComponentContext {
}
render() {
return <div style={{position: 'fixed', left: '50%', transform: 'translateX(-50%)', zIndex: 1000}}>
return <div style={{position: 'fixed', left: '50%', transform: 'translateX(-50%)', zIndex: 2000}}>
{this.forEachNotificationRender(this.notificationStore.errors, NotificationType.ERROR)}
{this.forEachNotificationRender(this.notificationStore.successes, NotificationType.SUCCESS)}
{this.forEachNotificationRender(this.notificationStore.warnings, NotificationType.WARNING)}
@ -51,37 +51,25 @@ class NotificationPopup extends ComponentContext<{ notification: Notification, t
render() {
const hasTitle = !!this.props.notification.title && this.props.notification.title.length > 0;
const closeIcon = <FontAwesomeIcon icon={'close'} onClick={this.close}/>;
const closeIcon = <span className={'ms-2'}><FontAwesomeIcon icon={'close'} onClick={this.close}/></span>;
return <Card className={`position-relative mt-3 opacity-75 ${this.cardClassName}`}>
{
hasTitle &&
<CardHeader>
<CardTitle>
<Row>
<Col sm={11}>
<CardTitle className={'d-flex justify-content-between align-items-start'}>
{this.props.notification.title}
</Col>
<Col className={'text-end'}>
{closeIcon}
</Col>
</Row>
</CardTitle>
</CardHeader>
}
<CardBody>
<CardText>
<Row>
<Col sm={11}>
<CardText className={'d-flex justify-content-between align-items-start'}>
{this.props.notification.message}
</Col>
{
!hasTitle &&
<Col className={'text-end'}>
{closeIcon}
</Col>
closeIcon
}
</Row>
</CardText>
</CardBody>
</Card>

View File

@ -74,7 +74,7 @@ export class PasswordInput extends React.Component<ReactiveInputProps<string>> {
className={`${this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`}
onChange={this.onChange} value={this.props.value.value}/>
</FloatingLabel>
<Button onClick={this.toggleShowPassword} variant={"outline-secondary"}>
<Button onClick={this.toggleShowPassword} variant={"outline-secondary"} className={'ms-2'}>
<FontAwesomeIcon icon={this.showPassword ? 'eye-slash' : 'eye'}/>
</Button>
</div>

View File

@ -0,0 +1,55 @@
import {ComponentContext} from "../../utils/ComponentContext";
import {observer} from "mobx-react";
import {ModalState} from "../../utils/modalState";
import {action, computed, makeObservable, observable} from "mobx";
import {ReactiveValue} from "../../utils/reactive/reactiveValue";
import {required, strLength, strPattern} from "../../utils/reactive/validators";
import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap";
import {StringInput} from "../custom/controls/ReactiveControls";
import {post} from "../../utils/request";
export interface CreateGroupModalProps {
modalState: ModalState;
}
@observer
export class CreateGroupModal extends ComponentContext<CreateGroupModalProps> {
@observable name = new ReactiveValue<string>()
.addValidator(required)
.addValidator(strLength(3, 50))
.addValidator(strPattern(/^[а-яА-ЯёЁ0-9_-]*$/, "Имя группы должно содержать только русские буквы, дефис, нижнее подчеркивание и цифры"));
constructor(props: any) {
super(props);
makeObservable(this);
}
@action.bound
creationRequest() {
post<void>('/group/create-group', {name: this.name.value}).then(() => {
this.notificationStore.success(`Группа ${this.name.value} создана`);
}).finally(() => {
this.props.modalState.close();
});
}
@computed
get formInvalid() {
return this.name.invalid || !this.name.touched;
}
render() {
return <Modal show={this.props.modalState.isOpen}>
<ModalHeader>
<ModalTitle>Создание группы</ModalTitle>
</ModalHeader>
<ModalBody>
<StringInput value={this.name} label={'Имя группы'}/>
</ModalBody>
<ModalFooter>
<Button onClick={this.creationRequest} disabled={this.formInvalid}>Создать</Button>
<Button onClick={this.props.modalState.close}>Закрыть</Button>
</ModalFooter>
</Modal>
}
}

View File

@ -6,14 +6,18 @@ import {observer} from "mobx-react";
import {post} from "../../utils/request";
import {LoginModal} from "../user/LoginModal";
import {ModalState} from "../../utils/modalState";
import {action, makeObservable} from "mobx";
import {action, makeObservable, observable} from "mobx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {ComponentContext} from "../../utils/ComponentContext";
import {CreateGroupModal} from "../group/CreateGroupModal";
import {UserRegistrationModal} from "../user/UserRegistrationModal";
@observer
class Header extends ComponentContext {
loginModalState = new ModalState();
@observable loginModalState = new ModalState();
@observable createGroupModalState = new ModalState();
@observable userRegistrationModalState = new ModalState();
constructor(props: any) {
super(props);
@ -37,10 +41,17 @@ class Header extends ComponentContext {
user.authenticated && userStore.isAdministrator() &&
<NavDropdown title="Пользователи">
<NavDropdown.Item as={RouterLink} routeName={'userList'} children={'Список'}/>
<NavDropdown.Item as={RouterLink} routeName={'userRegistration'}
<NavDropdown.Item onClick={this.userRegistrationModalState.open}
children={'Зарегистрировать'}/>
</NavDropdown>
}
{
user.authenticated && userStore.isAdministrator() &&
<NavDropdown title="Группы">
<NavDropdown.Item as={RouterLink} routeName={'groupList'} children={'Список'}/>
<NavDropdown.Item onClick={this.createGroupModalState.open} children={'Добавить'}/>
</NavDropdown>
}
</Nav>
<Nav className="ms-auto">
@ -63,6 +74,8 @@ class Header extends ComponentContext {
</Navbar>
</header>
<LoginModal modalState={this.loginModalState}/>
<CreateGroupModal modalState={this.createGroupModalState}/>
<UserRegistrationModal modalState={this.userRegistrationModalState}/>
</>
}
}

View File

@ -1,7 +1,6 @@
import {observer} from "mobx-react";
import {DefaultPage} from "../layout/DefaultPage";
import {action, computed, makeObservable, observable} from "mobx";
import {Button, Col, Form, Row} from "react-bootstrap";
import {Button, Col, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle, Row} from "react-bootstrap";
import {UserRegistrationDTO} from "../../models/registration";
import {post} from "../../utils/request";
import {ReactiveValue} from "../../utils/reactive/reactiveValue";
@ -17,9 +16,15 @@ import {
phone,
required
} from "../../utils/reactive/validators";
import {ComponentContext} from "../../utils/ComponentContext";
import {ModalState} from "../../utils/modalState";
export interface UserRegistrationModalProps {
modalState: ModalState;
}
@observer
export class UserRegistration extends DefaultPage {
export class UserRegistrationModal extends ComponentContext<UserRegistrationModalProps> {
constructor(props: any) {
super(props);
makeObservable(this);
@ -31,7 +36,6 @@ export class UserRegistration extends DefaultPage {
@observable email = new ReactiveValue<string>().addValidator(required).addValidator(email);
@observable numberPhone = new ReactiveValue<string>().addValidator(required).addValidator(phone).setAuto('+7');
@observable accountType = new ReactiveValue<string>().addValidator(required).addValidator((value) => {
if (!['student', 'admin'].includes(value)) {
return 'Тип аккаунта должен быть "СТУДЕНТ" или "АДМИНИСТРАТОР"';
@ -64,9 +68,13 @@ export class UserRegistration extends DefaultPage {
});
}
get page() {
return <div className={'w-75 ms-auto me-auto'}>
<Form>
render() {
return <Modal show={this.props.modalState.isOpen} size={'lg'}>
<ModalHeader>
<ModalTitle>Регистрация пользователя</ModalTitle>
</ModalHeader>
<ModalBody>
<Row>
<Col>
<StringInput value={this.login} label={"Логин"}/>
@ -78,10 +86,12 @@ export class UserRegistration extends DefaultPage {
<StringInput value={this.numberPhone} label={"Телефон"}/>
</Col>
</Row>
<SelectButtonInput value={this.accountType} label={'Тип аккаунта'}/>
</ModalBody>
<ModalFooter>
<Button disabled={this.formInvalid} onClick={this.submit}>Зарегистрировать</Button>
</Form>
</div>
<Button onClick={this.props.modalState.close} variant={'secondary'}>Закрыть</Button>
</ModalFooter>
</Modal>
}
}

12
web/src/models/error.ts Normal file
View File

@ -0,0 +1,12 @@
export enum ErrorCode {
BUSINESS_ERROR = 'BUSINESS_ERROR',
VALIDATION_ERROR = 'VALIDATION_ERROR',
INTERNAL_ERROR = 'INTERNAL_ERROR',
NOT_FOUND = 'NOT_FOUND',
ACCESS_DENIED = 'ACCESS_DENIED',
}
export interface ErrorResponse {
message: string;
errorCode: ErrorCode;
}

View File

@ -9,9 +9,6 @@ export const routes: Route[] = [{
}, {
name: 'userList',
pattern: '/users',
}, {
name: 'userRegistration',
pattern: '/user-registration',
}, {
name: 'error',
pattern: '/error',

View File

@ -3,12 +3,10 @@ import Home from "../components/layout/Home";
import Error from "../components/layout/Error";
import UserProfilePage from "../components/user/UserProfilePage";
import {UserList} from "../components/user/UserList";
import {UserRegistration} from "../components/user/UserRegistration";
export const viewMap: ViewMap = {
root: <Home/>,
profile: <UserProfilePage/>,
userList: <UserList/>,
userRegistration: <UserRegistration/>,
error: <Error/>,
}

View File

@ -1,6 +1,4 @@
import {action, computed, makeObservable, observable, reaction} from "mobx";
import React from "react";
import _ from "lodash";
export class ReactiveValue<T> {
@ -95,33 +93,33 @@ export class ReactiveValue<T> {
}
}
export class NumberField extends ReactiveValue<number> {
constructor(fireImmediately: boolean = false) {
super(fireImmediately);
makeObservable(this);
this.addValidator(value => {
if (_.isNaN(value)) {
return 'Должно быть числом';
}
return null;
});
}
@action.bound
onChange(event: React.ChangeEvent<HTMLInputElement>) {
this.set(_.toNumber(event.currentTarget.value));
}
}
export class BooleanField extends ReactiveValue<boolean> {
constructor(fireImmediately: boolean = false) {
super(fireImmediately);
makeObservable(this);
}
@action.bound
onChange(event: React.ChangeEvent<HTMLInputElement>) {
this.set(event.currentTarget.checked);
}
}
// export class NumberField extends ReactiveValue<number> {
// constructor(fireImmediately: boolean = false) {
// super(fireImmediately);
// makeObservable(this);
// this.addValidator(value => {
// if (_.isNaN(value)) {
// return 'Должно быть числом';
// }
//
// return null;
// });
// }
//
// @action.bound
// onChange(event: React.ChangeEvent<HTMLInputElement>) {
// this.set(_.toNumber(event.currentTarget.value));
// }
// }
//
// export class BooleanField extends ReactiveValue<boolean> {
// constructor(fireImmediately: boolean = false) {
// super(fireImmediately);
// makeObservable(this);
// }
//
// @action.bound
// onChange(event: React.ChangeEvent<HTMLInputElement>) {
// this.set(event.currentTarget.checked);
// }
// }

View File

@ -3,17 +3,6 @@ export const required = (value: any, field = 'Поле') => {
return `${field} обязательно для заполнения`;
}
}
export const greaterThan = (min: number) => (value: number, field = 'Значение') => {
if (value && value <= min) {
return `${field} должно быть больше ${min}`;
}
}
export const lessThan = (max: number) => (value: number, field = 'Значение') => {
if (value && value >= max) {
return `${field} должно быть меньше ${max}`;
}
}
export const equals = (expected: any) => (value: any, field = 'Поле') => {
if (value && value !== expected) {
@ -21,13 +10,31 @@ export const equals = (expected: any) => (value: any, field = 'Поле') => {
}
}
export const length = (min: number, max: number) => (value: string, field = 'Поле') => {
export const number = (value: string, field = 'Поле') => {
if (!/^\d+$/.test(value)) {
return `${field} должно быть числом`;
}
}
export const numGreaterThan = (min: number) => (value: number, field = 'Значение') => {
if (value && value <= min) {
return `${field} должно быть больше ${min}`;
}
}
export const numLessThan = (max: number) => (value: number, field = 'Значение') => {
if (value && value >= max) {
return `${field} должно быть меньше ${max}`;
}
}
export const strLength = (min: number, max: number) => (value: string, field = 'Поле') => {
if (value && (value.length < min || value.length > max)) {
return `${field} должно содержать от ${min} до ${max} символов`;
}
}
export const pattern = (regexp: RegExp, message: string) => (value: string, field = '') => {
export const strPattern = (regexp: RegExp, message: string) => (value: string, field = '') => {
if (!regexp.test(value)) {
return message;
}
@ -81,12 +88,6 @@ export const nameLength = (value: string, field = 'Поле') => {
}
}
export const number = (value: string, field = 'Поле') => {
if (!/^\d+$/.test(value)) {
return `${field} должно быть числом`;
}
}
export const date = (value: string, field = 'Поле') => {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return `${field} должно быть датой в формате ГГГГ-ММ-ДД`;

View File

@ -1,5 +1,6 @@
import axios, {AxiosError, AxiosRequestConfig} from "axios";
import {NotificationService} from "../services/NotificationService";
import {ErrorResponse} from "../models/error";
export const apiUrl = "http://localhost:8080/api/v1/";
@ -21,7 +22,7 @@ export const request = async <R>(config: AxiosRequestConfig<any>, doReject: bool
resolve(response.data);
}).catch((error: AxiosError) => {
if (showError) {
if (error.response) NotificationService.error(JSON.stringify(error.response.data), 'Сервер вернул ошибку, код статуса: ' + error.response.status);
if (error.response) NotificationService.error((error.response.data as ErrorResponse).message, 'Сервер вернул ошибку, код статуса: ' + error.response.status);
else if (error.request) NotificationService.error(error.request, 'Не удалось получить ответ от сервера');
else NotificationService.error(error.message, 'Запрос отправить не удалось');
}