Extracted some pages to their own assembly and finished the artifact display page code

This commit is contained in:
Vincent Allen
2025-10-08 13:08:12 -04:00
parent fd0e6290fe
commit 02c2660b09
626 changed files with 39989 additions and 1553 deletions

View File

@@ -0,0 +1,86 @@
@namespace OpenArchival.Blazor.AdminPages
@using Microsoft.EntityFrameworkCore;
@using Microsoft.Extensions.Logging
@using MudBlazor.Interfaces
@using OpenArchival.DataAccess
@using MudBlazor
@using MudExtensions
@page "/categorieslist"
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<MudText Typo="Typo.h6">Categories</MudText>
<MudDivider Class="mb-2"></MudDivider>
<MudList T="string" Clickable="true">
@foreach (ArchiveCategory category in _categories)
{
<MudListItem OnClick="@(() => OnCategoryItemClicked(category))">
@category.Name
@if (ShowDeleteButton)
{
<MudListItemMeta ActionPosition="ActionPosition.End">
<MudIconButton
Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@((e) => HandleDeleteClick(category))"
/>
</MudListItemMeta>
}
</MudListItem>
}
</MudList>
@ChildContent
</MudPaper>
@inject IArchiveCategoryProvider CategoryProvider;
@inject ILogger<CategoriesListComponent> Logger;
@code {
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
[Parameter]
public bool ShowDeleteButton { get; set; } = false;
[Parameter]
public EventCallback<ArchiveCategory> ListItemClickedCallback { get; set; }
[Parameter]
public EventCallback<ArchiveCategory> OnDeleteClickedCallback { get; set; }
private List<ArchiveCategory> _categories = new();
protected override async Task OnInitializedAsync()
{
await LoadCategories();
}
private async Task LoadCategories()
{
var categories = await CategoryProvider.GetAllArchiveCategories();
if (categories is null)
{
Logger.LogError("There were no categories in the database when attempting to load the list of categories.");
_categories.Clear();
return;
}
_categories = categories.ToList();
}
public async Task RefreshData()
{
await LoadCategories();
StateHasChanged();
}
private async Task OnCategoryItemClicked(ArchiveCategory category)
{
await ListItemClickedCallback.InvokeAsync(category);
}
private async Task HandleDeleteClick(ArchiveCategory category)
{
await OnDeleteClickedCallback.InvokeAsync(category);
}
}

View File

@@ -0,0 +1,165 @@
@namespace OpenArchival.Blazor.AdminPages
@using System.ComponentModel.DataAnnotations;
@using OpenArchival.DataAccess;
@using MudBlazor
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">Create a Category</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="_form">
<MudTextField @bind-Value="ValidationModel.Name"
For="@(() => ValidationModel.Name)"
Label="Category Name"
Variant="Variant.Filled" />
<MudDivider Class="pt-4" DividerType="DividerType.Middle"/>
<MudText Typo="Typo.h6">Item Tag Identifier</MudText>
<MudText Typo="Typo.subtitle2">This will be the format of the identifier used for each archive entry.</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudText Typo="Typo.body2">Format Preview: </MudText>
<MudText Type="Typo.body2" Color="Color.Primary">@FormatPreview</MudText>
</MudStack>
<MudTextField @bind-Value="ValidationModel.FieldSeparator"
@bind-Value:after="UpdateFormatPreview"
For="@(() => ValidationModel.FieldSeparator)"
Label="Field Separator"
Variant="Variant.Filled"
MaxLength="1" />
<MudDivider Class="pt-4" />
<MudNumericField
Value="ValidationModel.NumFields"
ValueChanged="@((int newCount) => OnNumFieldsChanged(newCount))"
Label="Number of fields in the item identifiers"
Variant="Variant.Filled"
Min="1"></MudNumericField>
<MudDivider Class="pt-4" />
<MudGrid Class="pr-2 pt-2 pb-2 pl-8" Justify="Justify.FlexStart" Spacing="3">
@for (int index = 0; index < ValidationModel.FieldNames.Count; ++index)
{
var localIndex = index;
<MudItem xs="12" sm="6" md="6">
<CategoryFieldCardComponent Index="localIndex"
FieldName="@ValidationModel.FieldNames[localIndex]"
FieldDescription="@ValidationModel.FieldDescriptions[localIndex]"
OnNameUpdate="HandleNameUpdate"
OnDescriptionUpdate="HandleDescriptionUpdate"/>
</MudItem>
}
</MudGrid>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">OK</MudButton>
</DialogActions>
</MudDialog>
@inject IArchiveCategoryProvider CategoryProvider;
@code {
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public CategoryValidationModel ValidationModel { get; set; } = default!;
[Parameter]
public bool IsUpdate { get; set; }
[Parameter]
public string OriginalName { get; set; } = string.Empty;
private MudForm _form = default!;
private string FormatPreview { get; set; } = string.Empty;
protected override void OnParametersSet()
{
if (ValidationModel is null)
{
ValidationModel = new CategoryValidationModel { NumFields = 1 };
} else
{
ValidationModel.NumFields = ValidationModel.FieldNames.Count;
}
}
private void OnNumFieldsChanged(int newCount)
{
if (newCount < 1) return;
ValidationModel.NumFields = newCount;
UpdateStateFromModel();
}
private void UpdateStateFromModel()
{
ValidationModel.FieldNames ??= new List<string>();
ValidationModel.FieldDescriptions ??= new List<string>();
while (ValidationModel.FieldNames.Count < ValidationModel.NumFields)
{
ValidationModel.FieldNames.Add($"Field {ValidationModel.FieldNames.Count + 1}");
}
while (ValidationModel.FieldNames.Count > ValidationModel.NumFields)
{
ValidationModel.FieldNames.RemoveAt(ValidationModel.FieldNames.Count - 1);
}
while (ValidationModel.FieldDescriptions.Count < ValidationModel.NumFields)
{
ValidationModel.FieldDescriptions.Add("");
}
while (ValidationModel.FieldDescriptions.Count > ValidationModel.NumFields)
{
ValidationModel.FieldDescriptions.RemoveAt(ValidationModel.FieldDescriptions.Count - 1);
}
UpdateFormatPreview();
StateHasChanged();
}
private void UpdateFormatPreview()
{
var fieldNames = ValidationModel.FieldNames.Select(name => string.IsNullOrEmpty(name) ? "<...>" : $"<{name}>");
FormatPreview = string.Join(ValidationModel.FieldSeparator, fieldNames);
}
private async Task Submit()
{
await _form.Validate();
if (!_form.IsValid) return;
MudDialog.Close(DialogResult.Ok(ValidationModel));
}
private void Cancel() => MudDialog.Cancel();
// In your MudDialog component's @code block
private void HandleNameUpdate((int Index, string NewValue) data)
{
if (data.Index < ValidationModel.FieldNames.Count)
{
ValidationModel.FieldNames[data.Index] = data.NewValue;
UpdateFormatPreview(); // Update the preview in real-time
}
}
private void HandleDescriptionUpdate((int Index, string NewValue) data)
{
if (data.Index < ValidationModel.FieldDescriptions.Count)
{
ValidationModel.FieldDescriptions[data.Index] = data.NewValue;
}
}
}

View File

@@ -0,0 +1,42 @@
@namespace OpenArchival.Blazor.AdminPages
@using MudBlazor
<MudCard Outlined="true">
<MudCardContent>
<MudTextField @bind-Value="FieldName"
@bind-Value:after="OnNameChanged"
Label="Field Name"
Variant="Variant.Filled"
Immediate="true"
/>
<MudTextField @bind-Value="FieldDescription"
@bind-Value:after="OnDescriptionChanged"
Label="Field Description"
Variant="Variant.Filled"
Lines="2"
Class="mt-3"
Immediate="true"
/>
</MudCardContent>
</MudCard>
@code {
[Parameter] public int Index { get; set; }
[Parameter] public string FieldName { get; set; } = "";
[Parameter] public string FieldDescription { get; set; } = "";
[Parameter] public EventCallback<(int Index, string NewValue)> OnNameUpdate { get; set; }
[Parameter] public EventCallback<(int Index, string NewValue)> OnDescriptionUpdate { get; set; }
private async Task OnNameChanged()
{
await OnNameUpdate.InvokeAsync((Index, FieldName));
}
private async Task OnDescriptionChanged()
{
await OnDescriptionUpdate.InvokeAsync((Index, FieldDescription));
}
}

View File

@@ -0,0 +1,25 @@
namespace OpenArchival.Blazor;
public class ArtifactGroupingRowElement
{
public required int Id { get; set; }
public required string ArtifactGroupingIdentifier { get; set; }
public required string CategoryName { get; set; }
public required string Title { get; set; }
public bool IsPublicallyVisible { get; set; }
public bool Equals(ArtifactGroupingRowElement? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Id == other.Id; // Compare based on the unique Id
}
public override bool Equals(object? obj) => Equals(obj as ArtifactGroupingRowElement);
public override int GetHashCode() => Id.GetHashCode();
}

View File

@@ -0,0 +1,74 @@
namespace OpenArchival.Blazor;
using Microsoft.IdentityModel.Abstractions;
using Microsoft.IdentityModel.Tokens;
using OpenArchival.DataAccess;
using System.ComponentModel.DataAnnotations;
public class CategoryValidationModel
{
public int? DatabaseId { get; set; }
[Required(ErrorMessage = "Category name is required.")]
public string? Name { get; set; }
public string? Description { get; set; }
[Required(ErrorMessage = "Field separator is required.")]
[StringLength(1, ErrorMessage = "Separator must be a single character.")]
public string FieldSeparator { get; set; } = "-";
[Required(ErrorMessage = "At least one field is needed")]
[Range(1, int.MaxValue, ErrorMessage = "At least one field must be created.")]
public int NumFields { get; set; } = 1;
public List<string> FieldNames { get; set; } = [""];
public List<string> FieldDescriptions { get; set; } = [""];
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if ((FieldNames is null || FieldNames.Count == 0) || (FieldDescriptions is null || FieldDescriptions.Count > 0))
{
yield return new ValidationResult(
"Either the FieldNames or FieldDescriptions were null or empty. At least one is required",
new[] { nameof(FieldNames), nameof(FieldDescriptions) }
);
}
}
public static CategoryValidationModel FromArchiveCategory(ArchiveCategory category)
{
return new CategoryValidationModel()
{
Name = category.Name,
Description = category.Description,
DatabaseId = category.Id,
FieldSeparator = category.FieldSeparator,
FieldNames = category.FieldNames,
FieldDescriptions = category.FieldDescriptions,
};
}
public static ArchiveCategory ToArchiveCategory(CategoryValidationModel model)
{
return new ArchiveCategory()
{
Name = model.Name,
FieldSeparator = model.FieldSeparator,
Description = model.Description,
FieldNames = model.FieldNames,
FieldDescriptions = model.FieldDescriptions
};
}
public static void UpdateArchiveValidationModel(CategoryValidationModel model, ArchiveCategory category)
{
category.Name = model.Name ?? throw new ArgumentNullException(nameof(model.Name), "The model name was null.");
category.Description = model.Description;
category.FieldSeparator = model.FieldSeparator;
category.FieldNames = model.FieldNames;
category.FieldDescriptions = model.FieldDescriptions;
}
}

View File

@@ -0,0 +1,86 @@
@page "/categories"
@namespace OpenArchival.Blazor.AdminPages
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.EntityFrameworkCore
@using Microsoft.Extensions.Logging
@using MudBlazor
@using OpenArchival.DataAccess;
@inject IDialogService DialogService
@inject IArchiveCategoryProvider CategoryProvider;
@inject IDbContextFactory<ApplicationDbContext> DbContextFactory;
@inject ILogger<ViewAddCategoriesComponent> Logger;
<CategoriesListComponent
@ref=_categoriesListComponent
ListItemClickedCallback="ShowFilledDialog"
ShowDeleteButton=true
OnDeleteClickedCallback="DeleteCategory">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OnAddClick">Add Category</MudButton>
</CategoriesListComponent>
@code {
CategoriesListComponent _categoriesListComponent = default!;
private async Task DeleteCategory(ArchiveCategory category)
{
// 1. Show a confirmation dialog (recommended)
var confirmed = await DialogService.ShowMessageBox("Confirm", $"Delete {category.Name}?", yesText:"Delete", cancelText:"Cancel");
if (confirmed != true) return;
await CategoryProvider.DeleteCategoryAsync(category);
await _categoriesListComponent.RefreshData();
StateHasChanged();
}
private async Task ShowFilledDialog(ArchiveCategory category)
{
CategoryValidationModel validationModel = CategoryValidationModel.FromArchiveCategory(category);
var parameters = new DialogParameters { ["ValidationModel"] = validationModel, ["IsUpdate"] = true, ["OriginalName"] = category.Name};
var options = new DialogOptions { CloseOnEscapeKey = true, BackdropClick=false};
var dialog = await DialogService.ShowAsync<CategoryCreatorDialog>("Create a Category", parameters, options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && _categoriesListComponent is not null)
{
if (result.Data is null)
{
Logger.LogError($"The new category received by the result had a null data result member.");
throw new NullReferenceException($"The new category received by the result had a null data result member.");
}
CategoryValidationModel model = (CategoryValidationModel)result.Data;
CategoryValidationModel.UpdateArchiveValidationModel(model, category);
await using var context = await DbContextFactory.CreateDbContextAsync();
await context.SaveChangesAsync();
StateHasChanged();
await _categoriesListComponent.RefreshData();
}
}
private async Task OnAddClick()
{
var options = new DialogOptions { CloseOnEscapeKey = true, BackdropClick=false };
var dialog = await DialogService.ShowAsync<CategoryCreatorDialog>("Create a Category", options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && _categoriesListComponent is not null && result.Data is not null)
{
await using var context = await DbContextFactory.CreateDbContextAsync();
CategoryValidationModel model = (CategoryValidationModel)result.Data;
context.ArchiveCategories.Add(CategoryValidationModel.ToArchiveCategory(model));
await context.SaveChangesAsync();
StateHasChanged();
await _categoriesListComponent.RefreshData();
}
}
}