Got most of admin panel working. Data issues fixed
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user