From 0976da90be3f4ac325c4377d1299841313274f73 Mon Sep 17 00:00:00 2001 From: mathieu Date: Fri, 21 Aug 2026 14:07:51 +0200 Subject: [PATCH] Sors du chemin de la consultation d'une revue ce qui la modifie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La fiche d'une revue portait tout à la fois : ses numéros, le formulaire d'ajout, les champs de la revue et sa suppression. Elle prend la forme déjà tenue par la fiche d'un livre et par celle d'une série — une route pour regarder, une route pour changer. « Ajouter un numéro » et « Modifier la revue » remontent à droite du titre, dans le bloc d'en-tête partagé avec la fiche d'une série : les deux actions sont visibles sans descendre au bas d'une collection de trente numéros. ⚠️ Modifier et retirer un numéro n'existent plus QUE dans /revues/{id}/edition. En consultation, ces deux boutons se déclenchaient sous le pouce en faisant défiler la liste — même raison que les flèches d'ordre d'une série, reléguées sur leur propre écran. ⚠️ Ajouter un numéro, lui, RESTE en consultation, et ce n'est pas une entorse : le geste ne touche pas à la fiche de la revue, il range un objet de plus, comme noter un prêt depuis la fiche d'un livre. Le formulaire se replie derrière son bouton, et s'ouvre de lui-même quand on arrive du scanner avec un numéro lu sur l'add-on EAN-2 : demander un clic de plus pour saisir ce qu'on tient en main serait un détour. Un retrait de numéro se confirme désormais, comme la suppression d'un livre ou d'une revue : il ne se défait pas, et la liste s'égrène sous le pouce. ⚠️ Les deux routes partagent le paramètre {id} : le routeur ne redessine rien en passant de l'une à l'autre, d'où l'abonnement à LocationChanged — piège déjà rencontré sur la fiche livre. Quitter l'édition referme au passage ce qui n'a de sens que là : formulaire de numéro ouvert, retrait ou suppression en attente de confirmation. /revues/{id}/edition entre dans la table de RemonteeRoutes : elle retombait jusqu'ici sur le repli de branche, donc sur la liste des revues au lieu de la fiche qu'on venait de quitter. Deux tests de service viennent avec, sur ce dont l'écran dépend sans le recalculer : la revue rendue par la modification d'un numéro revient déjà rangée (corriger une parution déplace le numéro, les numéros sans date fermant toujours la liste), et un retrait ne touche qu'au numéro visé. 605 tests au vert. Pas de vérification en navigateur : le rendu repose sur la relecture. Co-Authored-By: Claude Opus 5 --- MaBibli.Client/Pages/Revue.razor | 326 +++++++++++++++------- MaBibli.Client/Services/RemonteeRoutes.cs | 1 + MaBibli.Tests/RemonteeRoutesTests.cs | 3 +- MaBibli.Tests/ServiceRevuesTests.cs | 59 ++++ 4 files changed, 287 insertions(+), 102 deletions(-) diff --git a/MaBibli.Client/Pages/Revue.razor b/MaBibli.Client/Pages/Revue.razor index cf5ef6c..7f04f3f 100644 --- a/MaBibli.Client/Pages/Revue.razor +++ b/MaBibli.Client/Pages/Revue.razor @@ -1,4 +1,5 @@ @page "/revues/{Id:int}" +@page "/revues/{Id:int}/edition" @inject ServiceLivresApi Api @inject NavigationManager Navigation @inject EtatReseau Reseau @@ -14,26 +15,139 @@ } else { -

@_revue.Titre

+ @* + Titre, identité de la revue et actions sur la MÊME ligne (lot W) : disposition + symétrique de celle de la fiche d'une série, et déjà tenue par la bibliographie et la + liste des auteurs. Elle rend les deux actions visibles sans descendre au bas d'une + collection de trente numéros. + *@ +
+
+

@_revue.Titre

-

- @if (!string.IsNullOrWhiteSpace(_revue.Issn)) +

+ @if (!string.IsNullOrWhiteSpace(_revue.Issn)) + { + @* K2 : un ISSN se coupe TOUJOURS au même endroit, contrairement aux tranches + d'un ISBN. Aucune table à embarquer, et aucune coupure ne peut être fausse. *@ + ISSN @FormatageIssn.Afficher(_revue.Issn) + } + @if (!string.IsNullOrWhiteSpace(_revue.Editeur)) + { + @_revue.Editeur + } +

+
+ + @if (!ModeEdition) { - @* K2 : un ISSN se coupe TOUJOURS au même endroit, contrairement aux tranches d'un - ISBN. Aucune table à embarquer, et aucune coupure ne peut être fausse. *@ - ISSN @FormatageIssn.Afficher(_revue.Issn) +
+ @* Ajouter un numéro reste en CONSULTATION, et ce n'est pas une entorse à + « consulter d'abord, modifier ensuite » : le geste ne touche pas à la fiche de + la revue, il range un objet de plus — comme noter un prêt depuis la fiche d'un + livre. Ce sont la modification et le retrait d'un numéro qui vivent, elles, + uniquement dans l'écran d'édition. *@ + + + @if (Reseau.EnLigne) + { + Modifier la revue + } + else + { + + } +
} - @if (!string.IsNullOrWhiteSpace(_revue.Editeur)) - { - @_revue.Editeur - } -

+
@if (_erreur is not null) { } + @if (!ModeEdition && _ajout) + { +

Ajouter un numéro

+ + @* + ⚠️ Le numéro se saisit à la main, et ce n'est pas un oubli : les deux chiffres de + parution du code-barres ne sont PAS un numéro fiable (CLAUDE.md), et l'add-on EAN-2 + qui le porterait vraiment n'a pas pu être vérifié sur un magazine réel. + *@ + @if (_numeroPropose && !string.IsNullOrWhiteSpace(NumeroScanne)) + { +

+ Le code-barres portait « @NumeroScanne » à côté du code principal, ce qui est + normalement le numéro de parution. Vérifiez-le sur la couverture + avant d'enregistrer. +

+ } + + + +
+ +
+ } + + @if (ModeEdition) + { +

La revue

+ + + + + + + +
+ + + +
+ + @if (_confirmeSuppression) + { +

+ Les @_revue.Numeros.Count numéro(s) recensés disparaîtront avec elle : ils + n'existent que par leur revue. +

+ } + } +

Numéros possédés

@if (_revue.Numeros.Count == 0) @@ -46,12 +160,13 @@ else { @* Du plus récent au plus ancien : devant un kiosque, la question est « ai-je le dernier ? », pas « par quoi ai-je commencé ». C'est l'inverse d'une série, qui suit - son ordre de lecture. *@ + son ordre de lecture. L'ordre est celui rendu par l'API — les numéros sans date + ferment la liste, une date inconnue n'étant pas une date ancienne. *@
    @foreach (var numero in _revue.Numeros) {
  • - @if (_edition == numero.Id) + @if (ModeEdition && _edition == numero.Id) {
    @@ -65,7 +180,7 @@ else
    @@ -112,96 +227,37 @@ else } -
    - + @* + ⚠️ Modifier et retirer un numéro n'existent QUE dans l'écran + d'édition : en consultation, ces deux boutons se déclenchaient sous + le pouce en faisant défiler la collection — même raison que les + flèches d'ordre d'une série, reléguées sur leur propre écran. + *@ + @if (ModeEdition) + { +
    + - -
    + @* Un retrait est destructeur et ne se défait pas : il se + confirme, comme la suppression d'un livre ou d'une revue. *@ + +
    + } }
  • }
} - -

Ajouter un numéro

- - @* - ⚠️ Le numéro se saisit à la main, et ce n'est pas un oubli : les deux chiffres de - parution du code-barres ne sont PAS un numéro fiable (CLAUDE.md), et l'add-on EAN-2 qui - le porterait vraiment n'a pas pu être vérifié sur un magazine réel. - *@ - @if (_numeroPropose && !string.IsNullOrWhiteSpace(NumeroScanne)) - { -

- Le code-barres portait « @NumeroScanne » à côté du code principal, ce qui est - normalement le numéro de parution. Vérifiez-le sur la couverture - avant d'enregistrer. -

- } - - - -
- -
- -

La revue

- - - - - - - -
- - - -
- - @if (_confirmeSuppression) - { -

- Les @_revue.Numeros.Count numéro(s) recensés disparaîtront avec elle : ils n'existent - que par leur revue. -

- } } @code { @@ -232,16 +288,54 @@ else private string? _issn; private string? _editeur; - /// Saisie du numéro qu'on ajoute. + /// Saisie du numéro qu'on ajoute, et repli du formulaire qui la porte. private AjoutNumeroRevue _saisie = new(); + private bool _ajout; /// Numéro en cours de modification, et sa saisie — null = aucun. private int? _edition; private AjoutNumeroRevue _saisieEdition = new(); + /// Numéro dont le retrait attend confirmation — null = aucun. + private int? _confirmeRetrait; + + /// + /// Écran d'édition de la revue : nom, ISSN, éditeur, suppression, et la seule place où un + /// numéro se modifie ou se retire. + /// + /// + /// ⚠️ Le mode est dans l'ADRESSE, pas dans un booléen interne : convention du projet depuis + /// la fiche livre. Comme les deux routes partagent le même paramètre Id, le routeur + /// ne déclenche aucun nouveau rendu en passant de l'une à l'autre — d'où l'abonnement + /// à LocationChanged, sans lequel l'URL changerait et l'écran resterait le même. + /// + private bool ModeEdition => + Navigation.ToAbsoluteUri(Navigation.Uri).AbsolutePath.TrimEnd('/') + .EndsWith("/edition", StringComparison.OrdinalIgnoreCase); + private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne; - protected override void OnInitialized() => Reseau.Change += SurChangementReseau; + protected override void OnInitialized() + { + Reseau.Change += SurChangementReseau; + Navigation.LocationChanged += SurNavigation; + } + + /// + /// Quitter l'édition referme ce qui n'a de sens que là : un formulaire de numéro resté + /// ouvert, et une confirmation de retrait en attente. + /// + private void SurNavigation(object? _, LocationChangedEventArgs __) => InvokeAsync(() => + { + if (!ModeEdition) + { + _edition = null; + _confirmeRetrait = null; + _confirmeSuppression = false; + } + + StateHasChanged(); + }); private void SurChangementReseau() => _ = InvokeAsync(async () => { @@ -249,7 +343,11 @@ else StateHasChanged(); }); - public void Dispose() => Reseau.Change -= SurChangementReseau; + public void Dispose() + { + Reseau.Change -= SurChangementReseau; + Navigation.LocationChanged -= SurNavigation; + } protected override Task OnParametersSetAsync() => ChargerAsync(); @@ -266,13 +364,22 @@ else _editeur = _revue.Editeur; } + // Un numéro lu au code-barres ouvre le formulaire de lui-même : on vient de scanner, + // demander un clic de plus pour saisir ce qu'on tient en main serait un détour. if (!_numeroPropose && !string.IsNullOrWhiteSpace(NumeroScanne)) { _numeroPropose = true; _saisie.Numero = NumeroScanne.Trim(); + _ajout = true; } } + private void BasculerAjout() + { + _erreur = null; + _ajout = !_ajout; + } + private void Appliquer(RevueDto revue) { _revue = revue; @@ -318,6 +425,7 @@ else private void OuvrirEdition(NumeroRevueDto numero) { _erreur = null; + _confirmeRetrait = null; _edition = numero.Id; _saisieEdition = new AjoutNumeroRevue { @@ -329,6 +437,8 @@ else }; } + private void FermerEdition() => _edition = null; + private async Task EnregistrerNumeroAsync() { if (_edition is not { } numeroId @@ -358,9 +468,19 @@ else private async Task RetirerAsync(NumeroRevueDto numero) { + // Premier clic : on demande confirmation. Un numéro retiré ne se récupère pas, et la + // liste s'égrène sous le pouce. + if (_confirmeRetrait != numero.Id) + { + _erreur = null; + _confirmeRetrait = numero.Id; + return; + } + _enCours = true; _erreur = await Api.RetirerNumeroAsync(numero.Id); _enCours = false; + _confirmeRetrait = null; // Le numéro retiré était peut-être celui qu'on modifiait : un formulaire ouvert sur une // ligne disparue n'aurait plus rien à enregistrer. @@ -392,6 +512,10 @@ else } Appliquer(resultat.Valeur!); + + // La fiche est à jour : on retombe sur la consultation, comme après l'édition d'un + // livre ou d'une série. Rester en édition inviterait à réenregistrer. + Navigation.NavigateTo($"revues/{Id}"); } private async Task SupprimerAsync() diff --git a/MaBibli.Client/Services/RemonteeRoutes.cs b/MaBibli.Client/Services/RemonteeRoutes.cs index 685c569..d983a71 100644 --- a/MaBibli.Client/Services/RemonteeRoutes.cs +++ b/MaBibli.Client/Services/RemonteeRoutes.cs @@ -67,6 +67,7 @@ public static class RemonteeRoutes // Revues. ⚠️ « /revues/ajout » est écrit AVANT « /revues/{id} » : « ajout » n'est pas un // identifiant, mais l'ordre de lecture est ce qui le garantit sans ambiguïté. ("/revues/ajout", "/revues"), + ("/revues/{id}/edition", "/revues/{id}"), ("/revues/{id}", "/revues"), ("/revues", Racine), diff --git a/MaBibli.Tests/RemonteeRoutesTests.cs b/MaBibli.Tests/RemonteeRoutesTests.cs index 7b91cda..a2536fc 100644 --- a/MaBibli.Tests/RemonteeRoutesTests.cs +++ b/MaBibli.Tests/RemonteeRoutesTests.cs @@ -29,6 +29,7 @@ public class RemonteeRoutesTests [InlineData("/ajout/manuel", "/")] // Revues [InlineData("/revues/ajout", "/revues")] + [InlineData("/revues/12/edition", "/revues/12")] [InlineData("/revues/12", "/revues")] [InlineData("/revues", "/")] // Auteurs @@ -74,7 +75,7 @@ public class RemonteeRoutesTests /// deviné, et jamais rien d'externe. /// [Theory] - [InlineData("/revues/12/edition", "/revues")] + [InlineData("/revues/12/inconnu", "/revues")] [InlineData("/series/7/inconnu", "/series")] [InlineData("/inconnu/total", "/")] [InlineData("/livres", "/")] diff --git a/MaBibli.Tests/ServiceRevuesTests.cs b/MaBibli.Tests/ServiceRevuesTests.cs index c8675be..7d8367a 100644 --- a/MaBibli.Tests/ServiceRevuesTests.cs +++ b/MaBibli.Tests/ServiceRevuesTests.cs @@ -307,6 +307,65 @@ public class ServiceRevuesTests : IDisposable Assert.Equal(2, (await _db.ArticlesUne.ToListAsync()).Count); } + /// + /// L'écran d'édition (lot W) affiche telle quelle la revue rendue par la + /// modification d'un numéro : elle doit donc revenir déjà rangée. + /// + /// + /// ⚠️ C'est ce qui empêche de croire qu'on a mal saisi une date : corriger la parution d'un + /// numéro le déplace dans la liste, et l'écran ne recharge rien de son côté. Les numéros + /// sans date ferment la liste, ici comme partout — une date inconnue n'est pas une date + /// ancienne. + /// + [Fact] + public async Task Corriger_la_parution_range_le_numero_a_sa_place() + { + var revue = await RevueAsync(); + + await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue + { + Numero = "43", + DateParution = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc), + }); + + var cree = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue + { + Numero = "hors-série 1", + })).Revue!; + + Assert.Equal(["43", "hors-série 1"], cree.Numeros.Select(n => n.Numero)); + + var horsSerie = cree.Numeros.First(n => n.Numero == "hors-série 1").Id; + + var apres = await _revues.ModifierNumeroAsync(horsSerie, new AjoutNumeroRevue + { + Numero = "hors-série 1", + DateParution = new DateTime(2025, 3, 1, 0, 0, 0, DateTimeKind.Utc), + }); + + Assert.Equal(["hors-série 1", "43"], apres.Revue!.Numeros.Select(n => n.Numero)); + } + + /// + /// Retirer un numéro depuis l'écran d'édition ne touche qu'à lui : les autres parutions et + /// la fiche de la revue restent en place. + /// + [Fact] + public async Task Retirer_un_numero_laisse_la_revue_et_ses_autres_numeros() + { + var revue = await RevueAsync(); + + await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43" }); + var cree = (await _revues.AjouterNumeroAsync( + revue.Id, new AjoutNumeroRevue { Numero = "44" })).Revue!; + + Assert.True(await _revues.RetirerNumeroAsync(cree.Numeros.First(n => n.Numero == "44").Id)); + + var restantes = await _revues.ListerAsync(); + + Assert.Equal(["43"], Assert.Single(restantes).Numeros.Select(n => n.Numero)); + } + public void Dispose() { _db.Dispose();