Finished adding basic category adding functionality

This commit is contained in:
Vincent Allen
2025-07-21 16:28:47 -04:00
parent 84108877d5
commit a822ad8559
184 changed files with 6557 additions and 28 deletions

View File

@@ -4,6 +4,7 @@
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
<MudAppBar Elevation="1">
<MudStaticNavDrawerToggle DrawerId="nav-drawer" Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" />

View File

@@ -0,0 +1,42 @@
@inject ICategoryProvider CategoryProvider;
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<MudText Typo="Typo.h6">Categories</MudText>
<MudDivider></MudDivider>
<MudList T="string">
@foreach (Category category in _categories)
{
<MudListItem Text=@category.CategoryName OnClick="@(() => OnCategoryItemClicked(category.CategoryName))"></MudListItem>
}
</MudList>
@ChildContent
</MudPaper>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
public List<Category> _categories = new();
[Parameter]
public EventCallback<string> ListItemClickedCallback { get; set; }
protected override async Task OnInitializedAsync()
{
var categories = await CategoryProvider.AllCategories();
_categories.AddRange(categories);
}
public async Task RefreshData()
{
_categories.Clear();
var categories = await CategoryProvider.AllCategories();
_categories.AddRange(categories);
StateHasChanged();
}
protected async Task OnCategoryItemClicked(string categoryName)
{
await ListItemClickedCallback.InvokeAsync(categoryName);
}
}

View File

@@ -0,0 +1,166 @@
@using System.ComponentModel.DataAnnotations;
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">Create a Category</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="_form">
<MudTextField @bind-Value="Model.Name"
For="@(() => Model.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="Model.FieldSeparator"
@bind-Value:after="UpdateFormatPreview"
For="@(() => Model.FieldSeparator)"
Label="Field Separator"
Variant="Variant.Filled"
MaxLength="1" />
<MudDivider Class="pt-4" />
<MudNumericField
Value="Model.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 < Model.FieldNames.Count; ++index)
{
var localIndex = index;
<MudItem xs="12" sm="6" md="6">
<CategoryFieldCardComponent Index="localIndex"
FieldName="@Model.FieldNames[localIndex]"
FieldDescription="@Model.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 ICategoryProvider CategoryProvider;
@code {
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public CategoryModel Model { 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()
{
Model ??= new CategoryModel { NumFields = 1 };
UpdateStateFromModel();
}
private void OnNumFieldsChanged(int newCount)
{
if (newCount < 1) return; // Prevent invalid counts
Model.NumFields = newCount;
UpdateStateFromModel();
}
private void UpdateStateFromModel()
{
Model.FieldNames ??= new List<string>();
Model.FieldDescriptions ??= new List<string>();
while (Model.FieldNames.Count < Model.NumFields)
{
Model.FieldNames.Add($"Field {Model.FieldNames.Count + 1}");
}
while (Model.FieldNames.Count > Model.NumFields)
{
Model.FieldNames.RemoveAt(Model.FieldNames.Count - 1);
}
while (Model.FieldDescriptions.Count < Model.NumFields)
{
Model.FieldDescriptions.Add("");
}
while (Model.FieldDescriptions.Count > Model.NumFields)
{
Model.FieldDescriptions.RemoveAt(Model.FieldDescriptions.Count - 1);
}
UpdateFormatPreview();
StateHasChanged();
}
private void UpdateFormatPreview()
{
var fieldNames = Model.FieldNames.Select(name => string.IsNullOrEmpty(name) ? "<...>" : $"<{name}>");
FormatPreview = string.Join(Model.FieldSeparator, fieldNames);
}
private async Task Submit()
{
await _form.Validate();
if (!_form.IsValid) return;
var categoryToSave = Model.ToCategory();
if (IsUpdate)
{
int lines = await CategoryProvider.UpdateCategoryAsync(OriginalName, categoryToSave);
Console.WriteLine($"{lines} effected");
}
else
{
await CategoryProvider.InsertCategoryAsync(categoryToSave);
}
MudDialog.Close(DialogResult.Ok(Model.ToCategory()));
}
private void Cancel() => MudDialog.Cancel();
// In your MudDialog component's @code block
private void HandleNameUpdate((int Index, string NewValue) data)
{
if (data.Index < Model.FieldNames.Count)
{
Model.FieldNames[data.Index] = data.NewValue;
UpdateFormatPreview(); // Update the preview in real-time
}
}
private void HandleDescriptionUpdate((int Index, string NewValue) data)
{
if (data.Index < Model.FieldDescriptions.Count)
{
Model.FieldDescriptions[data.Index] = data.NewValue;
}
}
}

View File

@@ -0,0 +1,49 @@
@* CategoryFieldCardComponent.razor *@
<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 {
// IN: The index of this field in the parent's list
[Parameter] public int Index { get; set; }
// IN: The initial values from the parent
[Parameter] public string FieldName { get; set; } = "";
[Parameter] public string FieldDescription { get; set; } = "";
// OUT: Callbacks to notify the parent of changes
[Parameter] public EventCallback<(int Index, string NewValue)> OnNameUpdate { get; set; }
[Parameter] public EventCallback<(int Index, string NewValue)> OnDescriptionUpdate { get; set; }
protected override void OnParametersSet()
{
// Sync internal state when parent's data changes
}
private async Task OnNameChanged()
{
await OnNameUpdate.InvokeAsync((Index, FieldName));
}
private async Task OnDescriptionChanged()
{
await OnDescriptionUpdate.InvokeAsync((Index, FieldDescription));
}
}

View File

@@ -0,0 +1,11 @@
using OpenArchival.Database;
using OpenArchival.Database.Category;
using System.ComponentModel.DataAnnotations;
public class CategoryFieldValidationModel
{
[Required(ErrorMessage = "A field name must be provided.")]
public string FieldName { get; set; } = "";
public string Description { get; set; } = "";
}

View File

@@ -0,0 +1,29 @@
using OpenArchival.Database.Category;
using System.ComponentModel.DataAnnotations;
public class CategoryModel
{
[Required(ErrorMessage = "Category name is required.")]
public string Name { 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 Category ToCategory()
{
return new Category() { CategoryName = Name, FieldSeparator = FieldSeparator, FieldNames = FieldNames.ToArray(), FieldDescriptions = FieldDescriptions.ToArray() };
}
public static CategoryModel FromCategory(Category category)
{
return new CategoryModel() { Name = category.CategoryName, FieldSeparator=category.FieldSeparator, NumFields=category.FieldNames.Length, FieldNames = new(category.FieldNames), FieldDescriptions = new(category.FieldDescriptions)};
}
}

View File

@@ -0,0 +1,56 @@
@page "/categories"
@inject IDialogService DialogService
@inject ICategoryProvider CategoryProvider;
<CategoriesListComponent @ref=_categoriesListComponent ListItemClickedCallback="ShowFilledDialog">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OnAddClick">Add Category</MudButton>
</CategoriesListComponent>
@code {
CategoriesListComponent _categoriesListComponent = default!;
protected override async Task OnInitializedAsync()
{
}
private async Task ShowFilledDialog(string categoryName)
{
Category? category = await CategoryProvider.GetCategoryAsync(categoryName);
if (category is null)
{
throw new ArgumentNullException($"The passed in categoryName={categoryName} resulted in no category in the database");
}
CategoryModel validationModel = CategoryModel.FromCategory(category);
var parameters = new DialogParameters { ["Model"] = validationModel, ["IsUpdate"] = true, ["OriginalName"] = category.CategoryName};
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)
{
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)
{
StateHasChanged();
await _categoriesListComponent.RefreshData();
}
}
}

View File

@@ -1,4 +1,5 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components;
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using MudBlazor.StaticInput
@@ -12,3 +13,4 @@
@using MudBlazor.Services
@using OpenArchival.Blazor
@using OpenArchival.Blazor.Components
@using OpenArchival.Database.Category;