MergeNotice in the layout and a Merge status link on Membership status
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5ddfca9b55
commit
ed3645e048
6 changed files with 76 additions and 4 deletions
|
|
@ -32,6 +32,14 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
@if (Model.HasMerges)
|
||||
{
|
||||
<div class="panel-actions">
|
||||
<a class="button secondary" asp-page="/Membership/MergeStatus">Merge status</a>
|
||||
<span class="muted">See an account merge and, for seven days, undo it.</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.Invitation is not null)
|
||||
{
|
||||
<div class="callout success">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ public sealed class StatusModel(
|
|||
/// <summary>Whether this session belongs to a Heimdall account whose logins the Linked accounts page can manage.</summary>
|
||||
public bool CanManageLogins => User.FindFirstValue(BifrostClaimTypes.HeimdallAccountId) is { Length: > 0 };
|
||||
|
||||
/// <summary>Whether this account is in, or was in, a merge: the link to Merge status is shown only then.</summary>
|
||||
public bool HasMerges { get; private set; }
|
||||
|
||||
public CurrentBifrostActor Actor { get; private set; } = CurrentBifrostActor.Anonymous;
|
||||
|
||||
public MembershipInvitation? Invitation { get; private set; }
|
||||
|
|
@ -50,6 +53,9 @@ public sealed class StatusModel(
|
|||
public async Task OnGetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Actor = await actorAccessor.GetAsync(cancellationToken);
|
||||
HasMerges = Actor.Merge is not null
|
||||
|| (Actor.UserAccount is { } account
|
||||
&& await dbContext.AccountMerges.AnyAsync(x => x.SurvivorUserAccountId == account.Id || x.FoldedUserAccountId == account.Id, cancellationToken));
|
||||
|
||||
if (Actor.UserAccount is null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
@model string
|
||||
<aside class="merge-notice" role="status">
|
||||
<span>@Model</span>
|
||||
<a asp-page="/Membership/MergeStatus">Merge status</a>
|
||||
</aside>
|
||||
|
|
@ -57,6 +57,11 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
@if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
@await Component.InvokeAsync("MergeNotice")
|
||||
}
|
||||
|
||||
<main class="page-frame">
|
||||
@RenderBody()
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,55 @@
|
|||
using Bifrost.Web.Data;
|
||||
using Bifrost.Web.Domain;
|
||||
using Bifrost.Web.Features.Membership;
|
||||
using Bifrost.Web.Features.Shared;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Bifrost.Web.ViewComponents;
|
||||
|
||||
/// <summary>Skeleton: the layout's merge notice. Filled in by the cut's next commits.</summary>
|
||||
public sealed class MergeNotice : ViewComponent
|
||||
/// <summary>
|
||||
/// The layout's merge notice for a signed-in actor: the survivor of a Committed or UndoRequested merge inside its grace (days left), a
|
||||
/// frozen account (a merge is in progress) or a folded one (where to sign in). It reads the merge row once per page view and keeps no
|
||||
/// state of its own; the words are <see cref="MergeWording"/>'s, and no account name or id is ever in them.
|
||||
/// </summary>
|
||||
public sealed class MergeNotice(ICurrentBifrostActorAccessor actors, BifrostDbContext dbContext, TimeProvider timeProvider) : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke() => Content(string.Empty);
|
||||
public async Task<IViewComponentResult> InvokeAsync()
|
||||
{
|
||||
var actor = await actors.GetAsync(HttpContext.RequestAborted);
|
||||
var text = await TextAsync(actor, HttpContext.RequestAborted);
|
||||
return text is null ? Content(string.Empty) : View("Default", text);
|
||||
}
|
||||
|
||||
private async Task<string?> TextAsync(CurrentBifrostActor actor, CancellationToken cancellationToken)
|
||||
{
|
||||
var now = timeProvider.GetUtcNow();
|
||||
if (actor.Merge is { } standing)
|
||||
{
|
||||
if (standing is MergeStanding.Frozen)
|
||||
{
|
||||
return MergeWording.FrozenNotice();
|
||||
}
|
||||
|
||||
var mergedOn = await dbContext.AccountMerges.AsNoTracking()
|
||||
.Where(x => x.Id == standing.MergeId)
|
||||
.Select(x => x.CommittedUtc ?? x.CreatedUtc)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return MergeWording.FoldedNotice(mergedOn);
|
||||
}
|
||||
|
||||
if (actor.UserAccount is not { } account)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var graceEnds = await dbContext.AccountMerges.AsNoTracking()
|
||||
.Where(x => x.SurvivorUserAccountId == account.Id
|
||||
&& (x.State == AccountMergeState.Committed || x.State == AccountMergeState.UndoRequested)
|
||||
&& x.GraceEndsUtc > now)
|
||||
.OrderBy(x => x.GraceEndsUtc)
|
||||
.Select(x => x.GraceEndsUtc)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return graceEnds is { } end ? MergeWording.SurvivorNotice(MergeWording.DaysUntil(end, now)) : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace Bifrost.Web.Tests;
|
|||
/// </summary>
|
||||
public sealed class MergeStatusTests
|
||||
{
|
||||
private static string Enc(string text) => WebUtility.HtmlEncode(text);
|
||||
private static string Enc(string text) => System.Text.Encodings.Web.HtmlEncoder.Default.Encode(text);
|
||||
|
||||
private static int Notices(string html) => Regex.Matches(html, "class=\"merge-notice\"").Count;
|
||||
|
||||
|
|
@ -113,6 +113,8 @@ public sealed class MergeStatusTests
|
|||
var final = await Html(w, w.SurvivorSession);
|
||||
Assert.Equal(AccountMergeState.Final, (await rig.MergeAsync(w.MergeId)).State);
|
||||
Assert.Equal(0, Notices(final));
|
||||
Assert.Contains("/Membership/MergeStatus", await Html(w, w.SurvivorSession, "/Membership/Status"));
|
||||
Assert.DoesNotContain("/Membership/MergeStatus", await Html(w, w.StrangerSession, "/Membership/Status"));
|
||||
Assert.Contains(Enc(MergeWording.State(AccountMergeState.Final)), final);
|
||||
|
||||
foreach (var state in new[] { AccountMergeState.Undone, AccountMergeState.Failed })
|
||||
|
|
|
|||
Loading…
Reference in a new issue