feat(ux): улучшение работы с датами validFrom/validTo
Backend (defensive): - GlobalExceptionHandler ловит HttpMessageNotReadableException → 400 c понятным сообщением вместо 500 'Internal server error'. - DateTimeParseException → code='invalid_date_format', сообщение с подсказкой ISO формата (например 2026-05-08T14:30:00+03:00). - InvalidFormatException → code='invalid_field_format' с именем сломанного поля. - Раньше падало 500 если фронт отправлял 'YYYY-MM-DD' вместо OffsetDateTime (из-за browser cache на старом bundle). Frontend: - Submit-валидация validFrom < validTo с inline error на validTo поле, переключение на таб 'Идентификация' с фокусом на ошибке. - DateTimeField теперь принимает hint и error props и показывает их под парой DatePicker+TextInput[time]. - Подсказки i18n под полями: 'Когда запись становится активной. Пусто = с момента создания.' / 'Когда перестаёт действовать. Пусто = бессрочно.' - Новая i18n ключ form.error.validToBeforeFrom (RU/EN).
This commit is contained in:
+28
@@ -1,15 +1,19 @@
|
||||
package cloud.nstart.terravault.ordinis.restapi.error;
|
||||
|
||||
import cloud.nstart.terravault.ordinis.validation.ValidationException;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
|
||||
import io.micrometer.tracing.Tracer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestControllerAdvice
|
||||
@@ -45,6 +49,30 @@ public class GlobalExceptionHandler {
|
||||
return ResponseEntity.badRequest().body(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Невалидный JSON или несовместимый формат поля (напр. дата без времени для OffsetDateTime).
|
||||
* Возвращаем 400 с понятным сообщением вместо 500.
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ResponseEntity<ApiErrorResponse> onMessageNotReadable(HttpMessageNotReadableException e) {
|
||||
String message = "Невалидный JSON в теле запроса";
|
||||
String code = "invalid_request_body";
|
||||
Throwable root = e.getRootCause() != null ? e.getRootCause() : e.getCause();
|
||||
if (root instanceof DateTimeParseException dtpe) {
|
||||
message = "Поле даты не распарсилось: '" + dtpe.getParsedString()
|
||||
+ "'. Ожидается ISO-8601 datetime, например 2026-05-08T14:30:00+03:00.";
|
||||
code = "invalid_date_format";
|
||||
} else if (e.getCause() instanceof InvalidFormatException ife) {
|
||||
String field = ife.getPath().isEmpty()
|
||||
? "?"
|
||||
: ife.getPath().get(ife.getPath().size() - 1).getFieldName();
|
||||
message = "Поле '" + field + "' не распарсилось. Ожидается " + ife.getTargetType().getSimpleName() + ".";
|
||||
code = "invalid_field_format";
|
||||
}
|
||||
var body = ApiErrorResponse.of(400, code, message, currentTraceId());
|
||||
return ResponseEntity.badRequest().body(body);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiErrorResponse> onGeneric(Exception e) {
|
||||
log.error("Unhandled exception in REST API", e);
|
||||
|
||||
Reference in New Issue
Block a user