yavsc/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs

132 lines
4.6 KiB
C#
Raw Normal View History

feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.Services;
using Yavsc.Api.Client;
namespace PostIt.ViewModels;
/// <summary>
/// View model for the "add a Yavsc user to a circle" modal.
///
/// <para>Resolves users through <see cref="IUserDirectory"/>
/// (which delegates to <c>/api/user-search</c>); the caller
/// (CirclesPage) decides whether to add the picked user to
/// the circle by calling
/// <see cref="AddCircleMemberDialogViewModel.AddCommand"/>
/// (which is bound to the dialog's "Ajouter" button).</para>
///
/// <para>The dialog itself doesn't know the target
/// <c>CircleId</c>: that's set by the caller via the
/// constructor and the dialog only triggers
/// <see cref="IUserDirectory.SearchAsync"/> against the
/// <see cref="SearchQuery"/> string. The "Add" command
/// returns the picked <see cref="UserSummary"/> via the
/// <see cref="Confirmed"/> event, and the hosting
/// <c>CirclesPage</c> then calls
/// <see cref="CircleApiClient.AddMemberAsync"/>.</para>
/// </summary>
public partial class AddCircleMemberDialogViewModel : ViewModelBase, IActionStatusViewModel
feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
{
private readonly IUserDirectory _directory;
[ObservableProperty]
public partial string SearchQuery { get; set; } = string.Empty;
[ObservableProperty]
public partial ObservableCollection<UserSummary> Results { get; set; } = new();
[ObservableProperty]
public partial UserSummary? Selected { get; set; }
[ObservableProperty]
public partial bool IsBusy { get; set; }
[ObservableProperty]
public partial string StatusMessage { get; set; } = "Pret.";
feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
[ObservableProperty]
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret.");
feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
/// <summary>
/// Raised when the user confirms a selection. The hosting
/// <c>CirclesPage</c> subscribes to this event and calls
/// <c>CircleApiClient.AddMemberAsync</c> with the target
/// circle id + the picked user's id. The dialog itself
/// does not know the circle id by design: separation of
/// concerns — the modal is a user picker, not a
/// "circle joiner" form.
/// </summary>
public event EventHandler<UserSummary>? Confirmed;
public AddCircleMemberDialogViewModel(IUserDirectory directory)
{
_directory = directory ?? throw new ArgumentNullException(nameof(directory));
}
public override bool CanNavigateNext { get => throw new NotImplementedException(); protected set => throw new NotImplementedException(); }
public override bool CanNavigatePrevious { get => throw new NotImplementedException(); protected set => throw new NotImplementedException(); }
/// <summary>
/// Search the directory for users matching the current
/// <see cref="SearchQuery"/>. Triggered explicitly via the
/// "Rechercher" button — no debouncing, so the caller
/// stays in control of how often the network is hit.
/// </summary>
[RelayCommand]
public async Task SearchAsync()
{
if (string.IsNullOrWhiteSpace(SearchQuery))
{
Results.Clear();
this.SetWarningStatus("Tapez un nom ou un email");
feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
return;
}
IsBusy = true;
try
{
var hits = await _directory.SearchAsync(SearchQuery, CancellationToken.None).ConfigureAwait(true);
Results = new ObservableCollection<UserSummary>(hits ?? Array.Empty<UserSummary>());
this.SetInfoStatus($"{Results.Count} résultat(s)");
feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
}
catch (Exception ex)
{
this.SetErrorStatus($"Erreur: {ex.Message}");
feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
}
finally
{
IsBusy = false;
}
}
/// <summary>
/// Raise <see cref="Confirmed"/> for the currently selected
/// user. No-op when no selection has been made — keeps the
/// UI from firing an event with a null payload.
/// </summary>
[RelayCommand]
2026-08-20 20:50:52 +01:00
public async Task AddAsync()
feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
{
if (Selected is null)
{
this.SetWarningStatus("Sélectionnez un utilisateur");
feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
return;
}
Confirmed?.Invoke(this, Selected);
var app = App.Current as App
?? throw new InvalidOperationException("Application PostIt indisponible.");
2026-08-20 20:50:52 +01:00
await app.GoBackAsync();
}
[RelayCommand]
public async Task CloseAsync()
{
var app = App.Current as App
?? throw new InvalidOperationException("Application PostIt indisponible.");
2026-08-20 20:50:52 +01:00
await app.GoBackAsync();
feat(acl): server endpoints + client + UI for circle membership Round-trip out a long-standing hole in the ACL feature: until this commit a circle on Yavsc was an empty named bucket. You could create 'Famille' and grant it on a post, but the circle carried no members, so 'Famille' authorised no one. The MVC admin controller (Yavsc.Org.Controllers.CircleMembersController) existed but had no REST counterpart, so PostIt — which only talks to the Yavsc.Blogs API — had no way to manage membership at all. This commit closes that gap end-to-end: Server (Yavsc.Blogs) - GET /api/circle/{id}/members list members - POST /api/circle/{id}/members add a user (body { userId }) - DELETE /api/circle/{id}/members/{userId} remove a user All three are scoped to caller == circle.OwnerId; non-owned circles return 404 (not 403) to avoid leaking existence, in line with the rest of the controller. - Two new DTOs (CircleMemberDto, AddCircleMemberDto) for the wire shapes. CircleMemberDto mirrors UserSearchResultDto minus Email — membership UI doesn't need contact details. Tests (Yavsc.Blogs.Tests) - CircleMembersApiTests: 5 [Fact] covering empty list, add+get, duplicate add → 409, remove, and cross-owner 404. Test users (alice, bob) are seeded directly through the in-memory DbContext — the Blogs fixture doesn't stand up UserManager<ApplicationUser>. Client (Yavsc.Api.Client) - CircleMemberDto + 3 methods on CircleApiClient: GetMembersAsync, AddMemberAsync, RemoveMemberAsync. All match the server's contract: 404 flattens to null, 409 surfaces as an exception (callers can dedupe beforehand if they want idempotent behaviour). UI (PostIt) - CirclesPageViewModel gains a Members ObservableCollection that auto-loads on SelectedCircle change (via the partial setter generated by [ObservableProperty]). Commands: LoadMembersAsync, OpenAddMember (raises an event the view subscribes to), OnAddMemberConfirmedAsync (called by the view when the dialog confirms a selection), RemoveMemberAsync. 409 (already a member) is detected from the exception message and surfaced as a friendly status rather than an error — a likely race when the same user gets added twice through two UI paths. - CirclesPage layout is now two-pane (circles + editor on the left, members of the selected circle on the right). The member pane has an 'Ajouter un membre' button that opens AddCircleMemberDialog. Code-behind wires the dialog's Confirmed event back into the VM via an async lambda wrapper (EventHandler<T> wants void, the VM method is async Task). - AddCircleMemberDialog is a ContentPage (light modal, same pattern as PostAclDialog). Its ViewModel consumes IUserDirectory — the abstraction introduced by af5e71aa to fix the 'user search should not be IContactService' confusion. The dialog raises Confirmed with the picked UserSummary; the host (CirclesPage) is responsible for calling CircleApiClient.AddMemberAsync. Out of scope (tracked in MEMORY.md, 2026-08-18): - i18n: all visible text still hard-coded French. - XAML accessibility audit of pre-existing pages. - Avalonia.Headless UI tests of the new navigation flow. Tests: 51/51 PostIt.Tests green, 20/20 Yavsc.Blogs.Tests green (was 15; +5 for CircleMembersApiTests), 44/44 Yavsc.Org.Tests green (no regression).
2026-08-18 14:10:12 +01:00
}
}