From ba384ba4c0dbfd03cd292300806397aa553c69d0 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 18 Oct 2026 15:56:58 +0100 Subject: [PATCH] feat(PostIt): unify moderator administration and restore Android navigation Group users and usage in the moderator administration page; retain personal profile management and show personal usage. Allow moderators to read the corresponding API lists while preserving administrator-only mutations. Restore navigation pages after Android activity recreation. Update tests, architecture documentation and CHANGELOG for 1.0.8-rc27. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 12 + doc/architecture/postit.md | 34 +++ .../PostIt.Tests/ActivityNavigationTests.cs | 60 +++++ .../PostIt.Tests/AdministrationAccessTests.cs | 241 ++++++++++++++++++ .../PostIt.Tests/HomePageProviderFlowTests.cs | 2 +- src/PostIt/PostIt/App.axaml.cs | 104 +++++--- .../Helpers/ServiceCollectionHelpers.cs | 24 +- src/PostIt/PostIt/ViewLocator.cs | 1 - .../ViewModels/AdministrationPageViewModel.cs | 45 +++- .../ViewModels/Layout/HomePageViewModel.cs | 33 +-- .../Layout/ResourceUsagePageViewModel.cs | 90 ------- .../Layout/SessionStatusViewModel.cs | 37 ++- .../Layout/UserProfilePageViewModel.cs | 27 +- .../Views/Layout/AdministrationPage.axaml | 99 ++++--- src/PostIt/PostIt/Views/Layout/HomePage.axaml | 6 +- .../Views/Layout/ResourceUsagePage.axaml | 57 ----- .../Views/Layout/ResourceUsagePage.axaml.cs | 11 - .../PostIt/Views/Layout/UserProfilePage.axaml | 19 +- .../ResourceUsageApiController.cs | 2 +- .../ApplicationUserApiController.cs | 14 +- .../ResourceUsageApiControllerTests.cs | 43 ++++ 21 files changed, 682 insertions(+), 279 deletions(-) create mode 100644 src/PostIt/PostIt.Tests/ActivityNavigationTests.cs create mode 100644 src/PostIt/PostIt.Tests/AdministrationAccessTests.cs delete mode 100644 src/PostIt/PostIt/ViewModels/Layout/ResourceUsagePageViewModel.cs delete mode 100644 src/PostIt/PostIt/Views/Layout/ResourceUsagePage.axaml delete mode 100644 src/PostIt/PostIt/Views/Layout/ResourceUsagePage.axaml.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 951ce2d05..81eba2037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ ### Changed +* [PostIt] Une seule page Administration regroupe les utilisateurs et + leurs usages, avec le détail d’usage de l’utilisateur sélectionné. + L’accès depuis l’accueil et sa commande sont réservés au rôle `Moderator`, + vérifié via le profil du backend. « Mon profil » conserve la gestion + du compte et accueille l’usage personnel, sans consulter les usages globaux. +* [Yavsc.Api] Le rôle `Moderator` peut lire les listes d’utilisateurs et + d’usages ; les administrateurs conservent leurs droits existants et + les autres opérations sur les utilisateurs restent administrateur uniquement. + * [PostIt] La page des blogs passe en deux colonnes lorsque la largeur de la fenêtre atteint sa hauteur : billet à gauche, recherche, commandes et liste à droite, avec séparateur vertical redimensionnable. La disposition @@ -19,6 +28,9 @@ ### Fixed +* [PostIt.Android] Restauration de la pile de navigation lors de la + recréation de l'activité après suspension : nouvelles pages liées aux + mêmes modèles de vue, sans plus de procès. * [PostIt] Le changement de disposition de la page des blogs est différé sur le thread UI, évitant l'exception « Visual was invalidated during the render pass » au redimensionnement. diff --git a/doc/architecture/postit.md b/doc/architecture/postit.md index 476137a8b..faa67ad38 100644 --- a/doc/architecture/postit.md +++ b/doc/architecture/postit.md @@ -40,6 +40,40 @@ spécifiques (IntentFilter Android, `app.manifest` Desktop). Toute la logique (VM, services, navigation, settings, OIDC) est dans le code-base partagé. +## Reprise Android + +`IActivityApplicationLifetime.MainViewFactory` crée une nouvelle `MainView` +pour chaque activité Android. Avant de remplacer la racine, PostIt relève +les types et les modèles de vue des pages de la pile précédente, puis +recrée ces pages dans le même ordre, sans animation. Les contrôles de +l'ancienne activité ne sont pas réutilisés ; les modèles de vue conservent +les sélections et brouillons en mémoire. Une nouvelle racine sans historique +reçoit la page d'accueil, même si l'initialisation de session a déjà eu lieu. +Le rafraîchissement silencieux de session reste unique par processus. +Cette restauration couvre la recréation d'activité dans le même processus, +pas la destruction du processus par Android. + +## Administration et profil personnel + +L'accueil présente un seul accès **Administration**, visible et exécutable +uniquement pour le rôle exact `Moderator`. `SessionStatusViewModel.RefreshAsync` +lit les rôles dans `GET account/me` après le rafraîchissement de session ; +une erreur de lecture masque l'accès et affiche une erreur dans la bannière. +La déconnexion invalide aussi les vérifications de droits encore en vol. + +`AdministrationPage` regroupe deux onglets : utilisateurs et usages globaux. +Le détail d'un utilisateur associe son usage par `UserId`, sans dépendre +du nom affiché. Les erreurs ne déclenchent pas de repli sur l'usage personnel. +Le backend autorise la lecture des listes aux rôles `Moderator` et +`Administrator`, mais garde les autres opérations utilisateurs sous la +politique `AdministratorOnly`. L'autorisation serveur reste indispensable, +indépendamment de la visibilité du bouton dans PostIt. + +**Mon profil** réutilise la page de gestion du compte existante et les +endpoints `account/me` pour lire et enregistrer les informations personnelles. +Elle affiche aussi `resource-usage/me`, sans appeler la liste des usages +globaux. Une indisponibilité de l'usage n'empêche pas la gestion du profil. + ## Lecture des billets et fichiers utilisateur La page des blogs affiche par défaut une vue de lecture : titre, login diff --git a/src/PostIt/PostIt.Tests/ActivityNavigationTests.cs b/src/PostIt/PostIt.Tests/ActivityNavigationTests.cs new file mode 100644 index 000000000..607523deb --- /dev/null +++ b/src/PostIt/PostIt.Tests/ActivityNavigationTests.cs @@ -0,0 +1,60 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Avalonia.Threading; +using PostIt.ViewModels; +using PostIt.Views; + +namespace PostIt.Tests; + +public class ActivityNavigationTests +{ + [AvaloniaFact] + public async Task Activity_recreation_restores_fresh_pages_and_back_navigation() + { + var app = Assert.IsType(Application.Current); + var previousView = app.View; + var original = new MainView(); + var home = new HomePage(); + var draft = new Settings { SelectedLanguage = "fr" }; + var editor = new ContentPage { DataContext = draft }; + await original.NavRoot.PushAsync(home, null); + await original.NavRoot.PushAsync(editor, null); + app.AttachMainWindow(original); + var window = new Window(); + try + { + var restored = app.CreateActivityView(new Settings()); + window.Content = restored; + window.Show(); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal(2, restored.NavRoot.NavigationStack.Count); + Assert.IsType(restored.NavRoot.NavigationStack[0]); + Assert.NotSame(home, restored.NavRoot.NavigationStack[0]); + Assert.NotSame(editor, restored.NavRoot.NavigationStack[1]); + Assert.Same(draft, restored.NavRoot.NavigationStack[1].DataContext); + Assert.Equal("fr", draft.SelectedLanguage); + + window.Content = null; + window.Content = restored; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(2, restored.NavRoot.NavigationStack.Count); + + // Another activity recreation must retain the already-restored stack. + window.Content = null; + var recreatedAgain = app.CreateActivityView(new Settings()); + window.Content = recreatedAgain; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(2, recreatedAgain.NavRoot.NavigationStack.Count); + Assert.Same(draft, recreatedAgain.NavRoot.NavigationStack[1].DataContext); + await recreatedAgain.NavRoot.PopAsync(null); + Assert.IsType(Assert.Single(recreatedAgain.NavRoot.NavigationStack)); + } + finally + { + window.Close(); + app.AttachMainWindow(previousView ?? new MainView()); + } + } +} diff --git a/src/PostIt/PostIt.Tests/AdministrationAccessTests.cs b/src/PostIt/PostIt.Tests/AdministrationAccessTests.cs new file mode 100644 index 000000000..ef1466e5f --- /dev/null +++ b/src/PostIt/PostIt.Tests/AdministrationAccessTests.cs @@ -0,0 +1,241 @@ +using System.Net.Http; +using System.Text.Json; +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Avalonia.Threading; +using Avalonia.VisualTree; +using PostIt.Services; +using PostIt.ViewModels; +using PostIt.Views; +using Yavsc.Abstract.Identity; +using Yavsc.Abstract.Resources; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +public class AdministrationAccessTests +{ + [AvaloniaTheory] + [InlineData("Moderator", true)] + [InlineData("Administrator", false)] + [InlineData("Performer", false)] + [InlineData("moderator", false)] + [InlineData("", false)] + public async Task Home_administration_visibility_and_command_follow_the_moderator_role(string role, bool allowed) + { + await using var api = new ProfileTransport(role); + var session = new SessionStatusViewModel { Api = api }; + var vm = new HomePageViewModel(api, api.Settings, session, null!); + var page = new HomePage { DataContext = vm }; + var window = new Window { Content = page }; + try + { + window.Show(); + var button = Assert.Single(page.GetVisualDescendants().OfType