Adding of archive items is mostly operational. Need to handle file upload

This commit is contained in:
Vincent Allen
2025-07-29 16:16:42 -04:00
parent 13c45e8459
commit 6475a28263
158 changed files with 2628 additions and 801 deletions

View File

@@ -0,0 +1,308 @@
@page "/add"
@using OpenArchival.Blazor.Components.CustomComponents;
@using OpenArchival.Blazor.Components.Pages.Administration.Categories
@using OpenArchival.Core
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<MudText Typo="Typo.h5" Color="Color.Primary">Add an Archive Item</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
@if (!IsValid && _isFormDivVisible)
{
<MudAlert Severity="Severity.Error" Class="mt-4">
All identifier fields must be filled in.
</MudAlert>
}
@* Archive item category *@
<MudGrid Justify="Justify.Center" Class="pt-4">
<MudItem>
<MudAutocomplete T="string" Label="Category" @bind-Value="Model.Category" @bind-Value:after=OnCategoryChanged SearchFunc="SearchCategory" CoerceValue=false CoerceText=false/>
</MudItem>
<MudItem>
<MudFab Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="OnAddCategoryClicked"/>
</MudItem>
</MudGrid>
<div @ref="_formDiv" style="@_formDivStyle">
@* ID Creation *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Archive Item Identifier</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<IdentifierTextBox @ref="_identifierTextBox" IdentifierFields="@Model.IdentifierFields"></IdentifierTextBox>
@* Title *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Archive Item Title</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudTextField Required=true Placeholder="Archive Item Title" T="string" Class="pl-4 pr-4" @bind-Value=Model.Title @bind-Value:after=OnChanged></MudTextField>
@* Description *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Item Description</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudTextField Lines=8 Placeholder="Description" T="string" Class="pl-4 pr-4" @bind-Value=Model.Description @bind-Value:after=OnChanged></MudTextField>
@* Storage Location *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Storage Location</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudAutocomplete T="string" Label="Storage Location" Class="pt-0 mt-0 pl-2 pr-2" @bind-Value=Model.StorageLocation @bind-Value:after=OnChanged CoerceText=false CoerceValue=false></MudAutocomplete>
@* Artifact Type *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Artifact Type</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudAutocomplete T="string" Label="Artifact Type" Class="pt-0 mt-0 pl-2 pr-2" @bind-Value=Model.ArtifactType @bind-Value:after=OnChanged SearchFunc="SearchItemTypes"></MudAutocomplete>
@* Tags *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Tags</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipTagInput @bind-Items=Model.Tags OnChanged="OnChanged" AutocompleteSearchFunc="SearchTags" InputType="ChipTagInput.ChipTagInputType.AutoComplete" Placeholder="Add tags..."></ChipTagInput>
@* Names *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Listed Names</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipTagInput @bind-Items=Model.AssociatedNames OnChanged="OnChanged" AutocompleteSearchFunc="SearchListedNames" InputType="ChipTagInput.ChipTagInputType.AutoComplete" Placeholder="Add names..."></ChipTagInput>
@* Associated Dates *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Associated Dates</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipTagInput @bind-Items=DatesData OnChanged="OnChanged"></ChipTagInput>
@* Defects *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Defects</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipTagInput @bind-Items=Model.Defects OnChanged="OnChanged" AutocompleteSearchFunc="SearchDefects" InputType="ChipTagInput.ChipTagInputType.AutoComplete" Placeholder="Add defects..."></ChipTagInput>
@* Related Artifacts *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Related Artifacts</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipTagInput @bind-Items=Model.RelatedArtifacts OnChanged="OnChanged"></ChipTagInput>
@* Files *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Artifact Documents</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<UploadDropBox></UploadDropBox>
@* Submit Buttons *@
<MudGrid Justify="Justify.FlexEnd" Class="pt-6">
<MudItem>
<MudCheckBox Label="Publicly Visible" T="bool" @bind-Value=Model.IsPublic></MudCheckBox>
</MudItem>
<MudItem>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="ml-4" OnClick="CancelClicked">Cancel</MudButton>
</MudItem>
<MudItem>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="ml-4" OnClick="PublishClicked">Publish</MudButton>
</MudItem>
</MudGrid>
</div>
</MudPaper>
@using OpenArchival.Database
@using System.ComponentModel.DataAnnotations
@inject IDialogService DialogService
@inject ICategoryProvider CategoryProvider
@inject IArchiveStorageLocationProvider StorageLocationProvider
@inject IArtifactTypesProvider ArtifactTypesProvider
@inject ITagsProvider TagsProvider;
@inject IArtifactAssociatedNamesProvider AssociatedNamesProvider;
@inject IDefectsProvider DefectsProvider;
@inject NavigationManager NavigationManager;
@code {
private IdentifierTextBox _identifierTextBox = default!;
private ElementReference _formDiv = default!;
private bool _isFormDivVisible = false;
private string _formDivStyle => _isFormDivVisible ? "" : "display: none;";
public List<string> DatesData { get; set; } = [];
public List<Category> Categories { get; set; } = new();
private bool _categorySelected = false;
//public List<IdentifierFieldValidationModel> IdentifierFields { get; set; } = [new IdentifierFieldValidationModel() { Name = "Field One", Value = "" }, new IdentifierFieldValidationModel() { Name = "Field Two", Value = "" }, new IdentifierFieldValidationModel() { Name = "Field Three", Value = "" }];
public ArchiveItemValidationModel Model { get; set; } = new();
public bool IsValid { get; set; } = false;
/// <summary>
/// The URI to navigate to if cancel is pressed
/// </summary>
public string? BackLink { get; set; } = "/";
public string? ForwardLink { get; set; } = "/";
private void CancelClicked(MouseEventArgs args)
{
if (BackLink is not null) {
NavigationManager.NavigateTo(BackLink);
}
else
{
throw new ArgumentNullException("No back link provided for the add archive item page.");
}
}
private void PublishClicked(MouseEventArgs args)
{
var validationContext = new ValidationContext(Model);
var validationResult = new List<ValidationResult>();
IsValid = Validator.TryValidateObject(Model, validationContext, validationResult);
if (ForwardLink is not null)
{
if (IsValid)
{
NavigationManager.NavigateTo(ForwardLink);
}
else
{
StateHasChanged();
}
}
else
{
throw new ArgumentNullException("No forward link provided for the add archive item page.");
}
throw new NotImplementedException();
}
void OnChanged()
{
var validationContext = new ValidationContext(Model);
var validationResult = new List<ValidationResult>();
IsValid = Validator.TryValidateObject(Model, validationContext, validationResult);
}
async Task OnCategoryChanged()
{
Category? newCategory = await CategoryProvider.GetCategoryAsync(Model.Category);
if (newCategory is not null)
{
_identifierTextBox.VerifyFormatCategory = newCategory;
_isFormDivVisible = true;
}
if (!_categorySelected)
{
_categorySelected = true;
StateHasChanged();
}
OnChanged();
}
public async Task OnAddCategoryClicked()
{
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)
{
StateHasChanged();
}
}
private async Task<IEnumerable<string>> SearchDefects(string value, CancellationToken cancellationToken)
{
List<string> defects;
if (string.IsNullOrEmpty(value))
{
defects = new(await DefectsProvider.TopDefects(25));
}
else
{
defects = new(await DefectsProvider.SearchDefects(value));
}
return defects;
}
private async Task<IEnumerable<string>> SearchListedNames(string value, CancellationToken cancellationToken)
{
List<string> names;
if (string.IsNullOrEmpty(value))
{
names = new(await AssociatedNamesProvider.TopNames(25));
}
else
{
names = new(await AssociatedNamesProvider.SearchNames(value));
}
return names;
}
private async Task<IEnumerable<string>> SearchTags(string value, CancellationToken cancellationToken)
{
List<string> tags;
if (string.IsNullOrEmpty(value))
{
tags = new(await TagsProvider.TopTags(25));
}
else
{
tags = new(await TagsProvider.SearchTags(value));
}
return tags;
}
private async Task<IEnumerable<string>> SearchItemTypes(string value, CancellationToken cancellationToken)
{
List<string> itemTypes;
if (string.IsNullOrEmpty(value))
{
itemTypes = new(await ArtifactTypesProvider.TopTypes(25));
}
else
{
itemTypes = new(await ArtifactTypesProvider.SearchTypes(value));
}
return itemTypes;
}
private async Task<IEnumerable<string>> SearchStorageLocation(string value, CancellationToken cancellationToken)
{
List<string> storageLocations;
if (string.IsNullOrEmpty(value))
{
storageLocations = new(await StorageLocationProvider.TopLocations(25));
}
else
{
storageLocations = new(await StorageLocationProvider.SearchLocations(value));
}
return storageLocations;
}
private async Task<IEnumerable<string>> SearchCategory(string value, CancellationToken cancellationToken)
{
List<Category> categories;
if (string.IsNullOrEmpty(value))
{
categories = new(await CategoryProvider.TopCategories(25));
}
else
{
categories = new(await CategoryProvider.SearchCategories(value));
}
List<string> categoryStrings = [];
foreach (var category in categories)
{
categoryStrings.Add(category.CategoryName);
}
return categoryStrings;
}
}

View File

@@ -0,0 +1,108 @@
@using System.Text
<MudText Typo="Typo.body2" Color="Color.Primary">Item Identifier: @Value</MudText>
@if (_identifierError)
{
<MudAlert Severity="Severity.Error" Class="mt-4">
All identifier fields must be filled in.
</MudAlert>
}
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudGrid Class="pt-0 pl-4 pr-4" Justify="Justify.FlexStart" AlignItems="AlignItems.Center">
@for (int index = 0; index < IdentifierFields.Count; index++)
{
// You must create a local variable inside the loop for binding to work correctly.
var field = IdentifierFields[index];
<MudItem Class="pt-6">
<MudTextField Label="@field.Name"
@bind-Value="field.Value"
@bind-Value:after="OnInputChanged"
DebounceInterval="100"
Required=true/>
</MudItem>
@if (index < IdentifierFields.Count - 1)
{
<MudItem Class="pt-6">
<MudText>@FieldSeparator</MudText>
</MudItem>
}
}
</MudGrid>
@using OpenArchival.Database;
@inject ICategoryProvider CategoryProvider;
@code {
[Parameter]
public required string FieldSeparator { get; set; } = "-";
private List<IdentifierFieldValidationModel> _identifierFields = new();
[Parameter]
public required List<IdentifierFieldValidationModel> IdentifierFields
{
get => _identifierFields;
set => _identifierFields = value ?? new();
}
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
private Category _verifyFormatCategory;
public Category? VerifyFormatCategory
{
get
{
return _verifyFormatCategory;
}
set
{
if (value is not null)
{
_identifierFields.Clear();
_verifyFormatCategory = value;
foreach (var field in value.FieldsIterator)
{
_identifierFields.Add(new IdentifierFieldValidationModel() {Name=field.Key, Value=""});
}
}
}
}
public bool IsValid { get; set; } = false;
// Computed property that builds the final string
public string Value => string.Join(FieldSeparator, IdentifierFields.Select(f => f.Value).Where(v => !string.IsNullOrEmpty(v)));
private bool _identifierError = false;
/// <summary>
/// This runs when parameters are first set, ensuring the initial state is correct.
/// </summary>
protected override void OnParametersSet()
{
ValidateFields();
StateHasChanged();
}
/// <summary>
/// This runs after the user types into a field.
/// </summary>
private async Task OnInputChanged()
{
ValidateFields();
await ValueChanged.InvokeAsync(this.Value);
}
/// <summary>
/// Reusable method to check the validity of the identifier fields.
/// </summary>
private void ValidateFields()
{
// Set to true if ANY field is empty or null.
_identifierError = IdentifierFields.Any(f => string.IsNullOrEmpty(f.Value));
IsValid = !_identifierError;
}
}

View File

@@ -0,0 +1,52 @@
using OpenArchival.Core;
using System.ComponentModel.DataAnnotations;
namespace OpenArchival.Blazor;
public class ArchiveItemValidationModel
{
[Required(ErrorMessage = "A category is required", AllowEmptyStrings = false)]
public string Category { get; set; } = "";
[Required(ErrorMessage = "An item identifier is required", AllowEmptyStrings = false)]
public List<IdentifierFieldValidationModel> IdentifierFields { get; set; } = new();
public string Identifier { get; set; } = "";
[Required(ErrorMessage = "An item title is required", AllowEmptyStrings = false)]
public string Title { get; set; } = "";
public string? Description { get; set; }
public string? StorageLocation { get; set; }
public string? ArtifactType { get; set; }
public List<string> Tags { get; set; } = new();
public List<string> AssociatedNames { get; set; } = new();
public List<DateTime> AssociatedDates { get; set; } = new();
public List<string> Defects { get; set; } = new();
public List<string> RelatedArtifacts { get; set; } = new();
public bool IsPublic { get; set; } = true;
public ArchiveItem ToArchiveItem(Category category)
{
return new ArchiveItem() {
ArtifactType = ArtifactType,
Category = category,
Defects = Defects,
Description = Description,
AssociatedDates = AssociatedDates,
ItemTitle = Title,
ListedNames = AssociatedNames,
StorageLocation = StorageLocation,
Tags = Tags,
IsPublic = IsPublic
};
}
}

View File

@@ -0,0 +1,7 @@
namespace OpenArchival.Blazor;
public class IdentifierFieldValidationModel
{
public string Name { get; set; } = "";
public string Value { get; set; } = "";
}

View File

@@ -1,4 +1,6 @@
@inject ICategoryProvider CategoryProvider;
@using OpenArchival.Database
@inject ICategoryProvider CategoryProvider;
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<MudText Typo="Typo.h6">Categories</MudText>

View File

@@ -1,4 +1,6 @@
@using System.ComponentModel.DataAnnotations;
@using OpenArchival.Core;
@using OpenArchival.Database;
<MudDialog>
<TitleContent>
@@ -68,7 +70,7 @@
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public CategoryModel Model { get; set; } = default!;
public CategoryValidationModel Model { get; set; } = default!;
[Parameter]
public bool IsUpdate { get; set; }
@@ -81,7 +83,7 @@
protected override void OnParametersSet()
{
Model ??= new CategoryModel { NumFields = 1 };
Model ??= new CategoryValidationModel { NumFields = 1 };
UpdateStateFromModel();
}

View File

@@ -21,22 +21,14 @@
</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));

View File

@@ -1,6 +1,4 @@
using OpenArchival.Database;
using OpenArchival.Database.Category;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
public class CategoryFieldValidationModel
{

View File

@@ -1,7 +1,7 @@
using OpenArchival.Database.Category;
using OpenArchival.Core;
using System.ComponentModel.DataAnnotations;
public class CategoryModel
public class CategoryValidationModel
{
[Required(ErrorMessage = "Category name is required.")]
public string Name { get; set; }
@@ -22,8 +22,8 @@ public class CategoryModel
{
return new Category() { CategoryName = Name, FieldSeparator = FieldSeparator, FieldNames = FieldNames.ToArray(), FieldDescriptions = FieldDescriptions.ToArray() };
}
public static CategoryModel FromCategory(Category category)
public static CategoryValidationModel FromCategory(Category category)
{
return new CategoryModel() { Name = category.CategoryName, FieldSeparator=category.FieldSeparator, NumFields=category.FieldNames.Length, FieldNames = new(category.FieldNames), FieldDescriptions = new(category.FieldDescriptions)};
return new CategoryValidationModel() { Name = category.CategoryName, FieldSeparator=category.FieldSeparator, NumFields=category.FieldNames.Length, FieldNames = new(category.FieldNames), FieldDescriptions = new(category.FieldDescriptions)};
}
}

View File

@@ -1,4 +1,8 @@
@page "/categories"
@using OpenArchival.Core;
@using OpenArchival.Database;
@inject IDialogService DialogService
@inject ICategoryProvider CategoryProvider;
@@ -10,11 +14,6 @@
@code {
CategoriesListComponent _categoriesListComponent = default!;
protected override async Task OnInitializedAsync()
{
}
private async Task ShowFilledDialog(string categoryName)
{
Category? category = await CategoryProvider.GetCategoryAsync(categoryName);
@@ -24,7 +23,7 @@
throw new ArgumentNullException($"The passed in categoryName={categoryName} resulted in no category in the database");
}
CategoryModel validationModel = CategoryModel.FromCategory(category);
CategoryValidationModel validationModel = CategoryValidationModel.FromCategory(category);
var parameters = new DialogParameters { ["Model"] = validationModel, ["IsUpdate"] = true, ["OriginalName"] = category.CategoryName};