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

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

View File

@@ -18,6 +18,8 @@
<script src="_framework/blazor.web.js"></script>
<script src=@Assets["_content/MudBlazor/MudBlazor.min.js"]></script>
<script src="_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script>
<script src="js/downloadHelper.js"></script>
<script src="js/imageSizeGetter.js"></script>
</body>
</html>

View File

@@ -5,9 +5,17 @@
@* Loop through and display each item as a chip *@
@foreach (var item in Items)
{
<MudChip Color="Color.Primary" OnClose="() => RemoveItem(item)" T="T">
@DisplayFunc(item)
</MudChip>
@if (DeleteEnabled)
{
<MudChip Color="Color.Primary" OnClose="() => RemoveItem(item)" T="T">
@DisplayFunc(item)
</MudChip>
} else
{
<MudChip Color="Color.Primary" T="T">
@DisplayFunc(item)
</MudChip>
}
}
@* Render the input control provided by the consumer *@
@@ -22,6 +30,9 @@
</div>
@code {
[Parameter]
public bool DeleteEnabled { get; set; } = true;
/// <summary>
/// The list of items to display and manage.
/// </summary>
@@ -51,8 +62,6 @@
[Parameter]
public Func<T, string> DisplayFunc { get; set; } = item => item?.ToString() ?? string.Empty;
/// <summary>
/// A public method that the consumer's input control can call to add a new item.
/// </summary>

View File

@@ -230,4 +230,19 @@
private void ClearDragClass()
=> _dragClass = DefaultDragClass;
public void RemoveFile(string filename)
{
var existingFile = ExistingFiles.Where(f => f.OriginalName == filename).FirstOrDefault();
if (existingFile is not null)
{
ExistingFiles.Remove(existingFile);
}
var file = Files.Where(f => f.Name == filename).FirstOrDefault();
if (file is not null)
{
Files.Remove(file);
}
}
}

View File

@@ -44,6 +44,7 @@
if (result is not null && !result.Canceled)
{
}
}
*/

View File

@@ -2,6 +2,7 @@
@using OpenArchival.Blazor.Components.Pages.Administration.Categories
@using OpenArchival.DataAccess;
@using System.ComponentModel.DataAnnotations
@using MudBlazor
@inject IDialogService DialogService
@inject NavigationManager NavigationManager;
@@ -193,6 +194,9 @@
{
// The data entry should only be shown if a category has been selected
_isFormDivVisible = true;
} else
{
_isFormDivVisible = false;
}
if (Model is not null)
@@ -223,11 +227,11 @@
if (IsValid && ClearOnPublish)
{
Model = new();
Model = new();
await _uploadComponent.ClearClicked.InvokeAsync();
StateHasChanged();
}
await GroupingPublished.InvokeAsync(oldModel);
}
@@ -343,10 +347,10 @@
return categories;
}
private async void OnDeleteEntryClicked(int index)
private async void OnDeleteEntryClicked((int index, string filename) data)
{
Model.ArtifactEntries.RemoveAt(index);
_uploadComponent.Files.RemoveAt(index);
Model.ArtifactEntries.RemoveAt(data.index);
_uploadComponent.RemoveFile(data.filename);
StateHasChanged();
}
}

View File

@@ -96,7 +96,7 @@
<MudDivider DividerType="DividerType.Middle"></MudDivider>
<ChipContainer T="DateTime" @ref="_assocaitedDatesChipContainer" @bind-Items="Model.AssociatedDates" DisplayFunc="date => date.ToShortDateString()">
<InputContent>
<MudDatePicker @bind-Date=_associatedDateInputValue>
<MudDatePicker @bind-Date=_associatedDateInputValue MinDate="new DateTime(1000, 1, 1)">
</MudDatePicker>
</InputContent>
<SubmitButton>
@@ -149,7 +149,7 @@
For="@(() => Model.FileTextContent)"></MudTextField>
<MudText Typo=Typo.h6 Color="Color.Primary">Additional files</MudText>
<UploadDropBox FilesUploaded="OnFilesUploaded"></UploadDropBox>
<UploadDropBox @ref=_uploadDropBox FilesUploaded="OnFilesUploaded"></UploadDropBox>
</MudPaper>
@@ -170,7 +170,7 @@
public required int ArtifactEntryIndex {get; set;}
[Parameter]
public EventCallback<int> OnEntryDeletedClicked { get; set; }
public EventCallback<(int index, string filename)> OnEntryDeletedClicked { get; set; }
private ChipContainer<string> _tagsChipContainer;
@@ -198,6 +198,23 @@
public List<ValidationResult> ValidationResults { get; private set; } = [];
public UploadDropBox _uploadDropBox = default!;
protected override Task OnParametersSetAsync()
{
if (_uploadDropBox is not null && Model is not null && Model.Files is not null)
{
_uploadDropBox.ExistingFiles = Model.Files.GetRange(1, Model.Files.Count - 1);
}
if (Model.Files is not null && Model.Files.Any())
{
MainFilePath = Model.Files[0];
}
return base.OnParametersSetAsync();
}
public async Task OnInputsChanged()
{
// 1. Clear previous validation errors
@@ -217,12 +234,17 @@
{
if (MainFilePath is not null)
{
var oldFiles = Model.Files.GetRange(1, Model.Files.Count - 1);
Model.Files = [MainFilePath];
Model.Files.AddRange(oldFiles);
Model.Files.AddRange(filePathListings);
} else
{
Model.Files = [];
}
Model.Files.AddRange(filePathListings);
StateHasChanged();
}
private async Task OnFilesCleared()
@@ -296,6 +318,6 @@
}
private async Task OnDeleteEntryClicked(MouseEventArgs args)
{
await OnEntryDeletedClicked.InvokeAsync(ArtifactEntryIndex);
await OnEntryDeletedClicked.InvokeAsync((ArtifactEntryIndex, MainFilePath.OriginalName));
}
}

View File

@@ -6,6 +6,11 @@ using System.ComponentModel.DataAnnotations;
public class ArtifactEntryValidationModel
{
/// <summary>
/// Used when translating between the validation model and the database model
/// </summary>
public int? Id { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessage = "An artifact numbering must be supplied")]
public string? ArtifactNumber { get; set; }
@@ -33,14 +38,8 @@ public class ArtifactEntryValidationModel
public string? FileTextContent { get; set; }
public List<ArtifactEntry> RelatedArtifacts { get; set; } = [];
/*
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
}
*/
public bool IsPublicallyVisible { get; set; }
public bool IsPublicallyVisible { get; set; } = true;
public ArtifactEntry ToArtifactEntry(ArtifactGrouping? parent = null)
{
@@ -59,10 +58,9 @@ public class ArtifactEntryValidationModel
defects.Add(new ArtifactDefect() { Description=defect});
}
var entry = new ArtifactEntry()
{
Id = Id ?? 0,
Files = Files,
Type = new DataAccess.ArtifactType() { Name = Type },
ArtifactNumber = ArtifactNumber,

View File

@@ -26,7 +26,7 @@ public class ArtifactGroupingValidationModel : IValidatableObject
public List<ArtifactEntryValidationModel> ArtifactEntries { get; set; } = new();
public bool IsPublicallyVisible { get; set; }
public bool IsPublicallyVisible { get; set; } = true;
public ArtifactGrouping ToArtifactGrouping()
{
@@ -75,6 +75,7 @@ public class ArtifactGroupingValidationModel : IValidatableObject
var validationModel = new ArtifactEntryValidationModel()
{
Id = entry.Id,
Title = entry.Title,
StorageLocation = entry.StorageLocation.Location,
ArtifactNumber = entry.ArtifactNumber,
@@ -131,7 +132,7 @@ public class ArtifactGroupingValidationModel : IValidatableObject
}
}
if (ArtifactEntries.IsNullOrEmpty())
if (ArtifactEntries is null || ArtifactEntries.Count == 0)
{
yield return new ValidationResult("Must upload one or more files");
}

View File

@@ -29,7 +29,7 @@ public class CategoryValidationModel
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (FieldNames.IsNullOrEmpty() || FieldDescriptions.IsNullOrEmpty())
if ((FieldNames is null || FieldNames.Count == 0) || (FieldDescriptions is null || FieldDescriptions.Count == 0))
{
yield return new ValidationResult(
"Either the FieldNames or FieldDescriptions were null or empty. At least one is required",

View File

@@ -1,5 +1,85 @@
<h3>ArchiveEntryDisplay</h3>
@using OpenArchival.Blazor.FileViewer
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
<MudGrid Spacing="2">
<MudItem lg="8" xs="12" Class="d-flex align-center justify-center flex-column">
<MudText Class="d-flex" Typo=Typo.h6>@ArtifactEntry.Title</MudText>
<FileViewerCarousel FilePathListings="ArtifactEntry.Files" MaxHeight="350" ShowUnsupportedFiles=true></FileViewerCarousel>
</MudItem>
<MudItem lg="4" xs="12">
<MudText Typo="Typo.h6">Artifact Identifier</MudText>
<MudDivider></MudDivider>
<MudText Typo="Typo.caption">@ArtifactEntry.ArtifactIdentifier</MudText>
<MudText Typo="Typo.h6">Primary Artifact Type</MudText>
<MudDivider></MudDivider>
<MudText Typo="Typo.caption">@ArtifactEntry.Type.Name</MudText>
@if (!string.IsNullOrEmpty(ArtifactEntry.StorageLocation.Location))
{
<MudText Typo="Typo.h6">Storage Location</MudText>
<MudDivider></MudDivider>
<MudText Typo="Typo.caption">@ArtifactEntry.StorageLocation.Location</MudText>
}
@if (ArtifactEntry.Tags.Count > 0)
{
<MudText Typo="Typo.h6">Tags</MudText>
<MudDivider></MudDivider>
<OpenArchival.Blazor.Components.CustomComponents.ChipContainer DeleteEnabled=false @bind-Items="ArtifactEntry.Tags"></OpenArchival.Blazor.Components.CustomComponents.ChipContainer>
}
@if (ArtifactEntry.ListedNames.Count > 0)
{
<MudText Typo="Typo.h6">Listed Names</MudText>
<MudDivider></MudDivider>
<OpenArchival.Blazor.Components.CustomComponents.ChipContainer DeleteEnabled=false @bind-Items="ArtifactEntry.ListedNames"></OpenArchival.Blazor.Components.CustomComponents.ChipContainer>
}
@if (ArtifactEntry.AssociatedDates.Count > 0)
{
<MudText Typo="Typo.h6">Associated Dates</MudText>
<MudDivider></MudDivider>
<OpenArchival.Blazor.Components.CustomComponents.ChipContainer DeleteEnabled=false @bind-Items="ArtifactEntry.AssociatedDates" DisplayFunc="@(date => date.ToString("d"))"></OpenArchival.Blazor.Components.CustomComponents.ChipContainer>
}
</MudItem>
</MudGrid>
<MudText Class="mt-4" Typo="Typo.h6">Description</MudText>
<MudDivider></MudDivider>
<MudText Typo="Typo.body1">@ArtifactEntry.Description</MudText>
<MudExpansionPanel Text="Downloads">
<MudList T="string">
@foreach (FilePathListing file in ArtifactEntry.Files)
{
<MudListItem Icon="@Icons.Material.Filled.Download" IconColor="Color.Primary" OnClick="() => OnFileDownloadClicked(file)">@file.OriginalName</MudListItem>
}
</MudList>
</MudExpansionPanel>
</MudPaper>
@inject IJSRuntime JSRuntime
@inject ISnackbar Snackbar
@code {
[Parameter]
public required ArtifactEntry ArtifactEntry { get; set; }
private async Task OnFileDownloadClicked(FilePathListing file)
{
try
{
byte[] fileBytes = await File.ReadAllBytesAsync(file.Path);
string mimeType = "";
await JSRuntime.InvokeVoidAsync("downloadFileFromBytes", file.OriginalName, mimeType, Convert.ToBase64String(fileBytes));
}
catch (Exception ex)
{
Snackbar.Add($"Failed to download file {file.OriginalName}", Severity.Error);
throw ex;
}
}
}

View File

@@ -1,12 +1,55 @@
@page "/archive/{GroupingIdString}"
@using OpenArchival.DataAccess;
@using OpenArchival.Blazor.FileViewer
@inject IArtifactGroupingProvider GroupingProvider;
@inject NavigationManager NavigationManager;
@if (_artifactGrouping is not null)
{
<MudText Typo="Typo.h1">@_artifactGrouping.Title</MudText>
<MudContainer MaxWidth="MaxWidth.Large">
<MudPaper Class="pa-4 ma-2 rounded d-flex justify-center" Elevation="3">
<MudText Typo="Typo.h3">@_artifactGrouping.Title</MudText>
</MudPaper>
<MudPaper Class="pa-4 ma-2 rounded d-flex justify-center" Elevation="3">
@*<MudImage Class="d-flex justify-center" Src="https://dummyimage.com/600x400/000/fff"></MudImage>*@
<FileViewerCarousel FilePathListings="_artifactGrouping.ChildFilePathListings" MaxHeight="500" ShowUnsupportedFiles=false></FileViewerCarousel>
</MudPaper>
<MudGrid Spacing="2">
<MudItem xs="8">
<MudPaper Style="height: 100%;" Class="pa-4 ma-2 rounded" Elevation="3">
<MudText Typo="Typo.h6">Description</MudText>
<MudDivider></MudDivider>
<MudText Typo="Typo.body2">@_artifactGrouping.Description</MudText>
</MudPaper>
</MudItem>
<MudItem xs="4">
<MudPaper Style="height: 100%;" Class="pa-4 ma-2 rounded" Elevation="3">
<MudText Typo="Typo.h6">Artifact Identifier</MudText>
<MudDivider></MudDivider>
<MudText Typo="Typo.caption">@_artifactGrouping.ArtifactGroupingIdentifier</MudText>
<MudText Typo="Typo.h6">Primary Artifact Category</MudText>
<MudDivider></MudDivider>
<MudText Typo="Typo.caption">@_artifactGrouping.Category.Name</MudText>
<MudText Typo="Typo.h6">Primary Artifact Type</MudText>
<MudDivider></MudDivider>
<MudText Typo="Typo.caption">@_artifactGrouping.Type.Name</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudPaper Class="pa-4 mt-8 ma-2 rounded" Elevation="3">
@foreach (ArtifactEntry child in _artifactGrouping.ChildArtifactEntries) {
<ArchiveEntryDisplay ArtifactEntry="child"></ArchiveEntryDisplay>
}
</MudPaper>
</MudContainer>
}
@code {

View File

@@ -1,5 +0,0 @@
<h3>FileDisplayBase</h3>
@code {
}

View File

@@ -0,0 +1,34 @@
@if (File is not null)
{
<MudImage Fluid="true" Src="@File.Path" Alt="Swedish Farm House" Class="rounded-lg"/>
}
@code {
[Parameter]
public required FilePathListing File { get; set; }
/// <summary>
/// Sets the width of the image box while it loads
/// </summary>
[Parameter]
public int Width { get; set; } = 600;
/// <summary>
/// Sets the height of the image box while it loads
/// </summary>
[Parameter]
public int Height { get; set; } = 400;
[Parameter]
public string AltText { get; set; } = "";
protected override Task OnParametersSetAsync()
{
if (File is not null || !string.IsNullOrEmpty(AltText))
{
StateHasChanged();
}
return base.OnParametersSetAsync();
}
}

View File

@@ -0,0 +1,46 @@
using OpenArchival.Blazor.Components.Pages.ArchiveDisplay;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace OpenArchival.Blazor;
public class FileDisplayComponentFactory
{
private Dictionary<string, Type> _extensionToComponents { get; set; } = [];
public FileDisplayComponentFactory()
{
// Supported image file types taken from https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types
RegisterComponent<FileDisplayImage>(".apng");
RegisterComponent<FileDisplayImage>(".png");
RegisterComponent<FileDisplayImage>(".avif");
RegisterComponent<FileDisplayImage>(".gif");
RegisterComponent<FileDisplayImage>(".jpg");
RegisterComponent<FileDisplayImage>(".jpeg");
RegisterComponent<FileDisplayImage>(".jfif");
RegisterComponent<FileDisplayImage>(".pjpeg");
RegisterComponent<FileDisplayImage>(".pjg");
RegisterComponent<FileDisplayImage>(".png");
RegisterComponent<FileDisplayImage>(".svg");
RegisterComponent<FileDisplayImage>(".webp");
}
private string GetExtensionKey(string pathOrExtension)
{
return Path.GetExtension(pathOrExtension).ToUpperInvariant();
}
public bool RegisterComponent<ComponentType>(string filenameOrExtension) where ComponentType : IFileDisplayComponent
{
string extensionString = GetExtensionKey(filenameOrExtension);
_extensionToComponents.Add(extensionString, typeof(ComponentType));
return true;
}
public Type? GetDisplayComponentType(string filenameOrExtension) {
var result = _extensionToComponents.TryGetValue(GetExtensionKey(filenameOrExtension), out var component);
return (result) ? component : null;
}
}

View File

@@ -1,5 +1,7 @@
<h3>FileDisplayImage</h3>
@implements IFileDisplayComponent
<h3>FileDisplayImage</h3>
@code {
[Parameter]
public required FilePathListing FilePath { get; set; }
}

View File

@@ -1,5 +1,8 @@
namespace OpenArchival.Blazor;
using OpenArchival.DataAccess;
namespace OpenArchival.Blazor;
public interface IFileDisplayComponent
{
public FilePathListing FilePath { get; set; }
}

View File

@@ -1,5 +1,7 @@
@using OpenArchival.Blazor.Components.Account.Shared
<Router AppAssembly="typeof(Program).Assembly">
<Router AppAssembly="typeof(Program).Assembly"
AdditionalAssemblies="new[] {typeof(OpenArchival.Blazor.AdminPages.ArchiveConfiguration).Assembly}">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>