From fe36091bf0f78d086e63c4ac8d4a2852e865c6b6 Mon Sep 17 00:00:00 2001 From: mathieu Date: Fri, 21 Aug 2026 23:37:31 +0200 Subject: [PATCH] =?UTF-8?q?Rend=20enfin=20tapable=20le=20champ=20o=C3=B9?= =?UTF-8?q?=20l'on=20=C3=A9crit=20ce=20qu'un=20num=C3=A9ro=20annonce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le champ « À la une » se reliait à chaque frappe sur une propriété qui découpait puis recomposait la ligne : le point-virgule tapé ouvrait une entrée vide aussitôt élaguée, et l'espace qui le suit était rogné. Le seul caractère qu'on ne pouvait pas y taper était son propre séparateur. Un titre s'ajoute maintenant seul, par Entrée, et se modifie ou se retire depuis la liste. Plus rien ne découpe, donc plus rien n'est intapable. ArticlesTexte disparaît, et avec lui le paramètre virguleAussi de ListeSaisie, qui n'existait que pour ce champ. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 67 +++++++ .../Composants/ChampsNumeroRevue.razor | 179 +++++++++++++++++- MaBibli.Client/wwwroot/css/app.css | 31 +++ MaBibli.Shared/Dtos/EnregistrementLivre.cs | 2 +- MaBibli.Shared/Dtos/RevueDto.cs | 23 +-- MaBibli.Shared/Textes/ListeSaisie.cs | 21 +- MaBibli.Tests/ListeSaisieTests.cs | 30 ++- 7 files changed, 298 insertions(+), 55 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 231f398..717fd31 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3146,6 +3146,73 @@ n'affirme rien — comparer avec `RangDesEnvies`, qui avait dû reconduire un or **618 tests au vert** (506 au début de la série), aucun avertissement de compilation. +## Les articles à la une se saisissent un par un — champ à séparateur retiré (2026-08-21) + +Défaut remonté en usage sur le champ « À la une (titres séparés par des points-virgules) » : +« impossible de taper des espaces, des `;`, se comporte très bizarrement ». + +### ⚠️ Le séparateur d'un champ relié à chaque frappe est INTAPABLE — et ce n'est pas propre aux unes + +`AjoutNumeroRevue.ArticlesTexte` était une vue « une seule ligne » : son *getter* recomposait +(`ListeSaisie.Joindre`) ce que son *setter* venait de découper (`Decouper`). Reliée en +`@bind:event="oninput"`, elle **réécrivait la saisie à chaque caractère** : + +| Ce qu'on tape | Ce que la propriété rend | Ce qu'on voit | +|---|---|---| +| `Ukraine ;` | `;` ouvre une entrée **vide**, élaguée par `RemoveEmptyEntries` | le `;` **disparaît** | +| `Ukraine ; ` | `TrimEntries` rogne l'espace de fin | l'espace **disparaît** | +| `Ukraine ; L` | `Joindre` repose son propre `« ; »` | le curseur **saute** | + +Autrement dit : **le seul caractère qu'on ne pouvait pas taper dans ce champ était son propre +séparateur**. Le lot O2 avait longuement tranché *quel* séparateur employer (le point-virgule, +parce qu'un titre contient souvent une virgule) sans voir qu'aucun ne pouvait fonctionner par +cette mécanique. + +⚠️ **Deux champs voisins ont exactement la même mécanique et donc le même défaut** : +`EnregistrementLivre.ThemesTexte` et `EnregistrementLivre.Auteur`, tous deux reliés sur +`oninput` à une propriété qui découpe puis recompose. Non corrigés — le retour d'usage ne +portait que sur les unes — mais ce sont les mêmes symptômes, et `Auteur` porte en plus la +reprise des rôles. + +### Ce que l'écran fait maintenant + +Un titre, `Entrée`, et il rejoint une liste où chaque ligne porte **Modifier** et **Supprimer** +(demandé tel quel par l'utilisateur). `Modifier` **reprend le titre dans le champ**, là où on +l'a écrit, et le bouton devient « Enregistrer » avec un « Annuler » à côté. + +- **La liste est la donnée** : plus de propriété texte intermédiaire, plus de découpage, donc + plus rien à ne pas pouvoir taper — ni `;`, ni virgule, ni deux-points. +- **`Entrée` ne remonte pas à `OnEntree`** (qui enregistre le numéro entier) : sinon on ne + pourrait jamais saisir deux titres. +- **Le doublon reste refusé au sens de la normalisation**, avec un message plutôt qu'en silence. +- ⚠️ **Le composant se remet à zéro quand le parent change de `Saisie`** (numéro enregistré, + autre numéro ouvert en modification), en comparant la **référence** : sans cela, le titre resté + dans le champ appartiendrait au numéro d'avant. +- ⚠️ `min-width: 0` sur le titre de la ligne, encore une fois : sans lui, un titre long pousse + « Supprimer » hors d'un écran de 320 px. + +`ArticlesTexte` a disparu, et avec lui le paramètre `virguleAussi` de `ListeSaisie` — il +n'existait que pour ce champ. `ListeSaisie` ne sert plus qu'aux **thèmes**, où la virgule reste +acceptée en repli. + +### Vérifié en exécution (navigateur, 375 puis 320 px) + +Frappe caractère par caractère de `Ukraine, deux ans après ; le retour du train` : **la valeur +reste exactement ce qui a été tapé**, espaces et `;` compris. `Entrée` la range telle quelle, +le champ se vide. Un doublon de casse est refusé avec son message ; Modifier reprend le titre, +Enregistrer le remplace, Supprimer le retire. Numéro 44 créé puis relu par l'API avec ses deux +titres **intacts**, un article ajouté à un numéro existant par l'écran d'édition, et la liste +qui se recharge bien d'un numéro à l'autre. Aucun débordement horizontal à 320 px +(`scrollWidth` = 320). + +⚠️ **Défaut voisin constaté au passage, NON corrigé** : le bouton « Ajouter ce numéro » vit dans +`Revue.razor` alors que le champ « Numéro » vit dans `ChampsNumeroRevue`. Une frappe ne +re-rendant que le composant qui l'a traitée, le bouton **reste désactivé** tant que le parent ne +se redessine pas — seul `Entrée` (qui appelle `OnEntree`, donc le parent) enregistre le numéro. +À traiter. + +617 tests au vert (618 avant : le test du découpage des unes n'a plus d'objet). + ## La 7ᵉ série vérifiée en navigateur — trois défauts, tous invisibles aux tests (2026-08-21) La série Q à Y s'était close sur un aveu : « **aucun** écran n'a été regardé s'afficher ». Le diff --git a/MaBibli.Client/Composants/ChampsNumeroRevue.razor b/MaBibli.Client/Composants/ChampsNumeroRevue.razor index f8a4071..e09f49e 100644 --- a/MaBibli.Client/Composants/ChampsNumeroRevue.razor +++ b/MaBibli.Client/Composants/ChampsNumeroRevue.razor @@ -26,16 +26,77 @@ @* - ⚠️ Séparateur : le POINT-VIRGULE, et pas la virgule des thèmes de livres. Un titre d'article - en contient souvent (« Ukraine, deux ans après »), et la virgule le couperait en deux. + ⚠️ Un titre à la fois, et surtout PAS une ligne de titres séparés par des points-virgules. + Le champ unique se reliait à chaque frappe sur une propriété qui découpait puis recomposait + la liste : la saisie était donc réécrite sous les doigts — un point-virgule tapé disparaissait + (il ouvrait une valeur vide, aussitôt élaguée), et l'espace qui le suit était rogné par le + même élagage. Autrement dit, le séparateur du champ était le seul caractère qu'on ne pouvait + pas y taper. + + Ici la liste est la donnée : on écrit un titre, Entrée l'y range, et chaque titre se modifie + ou se retire ensuite. Une virgule (« Ukraine, deux ans après ») n'a plus rien à couper, + puisque plus rien ne découpe. *@ - +
+ + +
+ + + + + @if (_indexModifie is not null) + { + + } +
+ + @if (_messageArticle is not null) + { +

@_messageArticle

+ } + + @if (Saisie.Articles.Count > 0) + { +
    + @for (var rang = 0; rang < Saisie.Articles.Count; rang++) + { + @* La copie locale est indispensable : sans elle, les fermetures des deux + boutons captureraient la variable de boucle, donc son état final. *@ + var index = rang; + var titre = Saisie.Articles[rang]; + +
  • + @titre + +
    + + + +
    +
  • + } +
+ } +
@* ⚠️ Une URL, pas une photo. Aucune source ne donne la couverture d'un NUMÉRO : l'ISSN désigne @@ -53,6 +114,106 @@ @code { [Parameter, EditorRequired] public AjoutNumeroRevue Saisie { get; set; } = default!; + /// Identifiant du champ « à la une », pour que son libellé le désigne vraiment. + private readonly string _idChamp = $"une-{Guid.NewGuid():N}"; + + /// Titre en cours de frappe. Il n'est relié à rien d'autre : voir plus haut. + private string? _titreSaisi; + + /// Rang du titre qu'on modifie — null = on en ajoute un nouveau. + private int? _indexModifie; + + private string? _messageArticle; + + /// Saisie servie au rendu précédent, pour repérer que le parent en a changé. + private AjoutNumeroRevue? _saisiePrecedente; + + /// + /// Le parent remplace la saisie entière (numéro enregistré, autre numéro ouvert en + /// modification) : ce qui restait dans le champ appartenait au numéro d'avant. + /// + protected override void OnParametersSet() + { + if (!ReferenceEquals(_saisiePrecedente, Saisie)) + { + _saisiePrecedente = Saisie; + RemettreAZero(); + } + } + + private void RemettreAZero() + { + _titreSaisi = null; + _indexModifie = null; + _messageArticle = null; + } + + /// + /// Entrée range le titre, comme le bouton. + /// + /// + /// ⚠️ Cette touche ne remonte pas à OnEntree : celui-ci enregistre le numéro + /// entier, et l'on serait alors bien en peine de saisir deux titres à la une. + /// + private void SurToucheArticle(KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + ValiderArticle(); + } + } + + private void ValiderArticle() + { + var titre = _titreSaisi?.Trim(); + + if (string.IsNullOrEmpty(titre)) + { + return; + } + + // Même règle de doublon que partout ailleurs : au sens de la normalisation, et le + // libellé déjà rangé est celui qui reste. + var cle = NormalisationTexte.Normaliser(titre); + var deja = Saisie.Articles.FindIndex(a => NormalisationTexte.Normaliser(a) == cle); + + if (deja >= 0 && deja != _indexModifie) + { + _messageArticle = "Ce titre est déjà à la une de ce numéro."; + return; + } + + if (_indexModifie is { } index) + { + Saisie.Articles[index] = titre; + } + else + { + Saisie.Articles.Add(titre); + } + + RemettreAZero(); + } + + /// Reprend un titre dans le champ : on le corrige là où on l'a écrit. + private void ModifierArticle(int index) + { + _messageArticle = null; + _indexModifie = index; + _titreSaisi = Saisie.Articles[index]; + } + + private void AnnulerModification() => RemettreAZero(); + + private void SupprimerArticle(int index) + { + Saisie.Articles.RemoveAt(index); + + // Le titre qu'on modifiait vient peut-être de disparaître, et les rangs suivants ont + // glissé : reprendre la modification écrirait sur le mauvais titre. + RemettreAZero(); + } + /// /// Date affichée par le champ, en local. /// diff --git a/MaBibli.Client/wwwroot/css/app.css b/MaBibli.Client/wwwroot/css/app.css index 39e2cb3..0062fb3 100644 --- a/MaBibli.Client/wwwroot/css/app.css +++ b/MaBibli.Client/wwwroot/css/app.css @@ -1614,6 +1614,37 @@ body { font-style: italic; } +/* Saisie des unes : un titre par ligne, son texte à gauche et ses deux actions à droite. + ⚠️ min-width: 0 sur le titre est ce qui fait tenir la ligne à 320 px — un titre d'article + est long, et sans lui il pousserait « Supprimer » hors de l'écran (même leçon que la ligne + de création du lot M1, et que l'arbre des séries). */ +.liste-unes { + list-style: none; + margin: 0.25rem 0 0; + padding: 0; +} + +.ligne-une { + display: flex; + gap: 0.5rem; + align-items: center; + justify-content: space-between; + padding: 0.35rem 0; + border-bottom: 1px solid var(--mb-bord); +} + +.ligne-une-titre { + flex: 1 1 auto; + min-width: 0; + overflow-wrap: anywhere; + font-style: italic; +} + +.ligne-une .actions-tome { + flex: 0 0 auto; + margin-top: 0; +} + /* --- Créer depuis une liste : champ + bouton sur une ligne (lot M1) ---- ⚠️ min-width: 0 sur le champ est ce qui fait tenir la ligne à 320 px : sans lui, un élément de formulaire refuse de rétrécir sous sa largeur intrinsèque diff --git a/MaBibli.Shared/Dtos/EnregistrementLivre.cs b/MaBibli.Shared/Dtos/EnregistrementLivre.cs index 8f88583..667a042 100644 --- a/MaBibli.Shared/Dtos/EnregistrementLivre.cs +++ b/MaBibli.Shared/Dtos/EnregistrementLivre.cs @@ -125,7 +125,7 @@ public record EnregistrementLivre /// n'en contenant jamais. /// public static List DecouperThemes(string? saisie) => - ListeSaisie.Decouper(saisie, virguleAussi: true); + ListeSaisie.Decouper(saisie); } /// Un auteur tel qu'il est saisi : son nom, et son rôle sur ce livre. diff --git a/MaBibli.Shared/Dtos/RevueDto.cs b/MaBibli.Shared/Dtos/RevueDto.cs index 8539b5e..4ee8a77 100644 --- a/MaBibli.Shared/Dtos/RevueDto.cs +++ b/MaBibli.Shared/Dtos/RevueDto.cs @@ -1,6 +1,3 @@ -using System.Text.Json.Serialization; -using MaBibli.Shared.Textes; - namespace MaBibli.Shared.Dtos; /// @@ -71,21 +68,15 @@ public record AjoutNumeroRevue /// public string? CoverUrl { get; set; } - /// Articles à la une, dans l'ordre de la couverture. - public List Articles { get; set; } = []; - /// - /// Vue « une seule ligne » des , pour un champ de saisie simple. + /// Articles à la une, dans l'ordre de la couverture. /// /// - /// ⚠️ Séparateur : le point-virgule seul, contrairement aux thèmes d'un livre. Un - /// titre d'article contient souvent une virgule (« Ukraine, deux ans après »), qui le - /// couperait en deux. + /// ⚠️ La liste est la donnée, et il n'y a plus de vue « une seule ligne ». Elle a + /// existé — un champ texte à points-virgules, relié à chaque frappe sur une propriété qui + /// découpait puis recomposait — et c'était un champ dans lequel on ne pouvait taper ni le + /// séparateur (aussitôt élagué comme entrée vide) ni l'espace qui le suit. Un titre s'ajoute + /// désormais un par un. /// - [JsonIgnore] - public string? ArticlesTexte - { - get => Articles.Count == 0 ? null : ListeSaisie.Joindre(Articles); - set => Articles = ListeSaisie.Decouper(value, virguleAussi: false); - } + public List Articles { get; set; } = []; } diff --git a/MaBibli.Shared/Textes/ListeSaisie.cs b/MaBibli.Shared/Textes/ListeSaisie.cs index 6adfd43..d861018 100644 --- a/MaBibli.Shared/Textes/ListeSaisie.cs +++ b/MaBibli.Shared/Textes/ListeSaisie.cs @@ -1,18 +1,19 @@ namespace MaBibli.Shared.Textes; /// -/// Découpage d'un champ texte qui porte plusieurs valeurs : thèmes d'un livre, articles -/// à la une d'un numéro de revue. +/// Découpage du champ texte qui porte plusieurs thèmes d'un livre. /// /// /// Le séparateur est le point-virgule, comme celui des auteurs -/// (EnregistrementLivre.SeparateurAuteurs) : trois champs voisins du même formulaire ne -/// doivent pas se saisir de trois façons. +/// (EnregistrementLivre.SeparateurAuteurs) : deux champs voisins du même formulaire ne +/// doivent pas se saisir de deux façons. La virgule reste acceptée en repli — elle était le +/// séparateur jusqu'au 2026-08-21, et un thème (« dark fantasy ») n'en contient jamais. /// -/// ⚠️ La virgule n'est acceptée que là où elle ne peut rien couper de travers. Un thème -/// (« dark fantasy ») n'en contient jamais, et l'accepter évite de désapprendre une habitude ; -/// un titre d'article en contient souvent (« Ukraine, deux ans après »), et la virgule y -/// couperait le titre en deux. D'où le paramètre plutôt qu'une règle unique. +/// ⚠️ Ce découpage ne convient pas à tout champ à valeurs multiples. Les articles à la +/// une d'un numéro y sont passés jusqu'au 2026-08-21 et en sont sortis : un titre contient +/// souvent une virgule, et surtout un champ relié à chaque frappe sur une propriété qui découpe +/// puis recompose réécrit la saisie sous les doigts — le séparateur lui-même devient +/// intapable. Ils se saisissent maintenant un par un. /// /// public static class ListeSaisie @@ -28,14 +29,14 @@ public static class ListeSaisie /// normalisation — « Fantasy » et « fantasy » sont la même étiquette, et le premier /// libellé tapé est celui qui reste. /// - public static List Decouper(string? saisie, bool virguleAussi) + public static List Decouper(string? saisie) { if (string.IsNullOrWhiteSpace(saisie)) { return []; } - char[] separateurs = virguleAussi ? [Separateur, ','] : [Separateur]; + char[] separateurs = [Separateur, ',']; var vus = new HashSet(StringComparer.Ordinal); return saisie diff --git a/MaBibli.Tests/ListeSaisieTests.cs b/MaBibli.Tests/ListeSaisieTests.cs index 04b35da..5ddc4c4 100644 --- a/MaBibli.Tests/ListeSaisieTests.cs +++ b/MaBibli.Tests/ListeSaisieTests.cs @@ -4,24 +4,16 @@ using MaBibli.Shared.Textes; namespace MaBibli.Tests; /// -/// Découpage des champs qui portent plusieurs valeurs : thèmes d'un livre, articles à la une -/// d'un numéro (lot O2). +/// Découpage du champ des thèmes d'un livre. /// +/// +/// ⚠️ Les articles à la une d'un numéro passaient ici jusqu'au 2026-08-21. Ils en sont sortis : +/// un champ relié à chaque frappe sur une propriété qui découpe puis recompose réécrit la saisie +/// sous les doigts, et l'on ne pouvait y taper ni le point-virgule ni l'espace qui le suit. Ils +/// se saisissent un par un, et n'ont donc plus de découpage à éprouver. +/// public class ListeSaisieTests { - /// - /// ⚠️ Le cas qui a tranché le séparateur : un titre d'article contient souvent une virgule, - /// et elle le couperait en deux. - /// - [Fact] - public void Un_titre_darticle_garde_ses_virgules() - { - var articles = ListeSaisie.Decouper( - "Ukraine, deux ans après ; Le retour du train de nuit", virguleAussi: false); - - Assert.Equal(["Ukraine, deux ans après", "Le retour du train de nuit"], articles); - } - /// /// Les thèmes acceptent la virgule en repli : elle était le séparateur jusqu'au 2026-08-21, /// et un thème n'en contient jamais. @@ -44,16 +36,16 @@ public class ListeSaisieTests [Fact] public void Les_entrees_vides_disparaissent() { - Assert.Empty(ListeSaisie.Decouper(" ; ; ", virguleAussi: false)); - Assert.Empty(ListeSaisie.Decouper(null, virguleAussi: true)); + Assert.Empty(ListeSaisie.Decouper(" ; ; ")); + Assert.Empty(ListeSaisie.Decouper(null)); } /// La ligne recomposée se redécoupe à l'identique : c'est ce que fait le formulaire. [Fact] public void Joindre_puis_decouper_ne_perd_rien() { - List articles = ["Ukraine, deux ans après", "Le retour du train de nuit"]; + List themes = ["dark fantasy", "space opera"]; - Assert.Equal(articles, ListeSaisie.Decouper(ListeSaisie.Joindre(articles), virguleAussi: false)); + Assert.Equal(themes, ListeSaisie.Decouper(ListeSaisie.Joindre(themes))); } }