From 8b37c96671eea68ce80cd2be00f0bfa86a8c9b32 Mon Sep 17 00:00:00 2001 From: Nessar Date: Fri, 24 Jul 2026 19:31:00 +0200 Subject: [PATCH] Align rent revision validation flows (#56) --- .../adapter/in/web/rent/RentRevisionApi.java | 13 ++- .../in/web/rent/RentRevisionController.java | 76 ++++++++++-- .../application/rent/RentRevisionService.java | 9 +- .../rent/RentRevisionValidationFailure.java | 24 ++++ .../in/web/rent/RentRevisionWebTest.java | 77 ++++++++++++- .../rent-revision-application.css | 4 + .../rent-revision-application.html | 16 ++- .../rent-revision-application.ts | 73 +++++++++++- .../rent-revision-preview.css | 6 + .../rent-revision-preview.html | 28 ++++- .../rent-revision-preview.spec.ts | 11 +- .../rent-revision-preview.ts | 109 ++++++++++++++++-- .../rent-revision-history.state.ts | 8 +- .../rent-revision-proposals.css | 5 + .../rent-revision-proposals.html | 26 ++++- .../rent-revision-proposals.spec.ts | 1 + .../rent-revision-proposals.ts | 92 +++++++++++++-- 17 files changed, 519 insertions(+), 59 deletions(-) create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentRevisionValidationFailure.java diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionApi.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionApi.java index 238f77f..2c3cbb8 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionApi.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionApi.java @@ -5,6 +5,7 @@ import fr.nessar.lmnporganizer.rental.domain.rent.RentRevision; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.Size; import java.math.BigDecimal; import java.time.Instant; @@ -15,6 +16,12 @@ import java.util.Set; final class RentRevisionApi { private RentRevisionApi() {} +record OperationError(String message) {} + +record FieldError(String field, String code) {} + +record ValidationError(String code, List errors) {} + record PreviewRequest( @NotBlank String timeZone, @Size(max = 9) String seriesIdOverride, @@ -24,7 +31,7 @@ record PreviewRequest( Boolean manualIndicesConfirmed) { RentRevisionService.PreviewRequest toApplication() { return new RentRevisionService.PreviewRequest( - RentPaymentController.timeZone(timeZone), + RentRevisionController.timeZone(timeZone), seriesIdOverride, Boolean.TRUE.equals(seriesOverrideConfirmed), previousIndex, @@ -37,7 +44,7 @@ record ProposalRequest( @NotBlank String timeZone, @NotNull LocalDate requestedOn, @NotNull @Positive BigDecimal chosenRent, - @Positive BigDecimal chosenCharges, + @PositiveOrZero BigDecimal chosenCharges, List<@NotBlank String> confirmedWarningCodes, @Size(max = 9) String seriesIdOverride, Boolean seriesOverrideConfirmed, @@ -47,7 +54,7 @@ record ProposalRequest( RentRevisionService.ProposalRequest toApplication() { var preview = new RentRevisionService.PreviewRequest( - RentPaymentController.timeZone(timeZone), + RentRevisionController.timeZone(timeZone), seriesIdOverride, Boolean.TRUE.equals(seriesOverrideConfirmed), previousIndex, diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionController.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionController.java index 5712958..f585e99 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionController.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionController.java @@ -2,6 +2,7 @@ package fr.nessar.lmnporganizer.rental.adapter.in.web.rent; import fr.nessar.lmnporganizer.rental.adapter.in.web.common.AuthenticatedLandlord; import fr.nessar.lmnporganizer.rental.application.rent.RentRevisionService; +import fr.nessar.lmnporganizer.rental.application.rent.RentRevisionValidationFailure; import fr.nessar.lmnporganizer.rental.application.shared.AttachmentStorageFailure; import fr.nessar.lmnporganizer.rental.domain.lease.LeaseId; import fr.nessar.lmnporganizer.rental.domain.rent.RentRevision; @@ -9,16 +10,21 @@ import fr.nessar.lmnporganizer.rental.domain.rent.RentRevisionId; import jakarta.validation.Valid; import java.io.IOException; import java.time.Clock; +import java.time.DateTimeException; import java.time.LocalDate; import java.time.ZoneId; +import java.time.format.DateTimeParseException; import java.util.List; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -27,7 +33,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.support.MissingServletRequestPartException; @RestController @RequestMapping("/api/leases/{leaseId}/rent-revisions") @@ -43,6 +51,28 @@ public RentRevisionController( this.clock = clock; } +@ExceptionHandler(RentRevisionValidationFailure.class) +ResponseEntity invalid(RentRevisionValidationFailure failure) { + return ResponseEntity.badRequest() + .body( + new RentRevisionApi.ValidationError( + "validation_failed", + failure.errors().stream() + .map(error -> new RentRevisionApi.FieldError(error.field(), error.code())) + .toList())); +} + +@ExceptionHandler({ + MissingServletRequestParameterException.class, + MissingServletRequestPartException.class, + MethodArgumentTypeMismatchException.class, + HttpMessageNotReadableException.class +}) +ResponseEntity invalidRequest() { + return ResponseEntity.badRequest() + .body(new RentRevisionApi.OperationError("La requête de révision IRL est invalide.")); +} + @PostMapping("/preview") RentRevisionApi.PreviewResponse preview( @AuthenticationPrincipal OidcUser principal, @@ -118,18 +148,22 @@ RentRevisionApi.ApplyResponse apply( @PathVariable String leaseId, @PathVariable String revisionId, @RequestParam String timeZone, - @RequestParam LocalDate sentOn, + @RequestParam String sentOn, @RequestParam String deliveryMethod, - @RequestParam boolean deliveryConfirmed, + @RequestParam String deliveryConfirmed, @RequestPart(value = "proof", required = false) MultipartFile proof) { - ZoneId zone = RentPaymentController.timeZone(timeZone); + ZoneId zone = timeZone(timeZone); RentRevisionService.ApplyResult result = revisions.apply( authenticatedLandlord.id(principal), new LeaseId(leaseId), new RentRevisionId(revisionId), new RentRevisionService.ApplyRequest( - zone, sentOn, deliveryMethod(deliveryMethod), deliveryConfirmed, upload(proof)), + zone, + date(sentOn), + deliveryMethod(deliveryMethod), + deliveryConfirmed(deliveryConfirmed), + upload(proof)), principal.getSubject()); LocalDate today = LocalDate.now(clock.withZone(zone)); return new RentRevisionApi.ApplyResponse( @@ -150,16 +184,40 @@ private static RentRevisionService.PdfUpload upload(MultipartFile file) { } private static RentRevision.DeliveryMethod deliveryMethod(String value) { - if (value == null) { - throw new IllegalArgumentException("Le mode d'envoi est invalide."); - } - return switch (value) { + if (value == null) throw new RentRevisionValidationFailure("deliveryMethod", "invalid"); + return switch (value.trim()) { case "REGISTERED_LETTER" -> RentRevision.DeliveryMethod.REGISTERED_LETTER; case "POSTAL_LETTER" -> RentRevision.DeliveryMethod.POSTAL_LETTER; case "HAND_DELIVERY" -> RentRevision.DeliveryMethod.HAND_DELIVERY; case "EMAIL" -> RentRevision.DeliveryMethod.EMAIL; case "OTHER" -> RentRevision.DeliveryMethod.OTHER; - default -> throw new IllegalArgumentException("Le mode d'envoi est invalide."); + default -> throw new RentRevisionValidationFailure("deliveryMethod", "invalid"); }; } + +static ZoneId timeZone(String value) { + if (value == null || value.isBlank()) { + throw new RentRevisionValidationFailure("timeZone", "iana_time_zone"); + } + try { + return ZoneId.of(value); + } catch (DateTimeException failure) { + throw new RentRevisionValidationFailure("timeZone", "iana_time_zone"); + } +} + +private static LocalDate date(String value) { + try { + return LocalDate.parse(value); + } catch (DateTimeParseException failure) { + throw new RentRevisionValidationFailure("sentOn", "invalid"); + } +} + +private static boolean deliveryConfirmed(String value) { + if ("true".equals(value)) return true; + if ("false".equals(value)) return false; + throw new RentRevisionValidationFailure("deliveryConfirmed", "invalid"); +} + } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentRevisionService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentRevisionService.java index 8f817f5..79e758c 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentRevisionService.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentRevisionService.java @@ -213,12 +213,11 @@ public ApplyResult apply( Objects.requireNonNull(request, "Rent revision application request is required."); String author = required(authorId, "Application author"); if (!request.deliveryConfirmed()) { - throw new RentRuleViolation("L'envoi de la demande doit être confirmé."); + throw new RentRevisionValidationFailure("deliveryConfirmed", "required"); } if (request.sentOn() == null || request.sentOn().isAfter(LocalDate.now(clock.withZone(request.timeZone())))) { - throw new RentRuleViolation( - "La date d'envoi ne peut pas être future dans le fuseau du bailleur."); + throw new RentRevisionValidationFailure("sentOn", "future"); } RentRevision proposed = revisions @@ -639,7 +638,7 @@ private static String sha256(byte[] content) { private byte[] pdf(PdfUpload upload) { byte[] content = upload.content(); if (content == null || content.length == 0) { - throw new RentRuleViolation("Le justificatif PDF est vide."); + throw new RentRevisionValidationFailure("proof", "invalid"); } if (content.length > maximumPdfSize) { throw new AttachmentTooLarge(maximumPdfSize); @@ -647,7 +646,7 @@ private byte[] pdf(PdfUpload upload) { if (!"application/pdf".equalsIgnoreCase(upload.contentType()) || content.length < 5 || !Arrays.equals(Arrays.copyOf(content, 5), "%PDF-".getBytes(StandardCharsets.US_ASCII))) { - throw new RentRuleViolation("Le justificatif doit être un PDF lisible."); + throw new RentRevisionValidationFailure("proof", "invalid"); } return content; } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentRevisionValidationFailure.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentRevisionValidationFailure.java new file mode 100644 index 0000000..9ebeff1 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentRevisionValidationFailure.java @@ -0,0 +1,24 @@ +package fr.nessar.lmnporganizer.rental.application.rent; + +import java.util.List; +import java.util.Objects; + +public final class RentRevisionValidationFailure extends RuntimeException { +private final List errors; + +public RentRevisionValidationFailure(String field, String code) { + super("La requête de révision IRL est invalide."); + this.errors = List.of(new FieldError(field, code)); +} + +public List errors() { + return errors; +} + +public record FieldError(String field, String code) { + public FieldError { + Objects.requireNonNull(field); + Objects.requireNonNull(code); + } +} +} diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionWebTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionWebTest.java index c65095a..295b413 100644 --- a/backend/src/test/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionWebTest.java +++ b/backend/src/test/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentRevisionWebTest.java @@ -1,6 +1,7 @@ package fr.nessar.lmnporganizer.rental.adapter.in.web.rent; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -19,6 +20,7 @@ import fr.nessar.lmnporganizer.rental.adapter.in.web.common.AuthenticatedLandlor import fr.nessar.lmnporganizer.rental.adapter.in.web.common.RentalApiExceptionHandler; import fr.nessar.lmnporganizer.rental.application.landlord.LandlordAccountService; import fr.nessar.lmnporganizer.rental.application.rent.RentRevisionService; +import fr.nessar.lmnporganizer.rental.application.rent.RentRevisionValidationFailure; import fr.nessar.lmnporganizer.rental.application.shared.IrlIndexUnavailable; import fr.nessar.lmnporganizer.rental.domain.landlord.ExternalIdentity; import fr.nessar.lmnporganizer.rental.domain.landlord.Landlord; @@ -147,7 +149,7 @@ void unavailableOfficialAndCachedIndicesExposeAStableManualFallbackCode() throws } @Test -void rejectsAnUnknownDeliveryMethodWithoutExposingInternalClassNames() throws Exception { +void rejectsAnUnknownDeliveryMethodWithAStableFieldCode() throws Exception { mockMvc .perform( multipart("/api/leases/lease-1/rent-revisions/revision-1/apply") @@ -158,7 +160,55 @@ void rejectsAnUnknownDeliveryMethodWithoutExposingInternalClassNames() throws Ex .with(owner()) .with(csrf())) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Le mode d'envoi est invalide.")); + .andExpect(jsonPath("$.code").value("validation_failed")) + .andExpect(jsonPath("$.errors[0].field").value("deliveryMethod")) + .andExpect(jsonPath("$.errors[0].code").value("invalid")); +} + +@Test +void translatesInvalidDeliveryFactsAndProofMetadataToFieldErrors() throws Exception { + assertApplyViolation( + "false", + null, + "deliveryConfirmed", + "required"); + assertApplyViolation( + "true", + null, + "sentOn", + "future"); + assertApplyViolation( + "true", + new MockMultipartFile("proof", "preuve.pdf", "application/pdf", "not a PDF".getBytes()), + "proof", + "invalid"); +} + +@Test +void rejectsAnInvalidPreviewTimeZoneWithAStableFieldCode() throws Exception { + mockMvc + .perform( + post("/api/leases/lease-1/rent-revisions/preview") + .with(owner()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"timeZone\":\"invalid-zone\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("validation_failed")) + .andExpect(jsonPath("$.errors[0].field").value("timeZone")) + .andExpect(jsonPath("$.errors[0].code").value("iana_time_zone")); +} + +@Test +void rejectsMissingMultipartParametersWithASafeOperationMessage() throws Exception { + mockMvc + .perform( + multipart("/api/leases/lease-1/rent-revisions/revision-1/apply") + .param("timeZone", "Europe/Paris") + .with(owner()) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("La requête de révision IRL est invalide.")); } @Test @@ -342,4 +392,27 @@ private static RequestPostProcessor owner() { .claim("family_name", "Martin") .claim("email", "camille@example.com")); } + +private void assertApplyViolation( + String deliveryConfirmed, MockMultipartFile proof, String field, String code) + throws Exception { + doThrow(new RentRevisionValidationFailure(field, code)) + .when(revisions) + .apply(any(), any(), any(), any(), any()); + var request = multipart("/api/leases/lease-1/rent-revisions/revision-1/apply"); + if (proof != null) request.file(proof); + mockMvc + .perform( + request + .param("timeZone", "Europe/Paris") + .param("sentOn", "2027-09-02") + .param("deliveryMethod", "POSTAL_LETTER") + .param("deliveryConfirmed", deliveryConfirmed) + .with(owner()) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("validation_failed")) + .andExpect(jsonPath("$.errors[0].field").value(field)) + .andExpect(jsonPath("$.errors[0].code").value(code)); +} } diff --git a/frontend/src/app/components/rent-revision-application/rent-revision-application.css b/frontend/src/app/components/rent-revision-application/rent-revision-application.css index 7e9b918..5b844ca 100644 --- a/frontend/src/app/components/rent-revision-application/rent-revision-application.css +++ b/frontend/src/app/components/rent-revision-application/rent-revision-application.css @@ -61,6 +61,10 @@ input[type='file'] { color: var(--danger, #b42318); } +.field-error { + color: var(--danger, #b42318); +} + .apply-submit { justify-self: start; } diff --git a/frontend/src/app/components/rent-revision-application/rent-revision-application.html b/frontend/src/app/components/rent-revision-application/rent-revision-application.html index 0439065..b5b8b46 100644 --- a/frontend/src/app/components/rent-revision-application/rent-revision-application.html +++ b/frontend/src/app/components/rent-revision-application/rent-revision-application.html @@ -13,29 +13,43 @@
+ @if (fieldError('deliveryConfirmed')) { + {{ fieldError('deliveryConfirmed') }} + } @if (validationError() || error()) { } diff --git a/frontend/src/app/components/rent-revision-application/rent-revision-application.ts b/frontend/src/app/components/rent-revision-application/rent-revision-application.ts index 4bd4fd8..069cb4b 100644 --- a/frontend/src/app/components/rent-revision-application/rent-revision-application.ts +++ b/frontend/src/app/components/rent-revision-application/rent-revision-application.ts @@ -1,4 +1,4 @@ -import { Component, DestroyRef, inject, input, output, signal } from '@angular/core'; +import { Component, DestroyRef, ElementRef, inject, input, output, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { FormField, FormRoot, form } from '@angular/forms/signals'; @@ -10,7 +10,7 @@ import { RentRevisionDeliveryMethod, RentRevisionResponse, } from '../../services/lease.service'; -import { httpErrorMessage } from '../../services/http-error-message'; +import { FieldValidationError, saveFailure } from '../../services/http-error-message'; const frenchDate = new Intl.DateTimeFormat('fr-FR', { day: 'numeric', @@ -28,6 +28,7 @@ const frenchDate = new Intl.DateTimeFormat('fr-FR', { export class RentRevisionApplicationComponent { private readonly leases = inject(LeaseService); private readonly destroyRef = inject(DestroyRef); + private readonly host = inject(ElementRef); readonly leaseId = input.required(); readonly revision = input.required(); @@ -41,6 +42,7 @@ export class RentRevisionApplicationComponent { readonly pending = signal(false); readonly validationError = signal(''); readonly error = signal(''); + readonly fieldErrors = signal([]); readonly deliveryMethods = [ { value: 'REGISTERED_LETTER', label: 'Lettre recommandée' }, { value: 'POSTAL_LETTER', label: 'Lettre postale' }, @@ -65,6 +67,7 @@ export class RentRevisionApplicationComponent { this.pending.set(true); this.validationError.set(''); this.error.set(''); + this.fieldErrors.set([]); const proof = this.proof(); this.leases .applyRentRevision(this.leaseId(), this.revision().id, { @@ -80,14 +83,45 @@ export class RentRevisionApplicationComponent { ) .subscribe({ next: (response) => this.applied.emit(response), - error: (error: unknown) => + error: (error: unknown) => { + const failure = saveFailure( + error, + 'Impossible d’appliquer cette révision de loyer.', + ); + const visibleErrors = failure.fieldErrors.filter((fieldError) => + this.isKnownField(fieldError.field), + ); + this.fieldErrors.set(visibleErrors); this.error.set( - httpErrorMessage(error, 'Impossible d’appliquer cette révision de loyer.'), - ), + visibleErrors.length === failure.fieldErrors.length + ? failure.message + : this.operationError(failure.fieldErrors), + ); + this.focusFirstFieldError(); + }, }); } + clearFieldError(field: string): void { + if (!this.fieldErrors().some((error) => error.field === field)) return; + const remaining = this.fieldErrors().filter((error) => error.field !== field); + this.fieldErrors.set(remaining); + if (remaining.length === 0) this.error.set(''); + } + + fieldError(field: string): string { + if (!this.fieldErrors().some((error) => error.field === field)) return ''; + return ( + { + deliveryMethod: 'Choisissez un mode d’envoi valide.', + deliveryConfirmed: 'Confirmez l’envoi de la lettre.', + proof: 'Choisissez une preuve PDF lisible.', + }[field] ?? 'Vérifiez cette valeur.' + ); + } + selectProof(event: Event): void { + this.clearFieldError('proof'); const input = event.target; const file = input instanceof HTMLInputElement ? (input.files?.item(0) ?? null) : null; if (file && file.name.toLowerCase().endsWith('.pdf') && file.type === 'application/pdf') { @@ -104,4 +138,33 @@ export class RentRevisionApplicationComponent { const [year, month, day] = date.split('-').map(Number); return frenchDate.format(new Date(Date.UTC(year, month - 1, day))); } + + private isKnownField(field: string): boolean { + return ['deliveryMethod', 'deliveryConfirmed', 'proof'].includes(field); + } + + private operationError(errors: FieldValidationError[]): string { + return errors.some((error) => error.field === 'sentOn') + ? 'La date d’envoi de cette proposition est invalide.' + : errors.some((error) => error.field === 'timeZone') + ? 'Le fuseau horaire du navigateur est invalide.' + : 'Impossible d’appliquer cette révision de loyer.'; + } + + private focusFirstFieldError(): void { + const selector = this.fieldErrors() + .map( + (error) => + ({ + deliveryMethod: '.delivery-method', + deliveryConfirmed: '.delivery-confirmed', + proof: 'input[type=file]', + })[error.field], + ) + .find((value): value is string => typeof value === 'string'); + if (selector) + queueMicrotask(() => + (this.host.nativeElement.querySelector(selector) as HTMLElement | null)?.focus(), + ); + } } diff --git a/frontend/src/app/components/rent-revision-preview/rent-revision-preview.css b/frontend/src/app/components/rent-revision-preview/rent-revision-preview.css index d042db4..a17a81a 100644 --- a/frontend/src/app/components/rent-revision-preview/rent-revision-preview.css +++ b/frontend/src/app/components/rent-revision-preview/rent-revision-preview.css @@ -108,6 +108,12 @@ input[type='number'] { color: var(--danger, #b42318); } +.field-error, +.stale-preview { + color: var(--danger, #b42318); + font-size: 0.75rem; +} + .preview-warnings ul { display: grid; gap: var(--space-2, 0.5rem); diff --git a/frontend/src/app/components/rent-revision-preview/rent-revision-preview.html b/frontend/src/app/components/rent-revision-preview/rent-revision-preview.html index 51224a0..02148f8 100644 --- a/frontend/src/app/components/rent-revision-preview/rent-revision-preview.html +++ b/frontend/src/app/components/rent-revision-preview/rent-revision-preview.html @@ -17,7 +17,11 @@ Changer la série INSEE @if (requestModel().seriesIdOverride.trim()) {