Wave/Wave/Components/Pages/ArticleEditor.razor

198 lines
8.4 KiB
Plaintext

@page "/article/new"
@page "/article/{id:guid}/edit"
@using Wave.Data
@using Microsoft.EntityFrameworkCore
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@using Wave.Utilities
@rendermode InteractiveServer
@attribute [Authorize(Policy = "ArticleEditOrReviewPermissions")]
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
@inject NavigationManager Navigation
@inject UserManager<ApplicationUser> UserManager
@inject IStringLocalizer<ArticleEditor> Localizer
@if (Article is not null) {
<PageTitle>@(TitlePrefix + Localizer["PageTitle_Edit"]) | @Article.Title</PageTitle>
} else {
<PageTitle>@(TitlePrefix + Localizer["PageTitle_New"])</PageTitle>
}
@if (Busy) {
<div class="flex place-content-center">
<span class="loading loading-spinner loading-lg"></span>
</div>
} else {
<ErrorBoundary>
<ChildContent>
<h1 class="text-3xl lg:text-5xl font-light mb-6">@Localizer["EditorTitle"]</h1>
<div class="w-full">
<ul class="steps w-full max-w-xs">
<li class="step @(Article?.Status >= ArticleStatus.Draft ? "step-primary": "")">@Localizer["Draft"]</li>
<li class="step @(Article?.Status >= ArticleStatus.InReview ? "step-primary": "")">@Localizer["InReview"]</li>
<li class="step @(Article?.Status >= ArticleStatus.Published ? "step-primary": "")">@Localizer["Published"]</li>
</ul>
</div>
<EditForm method="post" FormName="article-editor" Model="@Model" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator />
<input type="hidden" @bind-value="@Model.Id" />
<InputLabelComponent LabelText="@Localizer["Title_Label"]" For="() => Model.Title">
<InputText class="input input-bordered w-full" maxlength="256" required aria-required
@bind-Value="@Model.Title" placeholder="@Localizer["Title_Placeholder"]" autocomplete="off" />
</InputLabelComponent>
<InputLabelComponent LabelText="@Localizer["PublishDate_Label"]" For="() => Model.PublishDate">
@if (Article?.Status is null or ArticleStatus.Draft)
{
<InputDate class="input input-bordered w-full" Type="InputDateType.DateTimeLocal"
@bind-Value="@Model.PublishDate" placeholder="@Localizer["PublishDate_Placeholder"]" autocomplete="off" />
}
else
{
<input class="input input-bordered w-full"
type="datetime-local" readonly value="@Article?.PublishDate.ToString("yyyy-MM-dd\\THH:mm:ss")" />
}
</InputLabelComponent>
<AdvancedMarkdownEditor Title="@Model.Title" MarkdownCallback="() => Model.Body">
<InputLabelComponent LabelText="@Localizer["Body_Label"]" For="() => Model.Body">
<textarea class="textarea textarea-bordered w-full min-h-96 h-full" required aria-required
@bind="@Model.Body" @bind:event="oninput" placeholder="@Localizer["Body_Placeholder"]" autocomplete="off"></textarea>
</InputLabelComponent>
</AdvancedMarkdownEditor>
<div class="flex gap-2 flex-wrap mt-3">
<button type="submit" class="btn btn-primary w-full sm:btn-wide">
@Localizer["EditorSubmit"]
</button>
@if (Article is not null)
{
<a class="btn w-full sm:btn-wide" href="/article/@(Article.Id)">
@Localizer["ViewArticle_Label"]
</a>
}
</div>
</EditForm>
</ChildContent>
<ErrorContent>
<h1 class="text-3xl lg:text-5xl font-light mb-6">Not found</h1>
</ErrorContent>
</ErrorBoundary>
}
@code {
[CascadingParameter(Name = "TitlePrefix")]
private string TitlePrefix { get; set; } = default!;
[CascadingParameter]
private Task<AuthenticationState>? AuthenticationState { get; set; }
[Parameter]
public Guid? Id { get; set; }
[SupplyParameterFromForm]
private InputModel Model { get; set; } = new();
private ApplicationUser? User { get; set; }
private Article? Article { get; set; }
private bool Busy { get; set; } = true;
protected override async Task OnInitializedAsync() {
if (AuthenticationState is null) throw new ApplicationException("???");
var state = await AuthenticationState;
var user = await UserManager.GetUserAsync(state.User);
User = user ?? throw new ApplicationException("???2");
if (Id is not null) {
await using var context = await ContextFactory.CreateDbContextAsync();
Article = await context.Set<Article>()
.Include(a => a.Author)
.Include(a => a.Reviewer)
.FirstAsync(a => a.Id == Id);
if (Article is null) throw new ApplicationException("Article not found.");
// Check permissions
if (state.User.IsInRole("Admin")) {
// Admins always have access
} else if (Article.Status is ArticleStatus.Draft && Article.Author.Id == User.Id) {
// It's our draft
} else if (Article.Status is ArticleStatus.InReview && state.User.IsInRole("Reviewer")) {
// It's in reviewer, and we are reviewer
} else if (Article.Status is ArticleStatus.Published && state.User.IsInRole("Moderator")) {
// It's published, and we are moderator
} else {
throw new ApplicationException("You do not have permissions to edit this article");
}
}
Model.Id ??= Article?.Id;
Model.Title ??= Article?.Title;
Model.Body ??= Article?.Body;
Model.PublishDate ??= Article?.PublishDate.LocalDateTime;
Busy = false;
}
private async Task OnValidSubmit() {
if (User is null) return;
Busy = true;
await using var context = await ContextFactory.CreateDbContextAsync();
context.Entry(User).State = EntityState.Unchanged;
Article article;
if (Model.Id is not null) {
article = await context.Set<Article>()
.Include(a => a.Author)
.Include(a => a.Reviewer)
.FirstAsync(a => a.Id == Model.Id);
article.Title = Model.Title!;
article.Body = Model.Body!;
} else {
article = new Article {
Title = Model.Title!,
Body = Model.Body!,
Author = User
};
await context.AddAsync(article);
}
if (Model.PublishDate is not null) article.PublishDate = Model.PublishDate.Value;
await HandleRoles(article, User);
article.LastModified = DateTimeOffset.UtcNow;
article.BodyHtml = MarkdownUtilities.Parse(article.Body);
await context.SaveChangesAsync();
Navigation.NavigateTo($"/article/{article.Id}");
}
private async Task HandleRoles(Article article, ApplicationUser me) {
// it's our draft
if (article.Status is ArticleStatus.Draft && article.Author.Id == me.Id) return;
var roles = await UserManager.GetRolesAsync(me);
// reviewers and admins can review articles
if (article.Status is ArticleStatus.InReview && roles.Any(r => r is "Admin" or "Reviewer")) {
article.Reviewer = me;
return;
}
// published articles may only be edited my admins or moderators
if (article.Status is ArticleStatus.Published && roles.Any(r => r is "Admin" or "Reviewer")) {
article.Reviewer = me; // TODO replace with editor or something?
return;
}
throw new ApplicationException("You do not have permissions to edit this article");
}
private sealed class InputModel {
public Guid? Id { get; set; }
[Required(AllowEmptyStrings = false), MaxLength(256)]
public string? Title { get; set; }
[Required(AllowEmptyStrings = false)]
public string? Body { get; set; }
public DateTimeOffset? PublishDate { get; set; }
}
}