Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions src/FluentCMS.Admin/Components/Pages/ChangeSetView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@using FluentCMS.Core.ChangeSets
@using FluentCMS.Core.Desk
@inject IChangeSetDirectory Directory
@inject IChangeSetLists Lists
@inject IDeskQueues Queues
@inject FluentCMS.Core.Checks.IChangeSetChecks Checks
@inject IAuditReader Audit
Expand Down Expand Up @@ -461,7 +462,7 @@

private async Task SubmitAsync()
{
await ActAsync(Submit, async () =>
await ActAsync(Submit, [ChangeSetState.Open], async () =>
{
var submitted = await Checks.SubmitAsync(Id);
return submitted.IsSuccess
Expand All @@ -472,20 +473,20 @@

private async Task ApproveAsync()
{
if (await ActAsync(Approve, () => Directory.TransitionAsync(Id, ChangeSetState.Approved), L["Approved. Publish the set to make its items live."]) is not null)
if (await ActAsync(Approve, [ChangeSetState.AwaitingReview], () => Directory.TransitionAsync(Id, ChangeSetState.Approved), L["Approved. Publish the set to make its items live."]) is not null)
{
MoveOn("approved");
}
}

private Task PublishAsync()
{
return ActAsync(Publish, () => Directory.PublishAsync(Id), L["Published. Its items are live."]);
return ActAsync(Publish, [ChangeSetState.Approved], () => Directory.PublishAsync(Id), L["Published. Its items are live."]);
}

private async Task RejectAsync()
{
if (await ActAsync(Reject, () => Directory.RejectAsync(Id, Reject?.Reason ?? string.Empty), L["Rejected. The reason is stored with the set."]) is not null)
if (await ActAsync(Reject, RejectableStates, () => Directory.RejectAsync(Id, Reject?.Reason ?? string.Empty), L["Rejected. The reason is stored with the set."]) is not null)
{
MoveOn("rejected");
}
Expand All @@ -503,24 +504,44 @@
Navigation.NavigateTo(_next is { } next ? $"sets/{next}?queue={slug}&done={done}" : $"?queue={slug}&done={done}");
}

// A revert already in progress is where a second click lands, so a resubmitted page or a later click opens no second set. Two
// posts at the same instant can still both open one, until the directory refuses it (#2646).
private async Task RevertAsync()
{
var rationale = _set is { } set ? L["Revert change set {0}: {1}", set.Id, set.Rationale].Value : string.Empty;
if (await ActAsync(Revert, () => Directory.RevertAsync(Id, rationale), null) is { } opened)
var opened = await ActAsync(Revert, [ChangeSetState.Published], async () =>
{
var inProgress = await Lists.ListAsync(new ChangeSetListQuery { States = RevertInProgress, Reverts = Id, Limit = 1 });
return inProgress is { IsSuccess: true, Value.Items: [var existing, ..] }
? await Directory.GetAsync(existing.Id)
: await Directory.RevertAsync(Id, rationale);
}, null);
if (opened is not null)
{
Navigation.NavigateTo($"sets/{opened.Id}");
}
}

private static readonly ChangeSetState[] RejectableStates = [ChangeSetState.AwaitingReview, ChangeSetState.ChecksFailed, ChangeSetState.Approved];

// A revert set that may still publish; a rejected one leaves room for another.
private static readonly ChangeSetState[] RevertInProgress =
[ChangeSetState.Open, ChangeSetState.ChecksRunning, ChangeSetState.ChecksFailed, ChangeSetState.AwaitingReview, ChangeSetState.Approved];

// A decision is saved by the call itself; the page then shows the set as it is now, whatever the outcome.
private async Task<ChangeSet?> ActAsync(ReviewForm? form, Func<Task<FluentCMS.Core.Results.Result<ChangeSet>>> action, string? done)
// An action is taken only from a state the panel offers it in, whatever a post claims, so no set skips its checks or review.
private async Task<ChangeSet?> ActAsync(
ReviewForm? form,
IReadOnlyCollection<ChangeSetState> offeredIn,
Func<Task<FluentCMS.Core.Results.Result<ChangeSet>>> action,
string? done)
{
if (_set is null)
{
return null;
}

if (form?.Seen is null || form.Seen != _seen)
if (form?.Seen is null || form.Seen != _seen || !offeredIn.Contains(_set.State))
{
_refusal = L["The set has moved on since this page was shown. Review it as it is now."];
return null;
Expand Down
8 changes: 6 additions & 2 deletions src/FluentCMS.Admin/Components/Review/ChangeSetItems.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@using FluentCMS.Core.ChangeSets
@inject IChangeSetDirectory Directory
@using Microsoft.Extensions.DependencyInjection
@inject IServiceScopeFactory Scopes
@inject IStringLocalizer<ChangeSetItems> L

@* The set view's interactive island: DiffView adds items in batches over the circuit (PERF-04) while the rest of the page stays static. *@
Expand Down Expand Up @@ -32,7 +33,10 @@
return;
}

var diff = await Directory.DiffAsync(ChangeSetId);
// Prerendered while the set view may still be loading: an async read on PostgreSQL can finish at once, so the view renders the
// island before its own reads are done. Its own scope keeps the two off one DbContext.
await using var scope = Scopes.CreateAsyncScope();
var diff = await scope.ServiceProvider.GetRequiredService<IChangeSetDirectory>().DiffAsync(ChangeSetId);
if (diff.IsSuccess)
{
(_diff, _error) = (diff.Value, null);
Expand Down
3 changes: 3 additions & 0 deletions src/FluentCMS.Core/ChangeSets/ChangeSetListQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public sealed record ChangeSetListQuery
// Only the sets this person or agent opened; null for every set.
public Initiator? OpenedBy { get; init; }

// Only the sets that revert this set; null for every set.
public Guid? Reverts { get; init; }

// Newest first puts the set that entered its state last first; otherwise the set that has waited longest comes first.
public bool NewestFirst { get; init; }

Expand Down
7 changes: 6 additions & 1 deletion src/FluentCMS.Infrastructure/ChangeSets/ChangeSetLists.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private static (string Sql, List<object> Parameters) Select(ChangeSetListQuery q
(SELECT MAX(e."Timestamp") FROM "AuditEvents" e
WHERE e."SubjectType" = {movedSubject} AND e."SubjectId" = s."Id" AND e."Operation" IN ({moves})) AS "StateSince"
FROM "ChangeSets" s
WHERE s."SiteId" = {site} AND s."State" IN ({states}){OpenedBy(query, parameters)}
WHERE s."SiteId" = {site} AND s."State" IN ({states}){OpenedBy(query, parameters)}{Reverts(query, parameters)}
) AS "Listed"
""";

Expand All @@ -101,6 +101,11 @@ private static string OpenedBy(ChangeSetListQuery query, List<object> parameters
return $" AND s.\"InitiatorKind\" = {Parameter(parameters, kind)} AND s.\"InitiatorId\" = {Parameter(parameters, id)}";
}

private static string Reverts(ChangeSetListQuery query, List<object> parameters)
{
return query.Reverts is { } reverted ? $" AND s.\"RevertsChangeSetId\" = {Parameter(parameters, reverted)}" : string.Empty;
}

private static string Parameter(List<object> parameters, object value)
{
parameters.Add(value);
Expand Down
Loading
Loading