Added automatic newsletter distribution to articles on publish
This commit is contained in:
parent
9cf97fbef7
commit
a94852298d
|
@ -8,162 +8,163 @@
|
||||||
|
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
@attribute [Authorize(Policy = "ArticleEditOrReviewPermissions")]
|
@attribute [Authorize(Policy = "ArticleEditOrReviewPermissions")]
|
||||||
|
@inject ILogger<ArticleEditor> Logger
|
||||||
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
|
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject UserManager<ApplicationUser> UserManager
|
@inject UserManager<ApplicationUser> UserManager
|
||||||
@inject IStringLocalizer<ArticleEditor> Localizer
|
@inject IStringLocalizer<ArticleEditor> Localizer
|
||||||
|
|
||||||
@if (Article is not null) {
|
@if (Article is not null) {
|
||||||
<PageTitle>@(TitlePrefix + Localizer["PageTitle_Edit"]) | @Article.Title</PageTitle>
|
<PageTitle>@(TitlePrefix + Localizer["PageTitle_Edit"]) | @Article.Title</PageTitle>
|
||||||
} else {
|
} else {
|
||||||
<PageTitle>@(TitlePrefix + Localizer["PageTitle_New"])</PageTitle>
|
<PageTitle>@(TitlePrefix + Localizer["PageTitle_New"])</PageTitle>
|
||||||
}
|
}
|
||||||
@if (Busy) {
|
@if (Busy) {
|
||||||
<div class="flex place-content-center">
|
<div class="flex place-content-center">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ChildContent>
|
<ChildContent>
|
||||||
<h1 class="text-3xl lg:text-5xl font-light mb-6 text-primary">@Localizer["EditorTitle"]</h1>
|
<h1 class="text-3xl lg:text-5xl font-light mb-6 text-primary">@Localizer["EditorTitle"]</h1>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<ul class="steps w-full max-w-xs">
|
<ul class="steps w-full max-w-xs">
|
||||||
<li class="step @(Article?.Status >= ArticleStatus.Draft ? "step-secondary": "")">@Localizer["Draft"]</li>
|
<li class="step @(Article?.Status >= ArticleStatus.Draft ? "step-secondary": "")">@Localizer["Draft"]</li>
|
||||||
<li class="step @(Article?.Status >= ArticleStatus.InReview ? "step-secondary": "")">@Localizer["InReview"]</li>
|
<li class="step @(Article?.Status >= ArticleStatus.InReview ? "step-secondary": "")">@Localizer["InReview"]</li>
|
||||||
<li class="step @(Article?.Status >= ArticleStatus.Published ? "step-secondary": "")">@Localizer["Published"]</li>
|
<li class="step @(Article?.Status >= ArticleStatus.Published ? "step-secondary": "")">@Localizer["Published"]</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditForm method="post" FormName="article-editor" Model="@Model" OnValidSubmit="OnValidSubmit">
|
<EditForm method="post" FormName="article-editor" Model="@Model" OnValidSubmit="OnValidSubmit">
|
||||||
<DataAnnotationsValidator />
|
<DataAnnotationsValidator />
|
||||||
<input type="hidden" @bind-value="@Model.Id" />
|
<input type="hidden" @bind-value="@Model.Id" />
|
||||||
|
|
||||||
<InputLabelComponent LabelText="@Localizer["Title_Label"]" For="() => Model.Title">
|
<InputLabelComponent LabelText="@Localizer["Title_Label"]" For="() => Model.Title">
|
||||||
<InputText class="input input-bordered w-full" maxlength="256" required aria-required
|
<InputText class="input input-bordered w-full" maxlength="256" required aria-required
|
||||||
@bind-Value="@Model.Title" placeholder="@Localizer["Title_Placeholder"]" autocomplete="off" />
|
@bind-Value="@Model.Title" placeholder="@Localizer["Title_Placeholder"]" autocomplete="off" />
|
||||||
</InputLabelComponent>
|
</InputLabelComponent>
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-8">
|
||||||
<InputLabelComponent LabelText="@Localizer["PublishDate_Label"]" For="() => Model.PublishDate">
|
<InputLabelComponent LabelText="@Localizer["PublishDate_Label"]" For="() => Model.PublishDate">
|
||||||
@if (Article?.Status is null or not ArticleStatus.Published || Article.PublishDate >= DateTimeOffset.UtcNow) {
|
@if (Article?.Status is null or not ArticleStatus.Published || Article.PublishDate >= DateTimeOffset.UtcNow) {
|
||||||
<InputDate class="input input-bordered w-full" Type="InputDateType.DateTimeLocal"
|
<InputDate class="input input-bordered w-full" Type="InputDateType.DateTimeLocal"
|
||||||
@bind-Value="@Model.PublishDate" placeholder="@Localizer["PublishDate_Placeholder"]" autocomplete="off" />
|
@bind-Value="@Model.PublishDate" placeholder="@Localizer["PublishDate_Placeholder"]" autocomplete="off" />
|
||||||
} else {
|
} else {
|
||||||
<input class="input input-bordered w-full"
|
<input class="input input-bordered w-full"
|
||||||
type="datetime-local" readonly value="@Article?.PublishDate.ToString("yyyy-MM-dd\\THH:mm:ss")" />
|
type="datetime-local" readonly value="@Article?.PublishDate.ToString("yyyy-MM-dd\\THH:mm:ss")" />
|
||||||
}
|
}
|
||||||
</InputLabelComponent>
|
</InputLabelComponent>
|
||||||
<InputLabelComponent LabelText="@Localizer["Categories_Label"]" For="() => Model.Categories">
|
<InputLabelComponent LabelText="@Localizer["Categories_Label"]" For="() => Model.Categories">
|
||||||
<select class="select select-bordered w-full h-32" @onchange="CategorySelectionChanged" multiple>
|
<select class="select select-bordered w-full h-32" @onchange="CategorySelectionChanged" multiple>
|
||||||
@foreach (var category in Categories) {
|
@foreach (var category in Categories) {
|
||||||
<option value="@category.Id" selected="@Model.Categories?.Contains(category.Id)">@category.Name</option>
|
<option value="@category.Id" selected="@Model.Categories?.Contains(category.Id)">@category.Name</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</InputLabelComponent>
|
</InputLabelComponent>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AdvancedMarkdownEditor Title="@Model.Title" MarkdownCallback="() => Model.Body">
|
<AdvancedMarkdownEditor Title="@Model.Title" MarkdownCallback="() => Model.Body">
|
||||||
<div class="join join-vertical min-h-96 h-full w-full" aria-role="toolbar">
|
<div class="join join-vertical min-h-96 h-full w-full" aria-role="toolbar">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarSection>
|
<ToolbarSection>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('# ', true);"
|
<ToolbarButton onclick="window.insertBeforeSelection('# ', true);"
|
||||||
title="@Localizer["Tools_H1_Tooltip"]">
|
title="@Localizer["Tools_H1_Tooltip"]">
|
||||||
<strong>@Localizer["Tools_H1_Label"]</strong>
|
<strong>@Localizer["Tools_H1_Label"]</strong>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('## ', true);"
|
<ToolbarButton onclick="window.insertBeforeSelection('## ', true);"
|
||||||
title="@Localizer["Tools_H2_Tooltip"]">
|
title="@Localizer["Tools_H2_Tooltip"]">
|
||||||
<strong>@Localizer["Tools_H2_Label"]</strong>
|
<strong>@Localizer["Tools_H2_Label"]</strong>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('### ', true);"
|
<ToolbarButton onclick="window.insertBeforeSelection('### ', true);"
|
||||||
title="@Localizer["Tools_H3_Tooltip"]">
|
title="@Localizer["Tools_H3_Tooltip"]">
|
||||||
<strong>@Localizer["Tools_H3_Label"]</strong>
|
<strong>@Localizer["Tools_H3_Label"]</strong>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('#### ', true);"
|
<ToolbarButton onclick="window.insertBeforeSelection('#### ', true);"
|
||||||
title="@Localizer["Tools_H4_Tooltip"]">
|
title="@Localizer["Tools_H4_Tooltip"]">
|
||||||
@Localizer["Tools_H4_Label"]
|
@Localizer["Tools_H4_Label"]
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</ToolbarSection>
|
</ToolbarSection>
|
||||||
<ToolbarSection>
|
<ToolbarSection>
|
||||||
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('**');"
|
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('**');"
|
||||||
title="@Localizer["Tools_Bold_Tooltip"]">
|
title="@Localizer["Tools_Bold_Tooltip"]">
|
||||||
<strong>B</strong>
|
<strong>B</strong>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('*')"
|
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('*')"
|
||||||
title="@Localizer["Tools_Italic_Tooltip"]">
|
title="@Localizer["Tools_Italic_Tooltip"]">
|
||||||
<em>I</em>
|
<em>I</em>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('++')"
|
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('++')"
|
||||||
title="@Localizer["Tools_Underline_Tooltip"]">
|
title="@Localizer["Tools_Underline_Tooltip"]">
|
||||||
<u>U</u>
|
<u>U</u>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('~~')"
|
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('~~')"
|
||||||
title="@Localizer["Tools_StrikeThrough_Tooltip"]">
|
title="@Localizer["Tools_StrikeThrough_Tooltip"]">
|
||||||
<del>@Localizer["Tools_StrikeThrough_Label"]</del>
|
<del>@Localizer["Tools_StrikeThrough_Label"]</del>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('==')"
|
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('==')"
|
||||||
title="@Localizer["Tools_Mark_Tooltip"]">
|
title="@Localizer["Tools_Mark_Tooltip"]">
|
||||||
<mark>@Localizer["Tools_Mark_Label"]</mark>
|
<mark>@Localizer["Tools_Mark_Label"]</mark>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('> ', true)"
|
<ToolbarButton onclick="window.insertBeforeSelection('> ', true)"
|
||||||
title="@Localizer["Tools_Cite_Tooltip"]">
|
title="@Localizer["Tools_Cite_Tooltip"]">
|
||||||
| <em>@Localizer["Tools_Cite_Label"]</em>
|
| <em>@Localizer["Tools_Cite_Label"]</em>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</ToolbarSection>
|
</ToolbarSection>
|
||||||
<ToolbarSection>
|
<ToolbarSection>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('1. ', true)">
|
<ToolbarButton onclick="window.insertBeforeSelection('1. ', true)">
|
||||||
1.
|
1.
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('a. ', true)">
|
<ToolbarButton onclick="window.insertBeforeSelection('a. ', true)">
|
||||||
a.
|
a.
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('A. ', true)">
|
<ToolbarButton onclick="window.insertBeforeSelection('A. ', true)">
|
||||||
A.
|
A.
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('i. ', true)">
|
<ToolbarButton onclick="window.insertBeforeSelection('i. ', true)">
|
||||||
i.
|
i.
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeSelection('I. ', true)">
|
<ToolbarButton onclick="window.insertBeforeSelection('I. ', true)">
|
||||||
I.
|
I.
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</ToolbarSection>
|
</ToolbarSection>
|
||||||
<ToolbarSection>
|
<ToolbarSection>
|
||||||
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('`')"
|
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('`')"
|
||||||
title="@Localizer["Tools_CodeLine_Tooltip"]">
|
title="@Localizer["Tools_CodeLine_Tooltip"]">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
||||||
<path fill-rule="evenodd" d="M14.447 3.026a.75.75 0 0 1 .527.921l-4.5 16.5a.75.75 0 0 1-1.448-.394l4.5-16.5a.75.75 0 0 1 .921-.527ZM16.72 6.22a.75.75 0 0 1 1.06 0l5.25 5.25a.75.75 0 0 1 0 1.06l-5.25 5.25a.75.75 0 1 1-1.06-1.06L21.44 12l-4.72-4.72a.75.75 0 0 1 0-1.06Zm-9.44 0a.75.75 0 0 1 0 1.06L2.56 12l4.72 4.72a.75.75 0 0 1-1.06 1.06L.97 12.53a.75.75 0 0 1 0-1.06l5.25-5.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M14.447 3.026a.75.75 0 0 1 .527.921l-4.5 16.5a.75.75 0 0 1-1.448-.394l4.5-16.5a.75.75 0 0 1 .921-.527ZM16.72 6.22a.75.75 0 0 1 1.06 0l5.25 5.25a.75.75 0 0 1 0 1.06l-5.25 5.25a.75.75 0 1 1-1.06-1.06L21.44 12l-4.72-4.72a.75.75 0 0 1 0-1.06Zm-9.44 0a.75.75 0 0 1 0 1.06L2.56 12l4.72 4.72a.75.75 0 0 1-1.06 1.06L.97 12.53a.75.75 0 0 1 0-1.06l5.25-5.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('\n```\n')"
|
<ToolbarButton onclick="window.insertBeforeAndAfterSelection('\n```\n')"
|
||||||
title="@Localizer["Tools_CodeBlock_Tooltip"]">
|
title="@Localizer["Tools_CodeBlock_Tooltip"]">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
||||||
<path fill-rule="evenodd" d="M3 6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6Zm14.25 6a.75.75 0 0 1-.22.53l-2.25 2.25a.75.75 0 1 1-1.06-1.06L15.44 12l-1.72-1.72a.75.75 0 1 1 1.06-1.06l2.25 2.25c.141.14.22.331.22.53Zm-10.28-.53a.75.75 0 0 0 0 1.06l2.25 2.25a.75.75 0 1 0 1.06-1.06L8.56 12l1.72-1.72a.75.75 0 1 0-1.06-1.06l-2.25 2.25Z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M3 6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6Zm14.25 6a.75.75 0 0 1-.22.53l-2.25 2.25a.75.75 0 1 1-1.06-1.06L15.44 12l-1.72-1.72a.75.75 0 1 1 1.06-1.06l2.25 2.25c.141.14.22.331.22.53Zm-10.28-.53a.75.75 0 0 0 0 1.06l2.25 2.25a.75.75 0 1 0 1.06-1.06L8.56 12l1.72-1.72a.75.75 0 1 0-1.06-1.06l-2.25 2.25Z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</ToolbarSection>
|
</ToolbarSection>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<textarea id="tool-target" class="textarea textarea-bordered outline-none w-full flex-1 join-item"
|
<textarea id="tool-target" class="textarea textarea-bordered outline-none w-full flex-1 join-item"
|
||||||
required aria-required placeholder="@Localizer["Body_Placeholder"]"
|
required aria-required placeholder="@Localizer["Body_Placeholder"]"
|
||||||
@bind="@Model.Body" @bind:event="oninput" autocomplete="off"></textarea>
|
@bind="@Model.Body" @bind:event="oninput" autocomplete="off"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</AdvancedMarkdownEditor>
|
</AdvancedMarkdownEditor>
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap mt-3">
|
<div class="flex gap-2 flex-wrap mt-3">
|
||||||
<button type="submit" class="btn btn-primary w-full sm:btn-wide">
|
<button type="submit" class="btn btn-primary w-full sm:btn-wide">
|
||||||
@Localizer["EditorSubmit"]
|
@Localizer["EditorSubmit"]
|
||||||
</button>
|
</button>
|
||||||
@if (Article is not null) {
|
@if (Article is not null) {
|
||||||
<a class="btn w-full sm:btn-wide" href="/article/@(Article.Id)">
|
<a class="btn w-full sm:btn-wide" href="/article/@(Article.Id)">
|
||||||
@Localizer["ViewArticle_Label"]
|
@Localizer["ViewArticle_Label"]
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</EditForm>
|
</EditForm>
|
||||||
<CategoryPicker />
|
<CategoryPicker />
|
||||||
</ChildContent>
|
</ChildContent>
|
||||||
<ErrorContent>
|
<ErrorContent>
|
||||||
<h1 class="text-3xl lg:text-5xl font-light mb-6">Not found</h1>
|
<h1 class="text-3xl lg:text-5xl font-light mb-6">Not found</h1>
|
||||||
</ErrorContent>
|
</ErrorContent>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
}
|
}
|
||||||
|
|
||||||
<SectionContent SectionName="scripts">
|
<SectionContent SectionName="scripts">
|
||||||
|
@ -175,12 +176,12 @@
|
||||||
const end = target.selectionEnd;
|
const end = target.selectionEnd;
|
||||||
const value = target.value;
|
const value = target.value;
|
||||||
let doStart = start;
|
let doStart = start;
|
||||||
if (startOfLine) {
|
if (startOfLine) {
|
||||||
doStart = value.lastIndexOf("\n", start) +1;
|
doStart = value.lastIndexOf("\n", start) +1;
|
||||||
}
|
}
|
||||||
|
|
||||||
target.focus();
|
target.focus();
|
||||||
target.value = value.substring(0, doStart) + markdown + value.substring(doStart);
|
target.value = value.substring(0, doStart) + markdown + value.substring(doStart);
|
||||||
|
|
||||||
target.selectionStart = start + markdown.length;
|
target.selectionStart = start + markdown.length;
|
||||||
target.selectionEnd = end + markdown.length;
|
target.selectionEnd = end + markdown.length;
|
||||||
|
@ -188,174 +189,181 @@
|
||||||
target.dispatchEvent(new Event("input", { bubbles: true }));
|
target.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
window.insertBeforeAndAfterSelection = function (markdown) {
|
window.insertBeforeAndAfterSelection = function (markdown) {
|
||||||
const target = document.getElementById("tool-target");
|
const target = document.getElementById("tool-target");
|
||||||
|
|
||||||
const start = target.selectionStart;
|
const start = target.selectionStart;
|
||||||
const end = target.selectionEnd;
|
const end = target.selectionEnd;
|
||||||
const value = target.value;
|
const value = target.value;
|
||||||
|
|
||||||
target.focus();
|
target.focus();
|
||||||
target.value = value.substring(0, start) +
|
target.value = value.substring(0, start) +
|
||||||
markdown + value.substring(start, end) + markdown +
|
markdown + value.substring(start, end) + markdown +
|
||||||
value.substring(end);
|
value.substring(end);
|
||||||
|
|
||||||
target.selectionStart = start + markdown.length;
|
target.selectionStart = start + markdown.length;
|
||||||
target.selectionEnd = end + markdown.length;
|
target.selectionEnd = end + markdown.length;
|
||||||
target.focus();
|
target.focus();
|
||||||
target.dispatchEvent(new Event("input", { bubbles: true }));
|
target.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[CascadingParameter(Name = "TitlePrefix")]
|
[CascadingParameter(Name = "TitlePrefix")]
|
||||||
private string TitlePrefix { get; set; } = default!;
|
private string TitlePrefix { get; set; } = default!;
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
private Task<AuthenticationState>? AuthenticationState { get; set; }
|
private Task<AuthenticationState>? AuthenticationState { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Guid? Id { get; set; }
|
public Guid? Id { get; set; }
|
||||||
[SupplyParameterFromForm]
|
[SupplyParameterFromForm]
|
||||||
private InputModel Model { get; set; } = new();
|
private InputModel Model { get; set; } = new();
|
||||||
|
|
||||||
private List<Category> Categories { get; set; } = new();
|
private List<Category> Categories { get; set; } = new();
|
||||||
private ApplicationUser? User { get; set; }
|
private ApplicationUser? User { get; set; }
|
||||||
private Article? Article { get; set; }
|
private Article? Article { get; set; }
|
||||||
private bool Busy { get; set; } = true;
|
private bool Busy { get; set; } = true;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
if (AuthenticationState is null) throw new ApplicationException("???");
|
if (AuthenticationState is null) throw new ApplicationException("???");
|
||||||
var state = await AuthenticationState;
|
var state = await AuthenticationState;
|
||||||
var user = await UserManager.GetUserAsync(state.User);
|
var user = await UserManager.GetUserAsync(state.User);
|
||||||
User = user ?? throw new ApplicationException("???2");
|
User = user ?? throw new ApplicationException("???2");
|
||||||
|
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||||
if (Id is not null) {
|
if (Id is not null) {
|
||||||
Article = await context.Set<Article>()
|
Article = await context.Set<Article>()
|
||||||
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
|
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
|
||||||
.Include(a => a.Author)
|
.Include(a => a.Author)
|
||||||
.Include(a => a.Reviewer)
|
.Include(a => a.Reviewer)
|
||||||
.Include(a => a.Categories)
|
.Include(a => a.Categories)
|
||||||
.FirstAsync(a => a.Id == Id);
|
.FirstAsync(a => a.Id == Id);
|
||||||
if (Article is null) throw new ApplicationException("Article not found.");
|
if (Article is null) throw new ApplicationException("Article not found.");
|
||||||
|
|
||||||
// Check permissions
|
// Check permissions
|
||||||
if (state.User.IsInRole("Admin")) {
|
if (state.User.IsInRole("Admin")) {
|
||||||
// Admins always have access
|
// Admins always have access
|
||||||
} else if (Article.Status is ArticleStatus.Draft && Article.Author.Id == User.Id) {
|
} else if (Article.Status is ArticleStatus.Draft && Article.Author.Id == User.Id) {
|
||||||
// It's our draft
|
// It's our draft
|
||||||
} else if (Article.Status is ArticleStatus.InReview && state.User.IsInRole("Reviewer")) {
|
} else if (Article.Status is ArticleStatus.InReview && state.User.IsInRole("Reviewer")) {
|
||||||
// It's in reviewer, and we are reviewer
|
// It's in reviewer, and we are reviewer
|
||||||
} else if (Article.Status is ArticleStatus.Published && state.User.IsInRole("Moderator")) {
|
} else if (Article.Status is ArticleStatus.Published && state.User.IsInRole("Moderator")) {
|
||||||
// It's published, and we are moderator
|
// It's published, and we are moderator
|
||||||
} else {
|
} else {
|
||||||
throw new ApplicationException("You do not have permissions to edit this article");
|
throw new ApplicationException("You do not have permissions to edit this article");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Categories = await context.Set<Category>().OrderBy(c => c.Color).ToListAsync();
|
Categories = await context.Set<Category>().OrderBy(c => c.Color).ToListAsync();
|
||||||
|
|
||||||
if (Article is not null) {
|
if (Article is not null) {
|
||||||
Model.Id ??= Article.Id;
|
Model.Id ??= Article.Id;
|
||||||
Model.Title ??= Article.Title;
|
Model.Title ??= Article.Title;
|
||||||
Model.Body ??= Article.Body;
|
Model.Body ??= Article.Body;
|
||||||
Model.PublishDate ??= Article.PublishDate.LocalDateTime;
|
Model.PublishDate ??= Article.PublishDate.LocalDateTime;
|
||||||
Model.Categories ??= Article.Categories.Select(c => c.Id).ToArray();
|
Model.Categories ??= Article.Categories.Select(c => c.Id).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
Busy = false;
|
Busy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnValidSubmit() {
|
private async Task OnValidSubmit() {
|
||||||
if (User is null) return;
|
if (User is null) return;
|
||||||
Busy = true;
|
Busy = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||||
context.Entry(User).State = EntityState.Unchanged;
|
context.Entry(User).State = EntityState.Unchanged;
|
||||||
foreach (var category in Categories) {
|
foreach (var category in Categories) {
|
||||||
context.Entry(category).State = EntityState.Unchanged;
|
context.Entry(category).State = EntityState.Unchanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
Article article;
|
Article article;
|
||||||
if (Model.Id is not null) {
|
if (Model.Id is not null) {
|
||||||
article = await context.Set<Article>()
|
article = await context.Set<Article>()
|
||||||
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
|
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
|
||||||
.Include(a => a.Author)
|
.Include(a => a.Author)
|
||||||
.Include(a => a.Reviewer)
|
.Include(a => a.Reviewer)
|
||||||
.Include(a => a.Categories)
|
.Include(a => a.Categories)
|
||||||
.FirstAsync(a => a.Id == Model.Id);
|
.FirstAsync(a => a.Id == Model.Id);
|
||||||
article.Title = Model.Title!;
|
article.Title = Model.Title!;
|
||||||
article.Body = Model.Body!;
|
article.Body = Model.Body!;
|
||||||
} else {
|
} else {
|
||||||
article = new Article {
|
article = new Article {
|
||||||
Title = Model.Title!,
|
Title = Model.Title!,
|
||||||
Body = Model.Body!,
|
Body = Model.Body!,
|
||||||
Author = User
|
Author = User
|
||||||
};
|
};
|
||||||
await context.AddAsync(article);
|
await context.AddAsync(article);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Model.PublishDate is not null) article.PublishDate = Model.PublishDate.Value;
|
if (Model.PublishDate is not null) article.PublishDate = Model.PublishDate.Value;
|
||||||
|
|
||||||
await HandleRoles(article, User);
|
await HandleRoles(article, User);
|
||||||
article.LastModified = DateTimeOffset.UtcNow;
|
article.LastModified = DateTimeOffset.UtcNow;
|
||||||
article.BodyHtml = MarkdownUtilities.Parse(article.Body);
|
article.BodyHtml = MarkdownUtilities.Parse(article.Body);
|
||||||
foreach (var category in article.Categories.ToList()) {
|
foreach (var category in article.Categories.ToList()) {
|
||||||
if (Model.Categories?.Contains(category.Id) is not true) {
|
if (Model.Categories?.Contains(category.Id) is not true) {
|
||||||
article.Categories.Remove(category);
|
article.Categories.Remove(category);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var categoryId in Model.Categories ?? Array.Empty<Guid>()) {
|
foreach (var categoryId in Model.Categories ?? Array.Empty<Guid>()) {
|
||||||
if (article.Categories.Any(c => c.Id == categoryId) is not true) {
|
if (article.Categories.Any(c => c.Id == categoryId) is not true) {
|
||||||
article.Categories.Add(Categories.First(c => c.Id == categoryId));
|
article.Categories.Add(Categories.First(c => c.Id == categoryId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
var newsletter = await context.Set<EmailNewsletter>().FirstOrDefaultAsync(n => n.Article == article);
|
||||||
Navigation.NavigateTo($"/article/{article.Id}");
|
if (newsletter is not null) {
|
||||||
} catch (Exception ex) {
|
newsletter.DistributionDateTime = article.PublishDate;
|
||||||
// TODO toast
|
}
|
||||||
} finally {
|
|
||||||
Busy = false;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleRoles(Article article, ApplicationUser me) {
|
await context.SaveChangesAsync();
|
||||||
// it's our draft
|
|
||||||
if (article.Status is ArticleStatus.Draft && article.Author.Id == me.Id) return;
|
|
||||||
|
|
||||||
var roles = await UserManager.GetRolesAsync(me);
|
Navigation.NavigateTo($"/article/{article.Id}");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// TODO toast
|
||||||
|
Logger.LogError(ex, "Failed to save article.");
|
||||||
|
} finally {
|
||||||
|
Busy = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// reviewers and admins can review articles
|
private async Task HandleRoles(Article article, ApplicationUser me) {
|
||||||
if (article.Status is ArticleStatus.InReview && roles.Any(r => r is "Admin" or "Reviewer")) {
|
// it's our draft
|
||||||
article.Reviewer = me;
|
if (article.Status is ArticleStatus.Draft && article.Author.Id == me.Id) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// published articles may only be edited my admins or moderators
|
var roles = await UserManager.GetRolesAsync(me);
|
||||||
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");
|
// reviewers and admins can review articles
|
||||||
}
|
if (article.Status is ArticleStatus.InReview && roles.Any(r => r is "Admin" or "Reviewer")) {
|
||||||
|
article.Reviewer = me;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
private void CategorySelectionChanged(ChangeEventArgs args) {
|
// published articles may only be edited my admins or moderators
|
||||||
Model.Categories = ((string[]?)args.Value)?.Select(Guid.Parse).ToArray();
|
if (article.Status is ArticleStatus.Published && roles.Any(r => r is "Admin" or "Reviewer")) {
|
||||||
}
|
article.Reviewer = me; // TODO replace with editor or something?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class InputModel {
|
throw new ApplicationException("You do not have permissions to edit this article");
|
||||||
public Guid? Id { get; set; }
|
}
|
||||||
[Required(AllowEmptyStrings = false), MaxLength(256)]
|
|
||||||
public string? Title { get; set; }
|
private void CategorySelectionChanged(ChangeEventArgs args) {
|
||||||
[Required(AllowEmptyStrings = false)]
|
Model.Categories = ((string[]?)args.Value)?.Select(Guid.Parse).ToArray();
|
||||||
public string? Body { get; set; }
|
}
|
||||||
|
|
||||||
|
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 Guid[]? Categories { get; set; }
|
public Guid[]? Categories { get; set; }
|
||||||
public DateTimeOffset? PublishDate { get; set; }
|
public DateTimeOffset? PublishDate { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
@using System.Diagnostics.CodeAnalysis
|
@using System.Diagnostics.CodeAnalysis
|
||||||
@using Microsoft.Extensions.Options
|
@using Microsoft.Extensions.Options
|
||||||
|
|
||||||
|
@inject ILogger<ArticleView> Logger
|
||||||
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
|
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IOptions<Customization> Customizations
|
@inject IOptions<Customization> Customizations
|
||||||
|
@ -15,62 +16,62 @@
|
||||||
<PageTitle>@(TitlePrefix + (Article?.Title ?? Localizer["NotFound_Title"]))</PageTitle>
|
<PageTitle>@(TitlePrefix + (Article?.Title ?? Localizer["NotFound_Title"]))</PageTitle>
|
||||||
|
|
||||||
@if (Article is not null) {
|
@if (Article is not null) {
|
||||||
<HeadContent>
|
<HeadContent>
|
||||||
<meta name="author" content="@Article.Author.Name">
|
<meta name="author" content="@Article.Author.Name">
|
||||||
<meta name="description" content="@string.Format(Localizer["Meta_Description"], Customizations.Value.AppName, Article.Body[..Math.Min(80, Article.Body.Length)] + "... ")">
|
<meta name="description" content="@string.Format(Localizer["Meta_Description"], Customizations.Value.AppName, Article.Body[..Math.Min(80, Article.Body.Length)] + "... ")">
|
||||||
|
|
||||||
<!-- Open Graph -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:title" content="@Article.Title">
|
<meta property="og:title" content="@Article.Title">
|
||||||
<meta property="og:description" content="@string.Format(Localizer["Meta_Description"], Customizations.Value.AppName, Article.Body[..Math.Min(80, Article.Body.Length)] + "... ")">
|
<meta property="og:description" content="@string.Format(Localizer["Meta_Description"], Customizations.Value.AppName, Article.Body[..Math.Min(80, Article.Body.Length)] + "... ")">
|
||||||
<meta property="og:url" content="@Navigation.ToAbsoluteUri("/article/" + Article.Id)">
|
<meta property="og:url" content="@Navigation.ToAbsoluteUri("/article/" + Article.Id)">
|
||||||
<meta property="og:image" content="@Navigation.ToAbsoluteUri("/api/user/pfp/" + Article.Author.Id)">
|
<meta property="og:image" content="@Navigation.ToAbsoluteUri("/api/user/pfp/" + Article.Author.Id)">
|
||||||
<meta property="og:type" content="article">
|
<meta property="og:type" content="article">
|
||||||
<meta property="og:article:author" content="@Article.Author.Name">
|
<meta property="og:article:author" content="@Article.Author.Name">
|
||||||
<meta property="og:article:published_time" content="@Article.PublishDate.ToString("u")">
|
<meta property="og:article:published_time" content="@Article.PublishDate.ToString("u")">
|
||||||
@if (Article.LastModified.HasValue) {
|
@if (Article.LastModified.HasValue) {
|
||||||
<meta property="og:article:modified_time" content="@Article.LastModified.Value.ToString("u")">
|
<meta property="og:article:modified_time" content="@Article.LastModified.Value.ToString("u")">
|
||||||
}
|
}
|
||||||
<meta property="og:site_name" content="@Customizations.Value.AppName">
|
<meta property="og:site_name" content="@Customizations.Value.AppName">
|
||||||
@if (Features.Value.Rss) {
|
@if (Features.Value.Rss) {
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS Feed on @Customizations.Value.AppName" href="/rss/rss.xml">
|
<link rel="alternate" type="application/rss+xml" title="RSS Feed on @Customizations.Value.AppName" href="/rss/rss.xml">
|
||||||
<link rel="alternate" type="application/atom+xml" title="Atom RSS Feed on @Customizations.Value.AppName" href="/rss/atom.xml">
|
<link rel="alternate" type="application/atom+xml" title="Atom RSS Feed on @Customizations.Value.AppName" href="/rss/atom.xml">
|
||||||
}
|
}
|
||||||
</HeadContent>
|
</HeadContent>
|
||||||
}
|
}
|
||||||
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ChildContent>
|
<ChildContent>
|
||||||
<AuthorizeView Policy="ArticleEditOrReviewPermissions">
|
<AuthorizeView Policy="ArticleEditOrReviewPermissions">
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<ArticleComponent Article="@GetArticleProtected(context.User)" />
|
<ArticleComponent Article="@GetArticleProtected(context.User)" />
|
||||||
<div class="flex gap-2 mt-3 flex-wrap">
|
<div class="flex gap-2 mt-3 flex-wrap">
|
||||||
<a class="btn btn-info w-full sm:btn-wide" href="article/@Article.Id/edit" data-enhance-nav="false">@Localizer["Edit"]</a>
|
<a class="btn btn-info w-full sm:btn-wide" href="article/@Article.Id/edit" data-enhance-nav="false">@Localizer["Edit"]</a>
|
||||||
@if (Article.Status is ArticleStatus.Draft) {
|
@if (Article.Status is ArticleStatus.Draft) {
|
||||||
<form @formname="submit-for-review" method="post" @onsubmit="SubmitForReview" class="max-sm:w-full">
|
<form @formname="submit-for-review" method="post" @onsubmit="SubmitForReview" class="max-sm:w-full">
|
||||||
<AntiforgeryToken />
|
<AntiforgeryToken />
|
||||||
<button type="submit" class="btn btn-primary w-full sm:btn-wide">@Localizer["Review_Submit"]</button>
|
<button type="submit" class="btn btn-primary w-full sm:btn-wide">@Localizer["Review_Submit"]</button>
|
||||||
</form>
|
</form>
|
||||||
} else if (Article.Status is ArticleStatus.InReview) {
|
} else if (Article.Status is ArticleStatus.InReview) {
|
||||||
<form @formname="submit-for-publish" method="post" @onsubmit="SubmitForPublish" class="max-sm:w-full">
|
<form @formname="submit-for-publish" method="post" @onsubmit="SubmitForPublish" class="max-sm:w-full">
|
||||||
<AntiforgeryToken />
|
<AntiforgeryToken />
|
||||||
<button type="submit" class="btn btn-primary w-full sm:btn-wide">@Localizer["Publish_Submit"]</button>
|
<button type="submit" class="btn btn-primary w-full sm:btn-wide">@Localizer["Publish_Submit"]</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
<ArticleComponent Article="@GetArticlePublic()" />
|
<ArticleComponent Article="@GetArticlePublic()" />
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
</ChildContent>
|
</ChildContent>
|
||||||
<ErrorContent>
|
<ErrorContent>
|
||||||
<h1 class="text-3xl lg:text-5xl font-light mb-6 text-primary">@Localizer["NotFound_Title"]</h1>
|
<h1 class="text-3xl lg:text-5xl font-light mb-6 text-primary">@Localizer["NotFound_Title"]</h1>
|
||||||
<p class="my-3">@Localizer["NotFound_Description"]</p>
|
<p class="my-3">@Localizer["NotFound_Description"]</p>
|
||||||
<a class="btn btn-primary" href="/">@Localizer["NotFound_BackToHome_Label"]</a>
|
<a class="btn btn-primary" href="/">@Localizer["NotFound_BackToHome_Label"]</a>
|
||||||
@if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.ToLower() == "development") {
|
@if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.ToLower() == "development") {
|
||||||
<p class="mt-3">[DEBUG] EXCEPTION MESSAGE: @context.Message</p>
|
<p class="mt-3">[DEBUG] EXCEPTION MESSAGE: @context.Message</p>
|
||||||
}
|
}
|
||||||
</ErrorContent>
|
</ErrorContent>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
@ -80,108 +81,122 @@
|
||||||
#region Route Parameters
|
#region Route Parameters
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Guid? Id { get; set; }
|
public Guid? Id { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public int? Year { get; set; }
|
public int? Year { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public int? Month { get; set; }
|
public int? Month { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public int? Day { get; set; }
|
public int? Day { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? TitleEncoded { get; set; }
|
public string? TitleEncoded { get; set; }
|
||||||
|
|
||||||
private DateTimeOffset? Date =>
|
private DateTimeOffset? Date =>
|
||||||
Year is {} y && Month is {} m && Day is {} d
|
Year is {} y && Month is {} m && Day is {} d
|
||||||
? new DateTimeOffset(new DateTime(y, m, d)) :
|
? new DateTimeOffset(new DateTime(y, m, d)) :
|
||||||
null;
|
null;
|
||||||
private string? Title => TitleEncoded is null ? null : Uri.UnescapeDataString(TitleEncoded.Replace("-", "%20").Replace("+", "-"));
|
private string? Title => TitleEncoded is null ? null : Uri.UnescapeDataString(TitleEncoded.Replace("-", "%20").Replace("+", "-"));
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
private Article? Article { get; set; }
|
private Article? Article { get; set; }
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public HttpContext HttpContext { get; set; } = default!;
|
public HttpContext HttpContext { get; set; } = default!;
|
||||||
|
|
||||||
private Article GetArticlePublic() {
|
private Article GetArticlePublic() {
|
||||||
if (Article is null) throw new ApplicationException("Article not found.");
|
if (Article is null) throw new ApplicationException("Article not found.");
|
||||||
if (Article.Status >= ArticleStatus.Published && Article.PublishDate <= DateTimeOffset.UtcNow) {
|
if (Article.Status >= ArticleStatus.Published && Article.PublishDate <= DateTimeOffset.UtcNow) {
|
||||||
return Article;
|
return Article;
|
||||||
}
|
}
|
||||||
throw new ApplicationException("Article is not public.");
|
throw new ApplicationException("Article is not public.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement")]
|
[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement")]
|
||||||
private Article GetArticleProtected(ClaimsPrincipal principal) {
|
private Article GetArticleProtected(ClaimsPrincipal principal) {
|
||||||
if (Article is null) throw new ApplicationException("Article not found.");
|
if (Article is null) throw new ApplicationException("Article not found.");
|
||||||
|
|
||||||
// The Article is publicly available
|
// The Article is publicly available
|
||||||
if (Article.Status >= ArticleStatus.Published && Article.PublishDate <= DateTimeOffset.UtcNow) {
|
if (Article.Status >= ArticleStatus.Published && Article.PublishDate <= DateTimeOffset.UtcNow) {
|
||||||
return Article;
|
return Article;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admins always get access
|
// Admins always get access
|
||||||
if (principal.IsInRole("Admin")) {
|
if (principal.IsInRole("Admin")) {
|
||||||
return Article;
|
return Article;
|
||||||
}
|
}
|
||||||
|
|
||||||
// You can only access your own drafts
|
// You can only access your own drafts
|
||||||
if (Article.Status is ArticleStatus.Draft) {
|
if (Article.Status is ArticleStatus.Draft) {
|
||||||
if (Article.Author.Id == principal.FindFirst("Id")!.Value) {
|
if (Article.Author.Id == principal.FindFirst("Id")!.Value) {
|
||||||
return Article;
|
return Article;
|
||||||
}
|
}
|
||||||
throw new ApplicationException("Cannot access draft article without being author or admin.");
|
throw new ApplicationException("Cannot access draft article without being author or admin.");
|
||||||
}
|
}
|
||||||
// InReview Articles can only be accessed by reviewers
|
// InReview Articles can only be accessed by reviewers
|
||||||
if (Article.Status is ArticleStatus.InReview) {
|
if (Article.Status is ArticleStatus.InReview) {
|
||||||
if (principal.IsInRole("Reviewer")) {
|
if (principal.IsInRole("Reviewer")) {
|
||||||
return Article;
|
return Article;
|
||||||
}
|
}
|
||||||
throw new ApplicationException("Cannot access in-review article without being a reviewer or admin.");
|
throw new ApplicationException("Cannot access in-review article without being a reviewer or admin.");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ApplicationException("User does not have access to this article.");
|
throw new ApplicationException("User does not have access to this article.");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override void OnInitialized() {
|
||||||
// We need blocking calls here, bc otherwise Blazor will execute Render in parallel,
|
// We need blocking calls here, bc otherwise Blazor will execute Render in parallel,
|
||||||
// running into a null pointer on the Article property and panicking
|
// running into a null pointer on the Article property and panicking
|
||||||
if (Id is not null) {
|
if (Id is not null) {
|
||||||
using var context = ContextFactory.CreateDbContext();
|
using var context = ContextFactory.CreateDbContext();
|
||||||
Article = context.Set<Article>()
|
Article = context.Set<Article>()
|
||||||
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
|
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
|
||||||
.Include(a => a.Author)
|
.Include(a => a.Author)
|
||||||
.Include(a => a.Reviewer)
|
.Include(a => a.Reviewer)
|
||||||
.Include(a => a.Categories)
|
.Include(a => a.Categories)
|
||||||
.FirstOrDefault(a => a.Id == Id);
|
.FirstOrDefault(a => a.Id == Id);
|
||||||
} else if (Date is { } date && Title is { } title) {
|
} else if (Date is { } date && Title is { } title) {
|
||||||
using var context = ContextFactory.CreateDbContext();
|
using var context = ContextFactory.CreateDbContext();
|
||||||
Article = context.Set<Article>()
|
Article = context.Set<Article>()
|
||||||
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
|
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
|
||||||
.Include(a => a.Author)
|
.Include(a => a.Author)
|
||||||
.Include(a => a.Reviewer)
|
.Include(a => a.Reviewer)
|
||||||
.Include(a => a.Categories)
|
.Include(a => a.Categories)
|
||||||
.FirstOrDefault(a => a.PublishDate.Date == date.Date && a.Title.ToLower() == title);
|
.FirstOrDefault(a => a.PublishDate.Date == date.Date && a.Title.ToLower() == title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SubmitForReview() {
|
private async Task SubmitForReview() {
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||||
Article!.Status = ArticleStatus.InReview;
|
Article!.Status = ArticleStatus.InReview;
|
||||||
context.Update(Article);
|
context.Update(Article);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
Navigation.NavigateTo("/");
|
Navigation.NavigateTo("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SubmitForPublish() {
|
private async Task SubmitForPublish() {
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||||
Article!.Status = ArticleStatus.Published;
|
Article!.Status = ArticleStatus.Published;
|
||||||
string userId = HttpContext.User.FindFirst("Id")!.Value;
|
string userId = HttpContext.User.FindFirst("Id")!.Value;
|
||||||
if (Article.Author.Id != userId) {
|
if (Article.Author.Id != userId) {
|
||||||
Article.Reviewer = await context.Users.FindAsync(userId);
|
Article.Reviewer = await context.Users.FindAsync(userId);
|
||||||
}
|
}
|
||||||
context.Update(Article);
|
context.Update(Article);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
Navigation.NavigateTo("/");
|
|
||||||
}
|
if (Features.Value.EmailSubscriptions) {
|
||||||
|
try {
|
||||||
|
var newsletter = new EmailNewsletter {
|
||||||
|
Article = Article,
|
||||||
|
DistributionDateTime = Article.PublishDate
|
||||||
|
};
|
||||||
|
context.Add(newsletter);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.LogError(ex, "Failed to schedule article {name} for E-Mail distribution.", Article.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigation.NavigateTo("/");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue