Got most of admin panel working. Data issues fixed

This commit is contained in:
Vincent Allen
2025-09-02 09:27:23 -04:00
parent 3d82040e75
commit b2835f65c0
401 changed files with 10478 additions and 10675 deletions

View File

@@ -1,23 +1,35 @@
@page "/add"
@using OpenArchival.Blazor.Components.CustomComponents;
@using OpenArchival.Blazor.Components.CustomComponents;
@using OpenArchival.Blazor.Components.Pages.Administration.Categories
@using OpenArchival.DataAccess;
@using System.ComponentModel.DataAnnotations
@inject IDialogService DialogService
@inject NavigationManager NavigationManager;
@inject IArchiveCategoryProvider CategoryProvider;
@inject ArtifactEntrySharedHelpers Helpers;
<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)
@foreach (var result in ValidationResults)
{
<MudAlert Severity="Severity.Error" Class="mt-4">
All identifier fields must be filled in.
</MudAlert>
<MudAlert Severity="Severity.Error">@result.ErrorMessage</MudAlert>
}
<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/>
<MudAutocomplete
T="ArchiveCategory"
ToStringFunc="@(val => val?.Name)"
Label="Category"
@bind-Value="Model.Category"
@bind-Value:after=OnCategoryChanged
SearchFunc="SearchCategory"
CoerceValue=false
CoerceText=false
/>
</MudItem>
<MudItem>
@@ -28,20 +40,60 @@
</MudPaper>
<div @ref="_formDiv" style="@_formDivStyle">
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<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>
<IdentifierTextBox @ref="_identifierTextBox" IdentifierFields="@Model.IdentifierFieldValues"></IdentifierTextBox>
</MudPaper>
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<UploadDropBox FilesUploaded="OnFilesUploaded" ClearClicked="OnClearFilesClicked"></UploadDropBox>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Grouping Title</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudTextField
T="string"
For="@(() => Model.Title)"
Placeholder="Grouping Title"
@bind-Value=Model.Title></MudTextField>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Grouping Description</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudTextField
T="string"
For="@(() => Model.Description)"
Lines="5"
Placeholder="Grouping Description"
@bind-Value=Model.Description></MudTextField>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Grouping Type</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudAutocomplete
For="@(() => Model.Type)"
T="string"
Label="Artifact Type"
Class="pt-0 mt-0 pl-2 pr-2"
@bind-Value=Model.Type
SearchFunc="Helpers.SearchItemTypes"
CoerceValue=true></MudAutocomplete>
</MudPaper>
@foreach (FilePathListing listing in _filePathListings)
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<UploadDropBox
@ref="@_uploadComponent"
FilesUploaded="OnFilesUploaded"
ClearClicked="OnClearFilesClicked"></UploadDropBox>
</MudPaper>
@for (int index = 0; index < Model.ArtifactEntries.Count; ++index)
{
<ArchiveEntryCreatorCard FilePath="listing" OnValueChanged="OnChanged"></ArchiveEntryCreatorCard>
// Capture the current item in a local variable for the lambda
var currentEntry = Model.ArtifactEntries[index];
<ArchiveEntryCreatorCard Model="currentEntry"
ModelChanged="(updatedEntry) => HandleEntryUpdate(currentEntry, updatedEntry)"
InputsChanged="OnChanged"
@key="currentEntry"
ArtifactEntryIndex="index"
OnEntryDeletedClicked="() => OnDeleteEntryClicked(index)"/>
}
</div>
@@ -51,25 +103,52 @@
@*<MudCheckBox Label="Publicly Visible" T="bool" @bind-Value=Model.IsPublic></MudCheckBox>*@
</MudItem>
<MudItem Style="pr-0">
<MudItem Class="pr-0">
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="ml-4" OnClick="CancelClicked">Cancel</MudButton>
</MudItem>
<MudItem Style="pl-2">
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="ml-4" OnClick="PublishClicked">Publish</MudButton>
<MudItem Class="pl-2">
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="ml-4" OnClick="PublishClicked" Disabled="@(!IsValid)" >Publish</MudButton>
</MudItem>
</MudGrid>
@using System.ComponentModel.DataAnnotations
@inject IDialogService DialogService
@inject NavigationManager NavigationManager;
@inject IArchiveCategoryProvider CategoryProvider;
@code {
[Parameter]
public bool ClearOnPublish { get; set; } = true;
/// <summary>
/// The URI to navigate to if cancel is pressed. null to navigate to no page
/// </summary>
[Parameter]
public string? BackLink { get; set; } = null;
/// <summary>
/// The URI to navigate to if publish is pressed, null to navigate to no page
/// </summary>
[Parameter]
public string? ForwardLink { get; set; } = null;
/// <summary>
/// Called when publish is clicked
/// </summary>
[Parameter]
public EventCallback<ArtifactGroupingValidationModel> GroupingPublished { get; set; }
/// <summary>
/// The model to display on the form
/// </summary>
[Parameter]
public ArtifactGroupingValidationModel Model { get; set; } = new();
private UploadDropBox _uploadComponent = default!;
private IdentifierTextBox _identifierTextBox = default!;
private ElementReference _formDiv = default!;
private bool _isFormDivVisible = false;
private string _formDivStyle => _isFormDivVisible ? "" : "display: none;";
public List<string> DatesData { get; set; } = [];
@@ -80,17 +159,34 @@
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; } = "/";
public List<ValidationResult> ValidationResults { get; private set; } = [];
private async Task PublishClicked(MouseEventArgs args)
{
var validationContext = new ValidationContext(Model);
var validationResult = new List<ValidationResult>();
IsValid = Validator.TryValidateObject(Model, validationContext, validationResult);
ArtifactGroupingValidationModel oldModel = Model;
if (ForwardLink is not null)
{
if (IsValid)
{
NavigationManager.NavigateTo(ForwardLink);
}
}
if (IsValid && ClearOnPublish)
{
Model = new();
await _uploadComponent.ClearClicked.InvokeAsync();
StateHasChanged();
}
await GroupingPublished.InvokeAsync(oldModel);
}
private void CancelClicked(MouseEventArgs args)
{
@@ -103,74 +199,71 @@
}
}
private void HandleEntryUpdate(ArtifactEntryValidationModel originalEntry, ArtifactEntryValidationModel updatedEntry)
{
// Find the index of the original object in our list
var index = Model.ArtifactEntries.IndexOf(originalEntry);
// If found, replace it with the updated version
if (index != -1)
{
Model.ArtifactEntries[index] = updatedEntry;
}
// Now, run the validation logic
OnChanged();
}
// You can now simplify your OnFilesUploaded method slightly
private async Task OnFilesUploaded(List<FilePathListing> args)
{
_filePathListings = args;
StateHasChanged();
// This part is tricky. Adding items while iterating can be problematic.
// A better approach is to create the entries first, then tell Blazor to render.
var newEntries = new List<ArtifactEntryValidationModel>();
foreach (var file in args)
{
// Associate the file with the entry if needed
newEntries.Add(new ArtifactEntryValidationModel { Files = [file]});
}
Model.ArtifactEntries.AddRange(newEntries);
// StateHasChanged() is implicitly called by OnChanged() if validation passes
await OnChanged();
}
private async Task OnClearFilesClicked()
{
_filePathListings = [];
Model.ArtifactEntries = [];
StateHasChanged();
await OnChanged();
}
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.");
}
// TODO: assign parents to all file path listings
throw new NotImplementedException();
*/
}
async Task OnChanged()
{
var validationContext = new ValidationContext(Model);
var validationResult = new List<ValidationResult>();
IsValid = Validator.TryValidateObject(Model, validationContext, validationResult);
if (IsValid)
{
StateHasChanged();
}
}
async Task OnCategoryChanged()
{
List<ArchiveCategory>? newCategory = await CategoryProvider.GetArchiveCategory(Model.Category);
if (newCategory.Count != 1)
if (Model.Category is not null)
{
throw new ArgumentException(nameof(Model.Category), $"Got {newCategory.Count} rows for category name={Model.Category}");
}
if (newCategory is not null)
{
_identifierTextBox.VerifyFormatCategory = newCategory[0];
_identifierTextBox.VerifyFormatCategory = Model.Category;
_isFormDivVisible = true;
StateHasChanged();
}
if (!_categorySelected)
{
_categorySelected = true;
if (!_categorySelected)
{
_categorySelected = true;
}
StateHasChanged();
}
@@ -191,8 +284,8 @@
await OnChanged();
}
}
private async Task<IEnumerable<string>> SearchCategory(string value, CancellationToken cancellationToken)
private async Task<IEnumerable<ArchiveCategory>> SearchCategory(string value, CancellationToken cancellationToken)
{
List<ArchiveCategory> categories;
if (string.IsNullOrEmpty(value))
@@ -204,13 +297,12 @@
categories = new((await CategoryProvider.Search(value) ?? []));
}
List<string> categoryStrings = [];
foreach (var category in categories)
{
categoryStrings.Add(category.Name);
}
return categoryStrings;
return categories;
}
private async void OnDeleteEntryClicked(int index)
{
Model.ArtifactEntries.RemoveAt(index);
StateHasChanged();
}
}

View File

@@ -0,0 +1,32 @@
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">Create a Category</MudText>
</TitleContent>
<DialogContent>
<AddArchiveGroupingComponent Model="Model"></AddArchiveGroupingComponent>
</DialogContent>
<DialogActions>
<MudButton OnClick="OnCancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="OnSubmit">OK</MudButton>
</DialogActions>
</MudDialog>
@code {
[Parameter]
public required ArtifactGroupingValidationModel Model { get; set; }
[Parameter]
public bool IsUpdate { get; set; } = false;
private void OnCancel(MouseEventArgs args)
{
throw new NotImplementedException();
}
private void OnSubmit(MouseEventArgs args)
{
throw new NotImplementedException();
}
}

View File

@@ -1,134 +1,176 @@
@using OpenArchival.Blazor.Components.CustomComponents;
@using System.ComponentModel.DataAnnotations
@using MudBlazor
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<h3>@FilePath.OriginalName</h3>
<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=OnInputsChanged></MudTextField>
<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=OnInputsChanged></MudTextField>
<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=OnInputsChanged SearchFunc="SearchStorageLocation" CoerceValue=true></MudAutocomplete>
<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=OnInputsChanged SearchFunc="SearchItemTypes" CoerceValue=true></MudAutocomplete>
@* Tags entry *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Tags</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="string" @ref="@_tagsChipContainer" @bind-Items="Model.Tags">
<InputContent>
<MudAutocomplete
T="string"
OnInternalInputChanged="OnInputsChanged"
SearchFunc="SearchTags"
Value="_tagsInputValue"
ValueChanged="OnTagsInputTextChanged"
OnKeyDown="@(ev => HandleChipContainerEnter<string>(ev, _tagsChipContainer, _tagsInputValue, () => _tagsInputValue = string.Empty))"
CoerceValue=true
Placeholder="Add Tags..."
ShowProgressIndicator="true">
</MudAutocomplete>
</InputContent>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Listed Names</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pb-2">Enter any names of the people associated with this entry.</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="string" @ref=_listedNamesChipContainer>
<InputContent>
<MudAutocomplete
T="string"
SearchFunc="SearchListedNames"
OnInternalInputChanged="OnInputsChanged"
Value="_listedNamesInputValue"
ValueChanged="OnListedNamesTextChanged"
OnKeyDown="@(ev=>HandleChipContainerEnter<string>(ev, _listedNamesChipContainer, _listedNamesInputValue, () => _listedNamesInputValue = string.Empty))"
CoerceValue=true
Placeholder="Add Listed Names..."
ShowProgressIndicator=true>
</MudAutocomplete>
</InputContent>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Associated Dates</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="DateTime" @ref="_assocaitedDatesChipContainer" DisplayFunc="date=>date.ToShortDateString()">
<InputContent>
<MudDatePicker @bind-Date=_associatedDateInputValue>
</MudDatePicker>
</InputContent>
<SubmitButton>
<MudButton
Color="Color.Primary"
OnClick="HandleAssociatedDateChipContainerAdd">+</MudButton>
</SubmitButton>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Defects</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="string" @ref=_defectsChipContainer>
<InputContent>
<MudAutocomplete
T="string"
SearchFunc="SearchDefects"
OnInternalInputChanged="OnInputsChanged"
Value="_defectsInputValue"
ValueChanged="OnDefectsValueChanged"
OnKeyDown="@(ev=>HandleChipContainerEnter<string>(ev, _defectsChipContainer, _defectsInputValue, () => _defectsInputValue = string.Empty))"
CoerceValue=true
Placeholder="Add Defects..."
ShowProgressIndicator=true>
</MudAutocomplete>
</InputContent>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Related Artifacts</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pb-2">Tag this entry with the identifier of any other entry to link them.</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="ArtifactGrouping" @ref="_assocaitedArtifactsChipContainer" DisplayFunc="artifact => artifact.ArtifactGroupingIdentifier">
<InputContent>
<MudAutocomplete
T="ArtifactGrouping"
OnInternalInputChanged="OnInputsChanged"
Value="_associatedArtifactValue"
ValueChanged="OnAssociatedArtifactChanged"
OnKeyDown="@(EventArgs=>HandleChipContainerEnter<ArtifactGrouping>(EventArgs, _assocaitedArtifactsChipContainer, _associatedArtifactValue, () => _associatedArtifactValue = null))"
CoerceValue="false"
Placeholder="Link artifact groupings..."
ShowProgressIndicator=true>
</MudAutocomplete>
</InputContent>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Artifact Text Contents</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pb-2">Input the text transcription of the words on the artifact if applicable to aid the search engine.</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudTextField T="string" Value=_artifactTextContent ValueChanged="OnArtifactTextContentChanged"></MudTextField>
</MudPaper>
@inject ArtifactEntrySharedHelpers Helpers;
@inject IArtifactDefectProvider DefectsProvider;
@inject IArtifactStorageLocationProvider StorageLocationProvider;
@inject IArchiveEntryTagProvider TagsProvider;
@inject IArtifactTypeProvider TypesProvider;
@inject IListedNameProvider ListedNameProvider;
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<MudGrid>
<MudItem>
@if (Model.Files.Count > 0) {
<MudText Typo="Typo.h6">@(Model.Files[0].OriginalName)</MudText>
}
</MudItem>
<MudItem>
<MudButton
Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Delete"
Color="Color.Error"
OnClick="OnDeleteEntryClicked"
>Delete</MudButton>
</MudItem>
</MudGrid>
@foreach (var error in ValidationResults)
{
<MudAlert Severity="Severity.Error">
@error.ErrorMessage
</MudAlert>
}
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Archive Item Title</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudTextField For="@(() => Model.Title)" Required=true Placeholder="Archive Item Title" T="string" Class="pl-4 pr-4" @bind-Value=Model.Title @bind-Value:after=OnInputsChanged></MudTextField>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Archive Item Numbering</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pb-2">Enter a unique ID for this entry</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudTextField For="@(() => Model.ArtifactNumber)" Placeholder="Numbering" T="string" @bind-Value=Model.ArtifactNumber @bind-Value:after=OnInputsChanged/>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Item Description</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudTextField For="@(() => Model.Description)" Lines=8 Placeholder="Description" T="string" Class="pl-4 pr-4" @bind-Value=Model.Description @bind-Value:after=OnInputsChanged></MudTextField>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Storage Location</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudAutocomplete For="@(() => Model.StorageLocation)" T="string" Label="Storage Location" Class="pt-0 mt-0 pl-2 pr-2" @bind-Value=Model.StorageLocation @bind-Value:after=OnInputsChanged SearchFunc="Helpers.SearchStorageLocation" CoerceValue=true></MudAutocomplete>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Artifact Type</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudAutocomplete For="@(() => Model.Type)" T="string" Label="Artifact Type" Class="pt-0 mt-0 pl-2 pr-2" @bind-Value=Model.Type @bind-Value:after=OnInputsChanged SearchFunc="Helpers.SearchItemTypes" CoerceValue=true></MudAutocomplete>
@* Tags entry *@
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Tags</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="string" @ref="@_tagsChipContainer" @bind-Items="Model.Tags">
<InputContent>
<MudAutocomplete T="string"
OnInternalInputChanged="OnInputsChanged"
SearchFunc="Helpers.SearchTags"
Value="_tagsInputValue"
ValueChanged="OnTagsInputTextChanged"
OnKeyDown="@(ev => HandleChipContainerEnter<string>(ev, _tagsChipContainer, _tagsInputValue, () => _tagsInputValue = string.Empty))"
CoerceValue=true
Placeholder="Add Tags..."
ShowProgressIndicator="true"
>
</MudAutocomplete>
</InputContent>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Listed Names</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pb-2">Enter any names of the people associated with this entry.</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="string" @ref=_listedNamesChipContainer @bind-Items="Model.ListedNames">
<InputContent>
<MudAutocomplete T="string"
SearchFunc="Helpers.SearchListedNames"
OnInternalInputChanged="OnInputsChanged"
Value="_listedNamesInputValue"
ValueChanged="OnListedNamesTextChanged"
OnKeyDown="@(ev=>HandleChipContainerEnter<string>(ev, _listedNamesChipContainer, _listedNamesInputValue, () => _listedNamesInputValue = string.Empty))"
CoerceValue=true
Placeholder="Add Listed Names..."
ShowProgressIndicator=true>
</MudAutocomplete>
</InputContent>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Associated Dates</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="DateTime" @ref="_assocaitedDatesChipContainer" @bind-Items="Model.AssociatedDates" DisplayFunc="date => date.ToShortDateString()">
<InputContent>
<MudDatePicker @bind-Date=_associatedDateInputValue>
</MudDatePicker>
</InputContent>
<SubmitButton>
<MudButton Color="Color.Primary"
OnClick="HandleAssociatedDateChipContainerAdd">+</MudButton>
</SubmitButton>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Defects</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="string" @ref=_defectsChipContainer @bind-Items="Model.Defects">
<InputContent>
<MudAutocomplete T="string"
SearchFunc="Helpers.SearchDefects"
OnInternalInputChanged="OnInputsChanged"
Value="_defectsInputValue"
ValueChanged="OnDefectsValueChanged"
OnKeyDown="@(ev=>HandleChipContainerEnter<string>(ev, _defectsChipContainer, _defectsInputValue, () => _defectsInputValue = string.Empty))"
CoerceValue=true
Placeholder="Add Defects..."
ShowProgressIndicator=true>
</MudAutocomplete>
</InputContent>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Related Artifacts</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pb-2">Tag this entry with the identifier of any other entry to link them.</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="ArtifactEntry" @ref="_assocaitedArtifactsChipContainer" @bind-Items="Model.RelatedArtifacts" DisplayFunc="artifact => artifact.ArtifactIdentifier">
<InputContent>
<MudAutocomplete T="ArtifactEntry"
OnInternalInputChanged="OnInputsChanged"
Value="_associatedArtifactValue"
ValueChanged="OnAssociatedArtifactChanged"
OnKeyDown="@(EventArgs=>HandleChipContainerEnter<ArtifactEntry>(EventArgs, _assocaitedArtifactsChipContainer, _associatedArtifactValue, () => _associatedArtifactValue = null))"
CoerceValue="false"
Placeholder="Link artifact groupings..."
ShowProgressIndicator=true>
</MudAutocomplete>
</InputContent>
</ChipContainer>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="pt-4 pb-0">Artifact Text Contents</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pb-2">Input the text transcription of the words on the artifact if applicable to aid the search engine.</MudText>
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<MudTextField T="string"
Value=_artifactTextContent
ValueChanged="OnArtifactTextContentChanged"
Lines="5"
For="@(() => Model.FileTextContent)"></MudTextField>
<MudText Typo=Typo.h6 Color="Color.Primary">Additional files</MudText>
<UploadDropBox FilesUploaded="OnFilesUploaded"></UploadDropBox>
</MudPaper>
@code {
[Parameter]
public required FilePathListing FilePath { get; set; }
public required FilePathListing MainFilePath { get; set; }
[Parameter]
public EventCallback OnValueChanged { get; set; }
public EventCallback<ArtifactEntryValidationModel> ModelChanged { get; set; }
[Parameter]
public required ArtifactEntryValidationModel Model { get; set; } = new(){StorageLocation="hello", Title="Hello"};
public EventCallback InputsChanged { get; set; }
[Parameter]
public required ArtifactEntryValidationModel Model { get; set; } = new() { StorageLocation = "hello", Title = "Hello" };
[Parameter]
public required int ArtifactEntryIndex {get; set;}
[Parameter]
public EventCallback<int> OnEntryDeletedClicked { get; set; }
private ChipContainer<string> _tagsChipContainer;
@@ -146,49 +188,86 @@
private string _defectsInputValue = "";
private ChipContainer<ArtifactGrouping> _assocaitedArtifactsChipContainer;
private ChipContainer<ArtifactEntry> _assocaitedArtifactsChipContainer;
private ArtifactGrouping? _associatedArtifactValue = null;
private ArtifactEntry? _associatedArtifactValue = null;
private string _artifactTextContent = "";
private string _artifactTextContent = "";
public Task OnInputsChanged()
{
return OnValueChanged.InvokeAsync();
public bool IsValid { get; set; }
public List<ValidationResult> ValidationResults { get; private set; } = [];
public async Task OnInputsChanged()
{
// 1. Clear previous validation errors
ValidationResults.Clear();
var validationContext = new ValidationContext(Model);
// 2. Run the validator
IsValid = Validator.TryValidateObject(Model, validationContext, ValidationResults, validateAllProperties: true);
// 3. REMOVE this line. Let the parent's update trigger the re-render.
// StateHasChanged();
await InputsChanged.InvokeAsync();
}
private async Task OnFilesUploaded(List<FilePathListing> filePathListings)
{
if (MainFilePath is not null)
{
Model.Files = [MainFilePath];
} else
{
Model.Files = [];
}
Model.Files.AddRange(filePathListings);
}
private async Task OnFilesCleared()
{
if (MainFilePath is not null) {
Model.Files = [MainFilePath];
} else
{
Model.Files = [];
}
}
private Task OnDefectsValueChanged(string text)
{
_defectsInputValue = text;
return OnValueChanged.InvokeAsync();
return ModelChanged.InvokeAsync(Model);
}
private Task OnTagsInputTextChanged(string text)
{
_tagsInputValue = text;
return OnValueChanged.InvokeAsync();
return ModelChanged.InvokeAsync(Model);
}
private Task OnListedNamesTextChanged(string text)
{
_listedNamesInputValue = text;
return OnValueChanged.InvokeAsync();
_listedNamesInputValue = text;
return ModelChanged.InvokeAsync(Model);
}
private Task OnAssociatedArtifactChanged(ArtifactGrouping grouping)
private Task OnAssociatedArtifactChanged(ArtifactEntry grouping)
{
if (grouping is not null)
{
_associatedArtifactValue = grouping;
return OnValueChanged.InvokeAsync();
}
return OnValueChanged.InvokeAsync();
return ModelChanged.InvokeAsync(Model);
}
return ModelChanged.InvokeAsync(Model);
}
private Task OnArtifactTextContentChanged(string value)
{
return OnValueChanged.InvokeAsync();
Model.FileTextContent = value;
return ModelChanged.InvokeAsync(Model);
}
public async Task HandleChipContainerEnter<Type>(KeyboardEventArgs args, ChipContainer<Type> container, Type value, Action resetInputAction)
@@ -198,92 +277,25 @@
await container.AddItem(value);
resetInputAction?.Invoke();
StateHasChanged();
await OnValueChanged.InvokeAsync();
await ModelChanged.InvokeAsync(Model);
}
}
public async Task HandleAssociatedDateChipContainerAdd(MouseEventArgs args)
public async Task HandleAssociatedDateChipContainerAdd(MouseEventArgs args)
{
if (_associatedDateInputValue is not null)
{
await _assocaitedDatesChipContainer.AddItem((DateTime)_associatedDateInputValue);
DateTime unspecifiedDate = (DateTime)_associatedDateInputValue;
DateTime utcDate = DateTime.SpecifyKind(unspecifiedDate, DateTimeKind.Utc);
await _assocaitedDatesChipContainer.AddItem(utcDate);
_associatedDateInputValue = default;
}
}
private async Task<IEnumerable<string>> SearchDefects(string value, CancellationToken cancellationToken)
private async Task OnDeleteEntryClicked(MouseEventArgs args)
{
List<string> defects;
if (string.IsNullOrEmpty(value))
{
defects = new((await DefectsProvider.Top(25) ?? []).Select(prop => prop.Description));
}
else
{
defects = new((await DefectsProvider.Search(value) ?? []).Select(prop => prop.Description));
}
return defects;
}
private async Task<IEnumerable<string>> SearchStorageLocation(string value, CancellationToken cancellationToken)
{
List<string> storageLocations;
if (string.IsNullOrEmpty(value))
{
storageLocations = new((await StorageLocationProvider.Top(25) ?? []).Select(prop => prop.Location));
}
else
{
storageLocations = new((await StorageLocationProvider.Search(value) ?? []).Select(prop => prop.Location));
}
return storageLocations;
}
private async Task<IEnumerable<string>> SearchTags(string value, CancellationToken cancellationToken)
{
List<string> tags;
if (string.IsNullOrEmpty(value))
{
tags = new((await TagsProvider.Top(25) ?? []).Select(prop => prop.Name));
}
else
{
tags = new((await TagsProvider.Search(value) ?? []).Select(prop => prop.Name));
}
return tags;
}
private async Task<IEnumerable<string>> SearchItemTypes(string value, CancellationToken cancellationToken)
{
List<string> itemTypes;
if (string.IsNullOrEmpty(value))
{
itemTypes = new((await TypesProvider.Top(25) ?? []).Select(prop => prop.Name));
}
else
{
itemTypes = new((await TypesProvider.Search(value) ?? []).Select(prop => prop.Name));
}
return itemTypes;
}
private async Task<IEnumerable<string>> SearchListedNames(string value, CancellationToken cancellationToken)
{
List<string> names;
if (string.IsNullOrEmpty(value))
{
names = new((await ListedNameProvider.Top(25) ?? []).Select(prop=>$"{prop.FirstName} {prop.LastName}"));
}
else
{
names = new((await ListedNameProvider.Search(value) ?? []).Select(prop=>$"{prop.FirstName} {prop.LastName}"));
}
return names;
}
}
await OnEntryDeletedClicked.InvokeAsync(ArtifactEntryIndex);
}
}

View File

@@ -0,0 +1,125 @@
<MudDataGrid
T="ArtifactGroupingRowElement"
MultiSelection=true
Items="ArtifactGroupingRows"
Filterable=false
SelectOnRowClick=true
@ref=@DataGrid>
<ToolBarContent>
<MudButton
Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Delete"
Color="Color.Error"
OnClick="OnDeleteClicked">Delete</MudButton>
</ToolBarContent>
<Columns>
<SelectColumn T="ArtifactGroupingRowElement"/>
<PropertyColumn
Title="Id"
Property="x=>x.ArtifactGroupingIdentifier"
Filterable="false"/>
<PropertyColumn
Title="Category"
Property="x=>x.CategoryName"
Filterable="false"/>
<PropertyColumn
Title="Title"
Property="x=>x.Title"
Filterable="false"/>
<PropertyColumn
Title="Publically Visible"
Property="x=>x.IsPublicallyVisible"
Filterable="false"/>
<TemplateColumn Title="Edit">
<CellTemplate>
<MudIconButton
Color="Color.Primary"
Icon="@Icons.Material.Filled.Edit"
Variant="Variant.Filled"
OnClick="() => OnRowEditClick(context.Item)">Edit</MudIconButton>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="ArtifactGroupingRowElement"/>
</PagerContent>
</MudDataGrid>
@inject IArtifactGroupingProvider GroupingProvider;
@inject IDialogService DialogService;
@inject IArtifactGroupingProvider GroupingProvider;
@code
{
public List<ArtifactGroupingRowElement> ArtifactGroupingRows { get; set; } = new();
public MudDataGrid<ArtifactGroupingRowElement> DataGrid { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
// Load inital data
List<ArtifactGrouping> groupings = await GroupingProvider.GetGroupingsPaged(1, 25);
SetGroupingRows(groupings);
await base.OnInitializedAsync();
StateHasChanged();
}
private void SetGroupingRows(IEnumerable<ArtifactGrouping> groupings)
{
ArtifactGroupingRows.Clear();
foreach (var grouping in groupings)
{
ArtifactGroupingRows.Add(new ArtifactGroupingRowElement()
{
ArtifactGroupingIdentifier=grouping.ArtifactGroupingIdentifier ?? throw new ArgumentNullException(nameof(grouping), "Got a null grouping identifier"),
CategoryName=grouping.Category.Name,
Title=grouping.Title,
IsPublicallyVisible=grouping.IsPublicallyVisible,
Id=grouping.Id
}
);
}
}
private void OnRowEditClick(ArtifactGroupingRowElement row)
{
throw new NotImplementedException();
}
private async Task OnDeleteClicked(MouseEventArgs args)
{
HashSet<ArtifactGroupingRowElement> selected = DataGrid.SelectedItems;
bool? confirmed = await DialogService.ShowMessageBox
(
new MessageBoxOptions(){
Message=$"Are you sure you want to delete {selected.Count} groupings?",
Title="Delete Groupings",
CancelText="Cancel",
YesText="Delete"
});
if (confirmed is not null && (confirmed ?? throw new ArgumentNullException("confirmed was null")))
{
foreach (var grouping in selected)
{
await GroupingProvider.DeleteGroupingAsync(grouping.Id);
StateHasChanged();
}
}
}
}

View File

@@ -0,0 +1,316 @@
using Microsoft.EntityFrameworkCore;
using OpenArchival.DataAccess;
namespace OpenArchival.Blazor;
public class ArtifactEntrySharedHelpers
{
IArtifactDefectProvider DefectsProvider { get; set; }
IArtifactStorageLocationProvider StorageLocationProvider { get; set; }
IArchiveEntryTagProvider TagsProvider { get; set; }
IArtifactTypeProvider TypesProvider { get; set; }
IListedNameProvider ListedNameProvider { get; set; }
IDbContextFactory<ApplicationDbContext> DbContextFactory { get; set; }
public ArtifactEntrySharedHelpers(IArtifactDefectProvider defectsProvider, IArtifactStorageLocationProvider storageLocationProvider, IArchiveEntryTagProvider tagsProvider, IArtifactTypeProvider typesProvider, IListedNameProvider listedNamesProvider, IDbContextFactory<ApplicationDbContext> contextFactory)
{
DefectsProvider = defectsProvider;
StorageLocationProvider = storageLocationProvider;
TagsProvider = tagsProvider;
TypesProvider = typesProvider;
ListedNameProvider = listedNamesProvider;
DbContextFactory = contextFactory;
}
public async Task<IEnumerable<string>> SearchDefects(string value, CancellationToken cancellationToken)
{
List<string> defects;
if (string.IsNullOrEmpty(value))
{
defects = new((await DefectsProvider.Top(25) ?? []).Select(prop => prop.Description));
}
else
{
defects = new((await DefectsProvider.Search(value) ?? []).Select(prop => prop.Description));
}
return defects;
}
public async Task<IEnumerable<string>> SearchStorageLocation(string value, CancellationToken cancellationToken)
{
List<string> storageLocations;
if (string.IsNullOrEmpty(value))
{
storageLocations = new((await StorageLocationProvider.Top(25) ?? []).Select(prop => prop.Location));
}
else
{
storageLocations = new((await StorageLocationProvider.Search(value) ?? []).Select(prop => prop.Location));
}
return storageLocations;
}
public async Task<IEnumerable<string>> SearchTags(string value, CancellationToken cancellationToken)
{
List<string> tags;
if (string.IsNullOrEmpty(value))
{
tags = new((await TagsProvider.Top(25) ?? []).Select(prop => prop.Name));
}
else
{
tags = new((await TagsProvider.Search(value) ?? []).Select(prop => prop.Name));
}
return tags;
}
public async Task<IEnumerable<string>> SearchItemTypes(string value, CancellationToken cancellationToken)
{
List<string> itemTypes;
if (string.IsNullOrEmpty(value))
{
itemTypes = new((await TypesProvider.Top(25) ?? []).Select(prop => prop.Name));
}
else
{
itemTypes = new((await TypesProvider.Search(value) ?? []).Select(prop => prop.Name));
}
return itemTypes;
}
public async Task<IEnumerable<string>> SearchListedNames(string value, CancellationToken cancellationToken)
{
List<ListedName> names;
if (string.IsNullOrEmpty(value))
{
names = new((await ListedNameProvider.Top(25) ?? []));
}
else
{
names = new((await ListedNameProvider.Search(value) ?? []));
}
return names.Select(p => p.Value);
}
public async Task OnGroupingPublished(ArtifactGroupingValidationModel model)
{
await using var context = await DbContextFactory.CreateDbContextAsync();
var grouping = model.ToArtifactGrouping();
// Caches to track entities processed within this transaction
var processedFilePaths = new Dictionary<string, FilePathListing>();
var processedLocations = new Dictionary<string, ArtifactStorageLocation>();
var processedTags = new Dictionary<string, ArtifactEntryTag>();
var processedDefects = new Dictionary<string, ArtifactDefect>();
var processedTypes = new Dictionary<string, ArtifactType>();
var processedNames = new Dictionary<string, ListedName>();
// Process File Paths for each entry first
foreach (var entry in grouping.ChildArtifactEntries)
{
if (entry.Files is { Count: > 0 })
{
var correctedFileList = new List<FilePathListing>();
foreach (var fileListing in entry.Files)
{
var path = fileListing.Path;
if (string.IsNullOrWhiteSpace(path)) continue;
if (processedFilePaths.TryGetValue(path, out var trackedFile))
{
correctedFileList.Add(trackedFile);
}
else
{
var existingFile = await context.ArtifactFilePaths
.FirstOrDefaultAsync(f => f.Path == path);
if (existingFile != null)
{
correctedFileList.Add(existingFile);
processedFilePaths[path] = existingFile;
}
else
{
correctedFileList.Add(fileListing);
processedFilePaths[path] = fileListing;
}
}
}
entry.Files = correctedFileList;
}
}
// Process all other related entities for each entry
foreach (var entry in grouping.ChildArtifactEntries)
{
// Attach entry to its parent grouping
entry.ArtifactGrouping = grouping;
// --- Process Storage Location ---
var locationName = entry.StorageLocation?.Location;
if (!string.IsNullOrWhiteSpace(locationName))
{
if (processedLocations.TryGetValue(locationName, out var trackedLocation))
{
entry.StorageLocation = trackedLocation;
}
else
{
var existingLocation = await context.ArtifactStorageLocations
.FirstOrDefaultAsync(l => l.Location == locationName);
if (existingLocation != null)
{
entry.StorageLocation = existingLocation;
processedLocations[locationName] = existingLocation;
}
else
{
processedLocations[locationName] = entry.StorageLocation;
}
}
}
// --- Process Tags ---
if (entry.Tags is { Count: > 0 })
{
var correctedTagList = new List<ArtifactEntryTag>();
foreach (var tag in entry.Tags)
{
var tagName = tag.Name;
if (string.IsNullOrWhiteSpace(tagName)) continue;
if (processedTags.TryGetValue(tagName, out var trackedTag))
{
correctedTagList.Add(trackedTag);
}
else
{
var existingTag = await context.ArtifactEntryTags.FirstOrDefaultAsync(t => t.Name == tagName);
if (existingTag != null)
{
correctedTagList.Add(existingTag);
processedTags[tagName] = existingTag;
}
else
{
correctedTagList.Add(tag);
processedTags[tagName] = tag;
}
}
}
entry.Tags = correctedTagList;
}
// --- Process Defects ---
if (entry.Defects is { Count: > 0 })
{
var correctedDefectList = new List<ArtifactDefect>();
foreach (var defect in entry.Defects)
{
var defectDesc = defect.Description;
if (string.IsNullOrWhiteSpace(defectDesc)) continue;
if (processedDefects.TryGetValue(defectDesc, out var trackedDefect))
{
correctedDefectList.Add(trackedDefect);
}
else
{
var existingDefect = await context.ArtifactDefects.FirstOrDefaultAsync(d => d.Description == defectDesc);
if (existingDefect != null)
{
correctedDefectList.Add(existingDefect);
processedDefects[defectDesc] = existingDefect;
}
else
{
correctedDefectList.Add(defect);
processedDefects[defectDesc] = defect;
}
}
}
entry.Defects = correctedDefectList;
}
// --- Process Types ---
if (entry.Type is not null)
{
var typeName = entry.Type.Name;
if (!string.IsNullOrWhiteSpace(typeName))
{
if (processedTypes.TryGetValue(typeName, out var trackedType))
{
entry.Type = trackedType;
}
else
{
var existingType = await context.ArtifactTypes.FirstOrDefaultAsync(t => t.Name == typeName);
if (existingType != null)
{
entry.Type = existingType;
processedTypes[typeName] = existingType;
}
else
{
processedTypes[typeName] = entry.Type;
}
}
}
}
// --- Process Listed Names ---
if (entry.ListedNames is { Count: > 0 })
{
var correctedNameList = new List<ListedName>();
foreach (var name in entry.ListedNames)
{
var nameValue = name.Value;
if (string.IsNullOrWhiteSpace(nameValue)) continue;
if (processedNames.TryGetValue(nameValue, out var trackedName))
{
correctedNameList.Add(trackedName);
}
else
{
var existingName = await context.ArtifactAssociatedNames
.FirstOrDefaultAsync(n => n.Value == nameValue);
if (existingName != null)
{
correctedNameList.Add(existingName);
processedNames[nameValue] = existingName;
}
else
{
correctedNameList.Add(name);
processedNames[nameValue] = name;
}
}
}
entry.ListedNames = correctedNameList;
}
}
if (grouping.Category != null && grouping.Category.Id > 0)
{
context.Attach(grouping.Category);
}
// Add the entire graph. EF Core will correctly handle new vs. existing entities.
context.ArtifactGroupings.Add(grouping);
await context.SaveChangesAsync();
}
}

View File

@@ -1,37 +0,0 @@
using System.ComponentModel.DataAnnotations;
using OpenArchival.DataAccess;
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;
}

View File

@@ -4,19 +4,19 @@ using Microsoft.IdentityModel.Tokens;
using OpenArchival.DataAccess;
using System.ComponentModel.DataAnnotations;
public class ArtifactEntryValidationModel : IValidatableObject
public class ArtifactEntryValidationModel
{
[Required(AllowEmptyStrings = false, ErrorMessage = "An artifact numbering must be supplied")]
public string? ArtifactNumber { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessage = "A title must be provided")]
public required string Title { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public string? Type { get; set; }
public required string? StorageLocation { get; set; }
public string? StorageLocation { get; set; }
public List<string>? Tags { get; set; } = [];
@@ -28,22 +28,70 @@ public class ArtifactEntryValidationModel : IValidatableObject
public List<string>? Links { get; set; } = [];
public string? ArtifactType { get; set; }
public List<FilePathListing>? Files { get; set; } = [];
public Dictionary<string, string>? FileTextContent { get; set; } = [];
public string? FileTextContent { get; set; }
public List<ArtifactEntry> RelatedArtifacts { get; set; } = [];
/*
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (Links.IsNullOrEmpty() && Files.IsNullOrEmpty())
{
yield return new ValidationResult(
"Either uploaded files or add content links",
new[] {nameof(Links), nameof(Files)}
);
}
}
*/
public bool IsPublicallyVisible { get; set; }
public ArtifactEntry ToArtifactEntry(ArtifactGrouping? parent = null)
{
List<ArtifactEntryTag> tags = new();
if (Tags is not null)
{
foreach (var tag in Tags)
{
tags.Add(new ArtifactEntryTag() { Name = tag });
}
}
List<ArtifactDefect> defects = new();
foreach (var defect in Defects)
{
defects.Add(new ArtifactDefect() { Description=defect});
}
var entry = new ArtifactEntry()
{
Files = Files,
Type = new DataAccess.ArtifactType() { Name = Type },
ArtifactNumber = ArtifactNumber,
AssociatedDates = AssociatedDates,
Defects = defects,
Links = Links,
StorageLocation = null,
Description = Description,
FileTextContent = FileTextContent,
IsPubliclyVisible = IsPublicallyVisible,
Tags = tags,
Title = Title,
ArtifactGrouping = parent,
RelatedTo = RelatedArtifacts,
};
List<ListedName> listedNames = new();
foreach (var name in ListedNames)
{
listedNames.Add(new ListedName() { ParentArtifactEntry=entry, Value=name});
}
entry.ListedNames = listedNames;
if (!string.IsNullOrEmpty(StorageLocation))
{
entry.StorageLocation = new ArtifactStorageLocation() { Location = StorageLocation };
}
return entry;
}
}

View File

@@ -1,14 +1,76 @@
using OpenArchival.DataAccess;
using Microsoft.IdentityModel.Tokens;
using OpenArchival.DataAccess;
using System.ComponentModel.DataAnnotations;
namespace OpenArchival.Blazor;
public class ArtifactGroupingValidationModel
public class ArtifactGroupingValidationModel : IValidatableObject
{
public required ArchiveCategory Category { get; set; }
[Required(ErrorMessage = "A grouping title is required.")]
public string? Title { get; set; }
public List<string>? IdentifierFieldValues { get; set; }
[Required(ErrorMessage = "A grouping description is required.")]
public string? Description { get; set; }
public List<ArtifactEntryValidationModel>? ArtifactEntries { get; set; }
[Required(ErrorMessage = "A type is required.")]
public string? Type { get; set; }
public ArchiveCategory? Category { get; set; }
public List<IdentifierFieldValidationModel> IdentifierFieldValues { get; set; } = new();
public List<ArtifactEntryValidationModel> ArtifactEntries { get; set; } = new();
public bool IsPublicallyVisible { get; set; }
public ArtifactGrouping ToArtifactGrouping()
{
IdentifierFields identifierFields = new();
identifierFields.Values = IdentifierFieldValues.Select(p => p.Value).ToList();
List<ArtifactEntry> entries = [];
foreach (var entry in ArtifactEntries)
{
entries.Add(entry.ToArtifactEntry());
}
var grouping = new ArtifactGrouping()
{
Title = Title,
Description = Description,
Category = Category,
IdentifierFields = identifierFields,
IsPublicallyVisible = true,
ChildArtifactEntries = entries,
Type = Type
};
// Create the parent link
foreach (var entry in grouping.ChildArtifactEntries)
{
entry.ArtifactGrouping = grouping;
}
return grouping;
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach (var entry in ArtifactEntries)
{
var context = new ValidationContext(entry);
var validationResult = new List<ValidationResult>();
bool valid = Validator.TryValidateObject(entry, context, validationResult);
foreach (var result in validationResult)
{
yield return result;
}
}
if (ArtifactEntries.IsNullOrEmpty())
{
yield return new ValidationResult("Must upload one or more files");
}
}
}