diff --git a/AStar.Dev.slnx b/AStar.Dev.slnx index 04575dd..00f52f4 100644 --- a/AStar.Dev.slnx +++ b/AStar.Dev.slnx @@ -18,32 +18,29 @@ - + - - - - - - + + + + + + - - - - - + + + + + - - + + - - - - + + + + @@ -65,6 +62,10 @@ + + + + @@ -93,9 +94,8 @@ - - + + diff --git a/src/_aspire/AStar.Dev.AppHost/Configurations/DatabaseUpdaterApiProjectConfigurator.cs b/src/_aspire/AStar.Dev.AppHost/Configurations/DatabaseUpdaterApiProjectConfigurator.cs index 6eb1de1..ef64a64 100644 --- a/src/_aspire/AStar.Dev.AppHost/Configurations/DatabaseUpdaterApiProjectConfigurator.cs +++ b/src/_aspire/AStar.Dev.AppHost/Configurations/DatabaseUpdaterApiProjectConfigurator.cs @@ -5,10 +5,6 @@ namespace AStar.Dev.AppHost.Configurations; public static class DatabaseUpdaterApiProjectConfigurator { - public record DatabaseUpdaterApiProjectConfig(string ProjectName); - - public static DatabaseUpdaterApiProjectConfig GetConfig() => new(AspireConstants.Services.DatabaseUpdater); - public static void Configure( IDistributedApplicationBuilder builder, IResourceBuilder filesDb, @@ -16,8 +12,7 @@ public static void Configure( IResourceBuilder sqlServer, IResourceBuilder rabbitMq) { - DatabaseUpdaterApiProjectConfig config = GetConfig(); - _ = builder.AddProject(config.ProjectName) + _ = builder.AddProject(AspireConstants.Services.DatabaseUpdater) .WithReference(filesDb) .WaitFor(filesDb) .WithReference(migrations) diff --git a/src/_aspire/AStar.Dev.AppHost/Configurations/DistributedApplicationBuilderExtensions.cs b/src/_aspire/AStar.Dev.AppHost/Configurations/DistributedApplicationBuilderExtensions.cs index 8360764..693ac65 100644 --- a/src/_aspire/AStar.Dev.AppHost/Configurations/DistributedApplicationBuilderExtensions.cs +++ b/src/_aspire/AStar.Dev.AppHost/Configurations/DistributedApplicationBuilderExtensions.cs @@ -14,7 +14,7 @@ public static void AddApplicationProjects(this IDistributedApplicationBuilder bu IResourceBuilder astarDb = sqlServer.AddDatabase(AspireConstants.Sql.AStarDb); IResourceBuilder migrations = MigrationsConfigurator.Configure(builder, astarDb, sqlServer, sqlSaUserPassword, sqlAdminUserPassword, sqlFilesUserPassword, sqlUsageUserPassword); IResourceBuilder rabbitMq = RabbitMqConfigurator.Configure(builder); - DatabaseUpdaterApiProjectConfigurator.Configure(builder, astarDb, migrations, sqlServer, rabbitMq); + //DatabaseUpdaterApiProjectConfigurator.Configure(builder, astarDb, migrations, sqlServer, rabbitMq); UiProjectConfigurator.Configure(builder, astarDb, rabbitMq); } diff --git a/src/_aspire/AStar.Dev.AppHost/Configurations/SqlServerConfigurator.cs b/src/_aspire/AStar.Dev.AppHost/Configurations/SqlServerConfigurator.cs index 47616f1..2141747 100644 --- a/src/_aspire/AStar.Dev.AppHost/Configurations/SqlServerConfigurator.cs +++ b/src/_aspire/AStar.Dev.AppHost/Configurations/SqlServerConfigurator.cs @@ -13,7 +13,7 @@ public static IResourceBuilder Configure(IDistributedAp SqlServerConfig config = GetConfig(); return builder.AddSqlServer(config.ServerName, sqlPassword, config.Port) .WithLifetime(ContainerLifetime.Persistent) - //.WithDataBindMount(sqlMountDirectory) + .WithDataBindMount("/home/jason/databases") .WithExternalHttpEndpoints(); } } diff --git a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/FileClassificationJoined.cs b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/FileClassificationJoined.cs new file mode 100644 index 0000000..7561f57 --- /dev/null +++ b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/FileClassificationJoined.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.Files.Classifications.Api.Endpoints.FileClassifications.V1; + +/// +/// Provides an extension method to join file classifications with their parent classifications. +/// +public static class FileClassificationJoined +{ + /// + /// Joins file classifications with their associated parent classifications using a left join approach. + /// Each file classification is paired with its parent classification, if available. + /// + /// The database queryable set of file classifications. + /// An containing the joined file classification and parent classification data. + public static IQueryable JoinFileClassificationsToParents(this DbSet query) + => query.AsNoTracking() + .GroupJoin( + query, + fc => fc.ParentId, + p => p.Id, + (fc, parents) => new { fc, parents } + ) + .SelectMany(x => x.parents.DefaultIfEmpty(), (x, p) => new FileClassifications( x.fc, p )); +} diff --git a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/FileClassifications.cs b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/FileClassifications.cs new file mode 100644 index 0000000..4ddad7a --- /dev/null +++ b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/FileClassifications.cs @@ -0,0 +1,15 @@ +namespace AStar.Dev.Files.Classifications.Api.Endpoints.FileClassifications.V1; + +/// +/// Represents the aggregation of a file classification and its optional parent file classification. +/// This record is utilized to handle hierarchical relationships between file classifications, +/// where a file classification can have a parent classification for organizational purposes. +/// +/// +/// The primary file classification data, representing a specific classification instance. +/// +/// +/// The optional parent classification, indicating its hierarchical relationship to the primary +/// file classification. A null value signifies no parent classification. +/// +public record FileClassifications(Infrastructure.FilesDb.Models.FileClassification FileClassification, Infrastructure.FilesDb.Models.FileClassification? Parent); diff --git a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationRequest.cs b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationRequest.cs index 8d90f98..349e030 100644 --- a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationRequest.cs +++ b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationRequest.cs @@ -9,7 +9,7 @@ namespace AStar.Dev.Files.Classifications.Api.Endpoints.FileClassifications.V1; /// The object supports HTTP GET operation to access the required endpoint. /// [UsedImplicitly] -public record GetFileClassificationRequest(int CurrentPage = 1, int ItemsPerPage = 10) : IEndpointName +public record GetFileClassificationRequest(int CurrentPage = 1, int ItemsPerPage = 10) : IEndpointName,IPagingParameters { /// public string Name => EndpointConstants.GetFileClassificationsGroupName; diff --git a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationsHandler.cs b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationsHandler.cs index 682457e..848e2e9 100644 --- a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationsHandler.cs +++ b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationsHandler.cs @@ -19,14 +19,29 @@ public async Task> HandleAsy GetFileClassificationRequest fileClassifications, FilesContext filesContext, CancellationToken cancellationToken) { - if(fileClassifications.ItemsPerPage > 50) fileClassifications = fileClassifications with { ItemsPerPage = 50 }; + var pagingParams = PagingParams.CreateValid(fileClassifications); - List classifications = await filesContext.FileClassifications - .OrderBy(fc => fc.Name) - .Skip(fileClassifications.CurrentPage - 1).Take(fileClassifications.ItemsPerPage) - .Select(fc => new GetFileClassificationsResponse(fc.Id, fc.Name, fc.IncludeInSearch, fc.Celebrity)) + return await filesContext.FileClassifications + .AsNoTracking() + .GroupJoin( + filesContext.FileClassifications.AsNoTracking(), + fc => fc.ParentId, + p => p.Id, + (fc, parents) => new { fc, parents } + ) + .SelectMany(x => x.parents.DefaultIfEmpty(), (x, p) => new { x.fc, p }) + .OrderBy(x => x.fc.Name) + .Skip(pagingParams.SkipValue) + .Take(pagingParams.PageSize) + .Select(x => new GetFileClassificationsResponse( + x.fc.Id, + x.fc.Name, + x.fc.IncludeInSearch, + x.fc.Celebrity, + x.fc.ParentId, + x.p != null ? x.p.Name : null, + x.fc.SearchLevel + )) .ToListAsync(cancellationToken); - - return classifications; } } diff --git a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationsResponse.cs b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationsResponse.cs index fb24531..d6a0d50 100644 --- a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationsResponse.cs +++ b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/GetFileClassificationsResponse.cs @@ -3,4 +3,4 @@ namespace AStar.Dev.Files.Classifications.Api.Endpoints.FileClassifications.V1; /// /// Response model for file classification data /// -public record GetFileClassificationsResponse(Guid Id, string Name, bool IncludeInSearch, bool Celebrity); +public record GetFileClassificationsResponse(Guid Id, string Name, bool IncludeInSearch, bool Celebrity, Guid? ParentId, string? Parent = null, int SearchLevel = 2); diff --git a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/IPagingParameters.cs b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/IPagingParameters.cs new file mode 100644 index 0000000..e8c4b3f --- /dev/null +++ b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/IPagingParameters.cs @@ -0,0 +1,20 @@ +namespace AStar.Dev.Files.Classifications.Api.Endpoints.FileClassifications.V1; + +/// +/// Defines the parameters necessary to support pagination functionality, including the current page +/// and the number of items per page. +/// Objects implementing this interface provide a standardized way to manage paginated data retrieval. +/// +public interface IPagingParameters +{ + /// + /// Gets the current page number for pagination purposes. + /// This property specifies which page of results should be retrieved. + /// + int CurrentPage { get; } + + /// + /// Gets the number of items to be displayed per page in a paginated request. + /// + int ItemsPerPage { get; } +} diff --git a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/PagingParams.cs b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/PagingParams.cs new file mode 100644 index 0000000..937df0c --- /dev/null +++ b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Endpoints/FileClassifications/V1/PagingParams.cs @@ -0,0 +1,25 @@ +namespace AStar.Dev.Files.Classifications.Api.Endpoints.FileClassifications.V1; + +internal sealed class PagingParams +{ + private PagingParams(int pageSize, int skipValue) + { + PageSize = pageSize; + SkipValue = skipValue; + } + + public int PageSize { get; } + + public int SkipValue { get; } + + public static PagingParams CreateValid(IPagingParameters pagingParams) + { + var pageSize = pagingParams.ItemsPerPage <= 0 + ? 10 + : (pagingParams.ItemsPerPage > 50 ? 50 : pagingParams.ItemsPerPage); + var pageIndex = pagingParams.CurrentPage <= 0 ? 1 : pagingParams.CurrentPage; + var skip = (pageIndex - 1) * pageSize; + + return new PagingParams(pageSize, skip); + } +} diff --git a/src/modules/apis/AStar.Dev.Files.Classifications.Api/FileClassification.cs b/src/modules/apis/AStar.Dev.Files.Classifications.Api/FileClassification.cs index ff735a8..419971c 100644 --- a/src/modules/apis/AStar.Dev.Files.Classifications.Api/FileClassification.cs +++ b/src/modules/apis/AStar.Dev.Files.Classifications.Api/FileClassification.cs @@ -3,4 +3,4 @@ /// /// Represents a classification of files, containing details about name, search level, parent classification, and flags for celebrity status or search inclusion. /// -public record FileClassification(Guid Id, int SearchLevel, Guid? ParentId, string Name, bool Celebrity, bool IncludeInSearch); +public record FileClassification(Guid Id, int SearchLevel, string Name, Guid? ParentId, string? ParentName, bool Celebrity, bool IncludeInSearch); diff --git a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Services/FileClassificationsService.cs b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Services/FileClassificationsService.cs index 500f69d..64cc5be 100644 --- a/src/modules/apis/AStar.Dev.Files.Classifications.Api/Services/FileClassificationsService.cs +++ b/src/modules/apis/AStar.Dev.Files.Classifications.Api/Services/FileClassificationsService.cs @@ -21,5 +21,7 @@ public class FileClassificationsService2 : IFileClassificationsService2 /// /// A collection of file classification names. public async Task> GetFileClassificationsAsync() - => await _context.FileClassifications.Select(fc => new FileClassification(fc.Id, fc.SearchLevel, fc.ParentId, fc.Name, fc.Celebrity, fc.IncludeInSearch)).ToListAsync(); + => await _context.FileClassifications + .Select(fc => new FileClassification(fc.Id, fc.SearchLevel, fc.Name, fc.ParentId, fc.ParentId.ToString(), fc.Celebrity, fc.IncludeInSearch)) + .ToListAsync(); } diff --git a/src/modules/astar-dev-database-updater/AStar.Dev.Database.Updater/appsettings.json b/src/modules/astar-dev-database-updater/AStar.Dev.Database.Updater/appsettings.json index 113939e..0498a8a 100644 --- a/src/modules/astar-dev-database-updater/AStar.Dev.Database.Updater/appsettings.json +++ b/src/modules/astar-dev-database-updater/AStar.Dev.Database.Updater/appsettings.json @@ -23,7 +23,7 @@ }, "databaseUpdaterConfiguration": { "rootDirectory": "/home/jason/Documents/Pictures/_lookat/", - "mappingsFilePath": "/home/jason/Documents/Mappings.csv", + "mappingsFilePath": "/home/jason/Documents/File-Mappings.csv", "softDeleteScheduledTime": "02:00", "hardDeleteScheduledTime": "03:00", "newFilesScheduledTime": "04:00", diff --git a/src/nuget-packages/AStar.Dev.Infrastructure.FilesDb/AStar.Dev.Infrastructure.FilesDb.csproj b/src/nuget-packages/AStar.Dev.Infrastructure.FilesDb/AStar.Dev.Infrastructure.FilesDb.csproj index 632faac..2d4191a 100644 --- a/src/nuget-packages/AStar.Dev.Infrastructure.FilesDb/AStar.Dev.Infrastructure.FilesDb.csproj +++ b/src/nuget-packages/AStar.Dev.Infrastructure.FilesDb/AStar.Dev.Infrastructure.FilesDb.csproj @@ -3,6 +3,7 @@ latest-recommended True + true @@ -54,6 +55,8 @@ + + diff --git a/src/nuget-packages/AStar.Dev.Infrastructure.FilesDb/Models/FileId.cs b/src/nuget-packages/AStar.Dev.Infrastructure.FilesDb/Models/FileId.cs index b37bb08..47dea5c 100644 --- a/src/nuget-packages/AStar.Dev.Infrastructure.FilesDb/Models/FileId.cs +++ b/src/nuget-packages/AStar.Dev.Infrastructure.FilesDb/Models/FileId.cs @@ -1,9 +1,12 @@ +using AStar.Dev.Annotations; + namespace AStar.Dev.Infrastructure.FilesDb.Models; /// /// Defines the FileId /// -public record struct FileId() +[StrongId] +public partial record struct FileId() { /// The value of the File ID public Guid Value { get; set; } = Guid.CreateVersion7(); diff --git a/src/source-generators/AStar.Dev.Annotations/AStar.Dev.Annotations.csproj b/src/source-generators/AStar.Dev.Annotations/AStar.Dev.Annotations.csproj new file mode 100644 index 0000000..cfb3833 --- /dev/null +++ b/src/source-generators/AStar.Dev.Annotations/AStar.Dev.Annotations.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + preview + enable + enable + + + diff --git a/src/source-generators/AStar.Dev.Annotations/StrongIdAttribute.cs b/src/source-generators/AStar.Dev.Annotations/StrongIdAttribute.cs new file mode 100644 index 0000000..3b62b8b --- /dev/null +++ b/src/source-generators/AStar.Dev.Annotations/StrongIdAttribute.cs @@ -0,0 +1,40 @@ +using System; + +namespace AStar.Dev.Annotations; + +/// +/// Represents an attribute that can be used to mark a struct as a strong identifier. +/// +/// +/// This attribute is used to annotate structures with a specific identifier type, typically +/// to signify that the struct is a strongly typed id. The default identifier type is a GUID. +/// +/// +/// The type of the identifier associated with the struct. By default, it is "System.Guid". +/// +/// +/// Indicates whether the GUID used as the identifier for the struct conforms to the version 7 GUID format. +/// +[AttributeUsage(AttributeTargets.Struct)] +public sealed class StrongIdAttribute(string idType = "System.Guid", bool guidV7 = true) : Attribute +{ + /// + /// Gets the type of the identifier associated with a struct marked by the . + /// + /// + /// This property returns the string name of the identifier type. By default, it is "System.Guid". + /// The identifier type specifies the type used as an identifier for the struct. + /// + public string IdType { get; } = idType; + + /// + /// Indicates whether the GUID used as the identifier for a struct marked by the + /// conforms to the version 7 GUID format. + /// + /// + /// Version 7 GUIDs are designed to facilitate time-based sorting and are a newer standard + /// compared to the traditional random GUIDs (version 4). When this property is set to true, + /// the identifier adopts the version 7 format. + /// + public bool GuidV7 { get; } = guidV7; +} diff --git a/src/source-generators/AStar.Dev.SourceGenerators/AStar.Dev.SourceGenerators.csproj b/src/source-generators/AStar.Dev.SourceGenerators/AStar.Dev.SourceGenerators.csproj new file mode 100644 index 0000000..56651ec --- /dev/null +++ b/src/source-generators/AStar.Dev.SourceGenerators/AStar.Dev.SourceGenerators.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + preview + enable + true + false + true + false + + + + + + + + diff --git a/src/source-generators/AStar.Dev.SourceGenerators/StrongIdGenerator.cs b/src/source-generators/AStar.Dev.SourceGenerators/StrongIdGenerator.cs new file mode 100644 index 0000000..b78173a --- /dev/null +++ b/src/source-generators/AStar.Dev.SourceGenerators/StrongIdGenerator.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace AStar.Dev.SourceGenerators; + +[Generator] +public sealed class StrongIdGenerator : IIncrementalGenerator +{ + private const string AttrFqn = "Annotations.StrongIdAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext ctx) + { + // Discover partial structs annotated with [StrongId] + IncrementalValuesProvider candidates = ctx.SyntaxProvider.ForAttributeWithMetadataName( + AttrFqn, + static (node, _) => node is StructDeclarationSyntax s && s.Modifiers.Any(m => m.Text == "partial"), + static (syntaxCtx, _) => + { + var symbol = (INamedTypeSymbol)syntaxCtx.TargetSymbol; + AttributeData attr = syntaxCtx.Attributes[0]; + + var underlyingArg = attr.ConstructorArguments.Length == 1 + ? attr.ConstructorArguments[0].Value?.ToString() ?? "System.Guid" + : "System.Guid"; + + return new StrongIdModel( + symbol.ContainingNamespace.IsGlobalNamespace ? null : symbol.ContainingNamespace.ToDisplayString(), + symbol.Name, + symbol.DeclaredAccessibility, + underlyingArg + ); + }); + + // Use a custom comparer (no System.HashCode) so incremental semantics are stable + IncrementalValueProvider> models = candidates.WithComparer(StrongIdModelEqualityComparer.Instance).Collect(); + + ctx.RegisterSourceOutput(models, static (spc, batch) => + { + foreach (StrongIdModel? model in batch) spc.AddSource($"{model.Name}.StrongId.g.cs", Emit(model)); + }); + } + + private static string Emit(StrongIdModel m) + { + var ns = m.Namespace is null ? null : $"namespace {m.Namespace};"; + var acc = m.Accessibility.ToString().ToLowerInvariant(); + var t = m.UnderlyingTypeDisplay; + + // detect special-cases + var isGuid = string.Equals(t, "System.Guid", StringComparison.Ordinal) || + string.Equals(t, "Guid", StringComparison.Ordinal); + var isString = string.Equals(t, "System.String", StringComparison.Ordinal) || + string.Equals(t, "string", StringComparison.Ordinal); + + var toStringBody = isString ? "_value ?? string.Empty" : "_value.ToString()"; + var getHashBody = isString + ? "(_value == null ? 0 : _value.GetHashCode())" + : $"System.Collections.Generic.EqualityComparer<{t}>.Default.GetHashCode(_value)"; + + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + if (ns is not null) sb.AppendLine(ns).AppendLine(); + + sb.AppendLine( + $$""" + {{acc}} readonly partial struct {{m.Name}} : System.IEquatable<{{m.Name}}> + { + private readonly {{t}} _value; + public {{m.Name}}({{t}} value) => _value = value; + + public static implicit operator {{t}}({{m.Name}} id) => id._value; + public static explicit operator {{m.Name}}({{t}} value) => new(value); + + public bool Equals({{m.Name}} other) => System.Collections.Generic.EqualityComparer<{{t}}>.Default.Equals(_value, other._value); + public override bool Equals(object? obj) => obj is {{m.Name}} other && Equals(other); + public override int GetHashCode() => {{getHashBody}}; + public override string ToString() => {{toStringBody}}; + """); + + if (isGuid) + { + sb.AppendLine( + $$""" + + public static {{m.Name}} New() => new(System.Guid.NewGuid()); + + public static bool TryParse(string? s, out {{m.Name}} value) + { + var ok = System.Guid.TryParse(s, out var g); + value = ok ? new(g) : default; + return ok; + } + """); + } + + sb.AppendLine("}"); + return sb.ToString(); + } + + private sealed class StrongIdModel( + string? ns, + string name, + Accessibility accessibility, + string underlyingTypeDisplay) + { + public string? Namespace { get; } = ns; + public string Name { get; } = name; + public Accessibility Accessibility { get; } = accessibility; + public string UnderlyingTypeDisplay { get; } = underlyingTypeDisplay; + } + + private sealed class StrongIdModelEqualityComparer : IEqualityComparer + { + public static readonly StrongIdModelEqualityComparer Instance = new(); + + public bool Equals(StrongIdModel? x, StrongIdModel? y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + + return string.Equals(x.Namespace, y.Namespace, StringComparison.Ordinal) + && string.Equals(x.Name, y.Name, StringComparison.Ordinal) + && string.Equals(x.UnderlyingTypeDisplay, y.UnderlyingTypeDisplay, StringComparison.Ordinal) + && x.Accessibility == y.Accessibility; + } + + public int GetHashCode(StrongIdModel obj) => (obj.Namespace, obj.Name, obj.UnderlyingTypeDisplay, obj.Accessibility).GetHashCode(); + } +} diff --git a/src/uis/AStar.Dev.Web/Components/Layout/NavMenu.razor b/src/uis/AStar.Dev.Web/Components/Layout/NavMenu.razor index 0f5ea6e..348368f 100644 --- a/src/uis/AStar.Dev.Web/Components/Layout/NavMenu.razor +++ b/src/uis/AStar.Dev.Web/Components/Layout/NavMenu.razor @@ -9,48 +9,47 @@ diff --git a/src/uis/AStar.Dev.Web/Components/Pages/Admin/FileClassifications.razor b/src/uis/AStar.Dev.Web/Components/Pages/Admin/FileClassifications.razor index a07646e..a81a84f 100644 --- a/src/uis/AStar.Dev.Web/Components/Pages/Admin/FileClassifications.razor +++ b/src/uis/AStar.Dev.Web/Components/Pages/Admin/FileClassifications.razor @@ -1,4 +1,206 @@ @page "/admin/file-classifications" +@attribute [AllowAnonymous] +@using FluentUI.Demo.Shared +@using FluentUI.Demo.Shared.SampleData +@using Microsoft.FluentUI.AspNetCore.Components +@using App = AStar.Dev.Web.Components.App + +@inject HttpClient Http + +@inject NavigationManager NavManager + @App.PageTitle("File Classifications")

File Classifications

+ + + + + + + + + + +
+ + + + + + + + +   Nothing to see here. Carry on! + + + + Loading...
+ +
+
+
+
+
+ + +Simulate data loading + + + + + + + + + + + + Clear + + + Search + + + + +
+
+ + + + + + + + + + + + +
+ + +@code { + + FluentDataGrid _dataGrid = null!; + IQueryable _foodRecallItems = null!; + bool _loading = true; + readonly PaginationState _pagination2 = new PaginationState { ItemsPerPage = 10 }; + string? _stateFilter = "NY"; + + protected async Task RefreshItemsAsync(GridItemsProviderRequest req) + { + _loading = true; + await InvokeAsync(StateHasChanged); + + var filters = new Dictionary + { + { "skip", req.StartIndex }, + { "limit", req.Count }, + }; + + if (!string.IsNullOrWhiteSpace(_stateFilter)) + filters.Add("search", $"state:{_stateFilter}"); + + var s = req.GetSortByProperties().FirstOrDefault(); + if (req.SortByColumn != null && !string.IsNullOrEmpty(s.PropertyName)) + { + filters.Add("sort", s.PropertyName + (s.Direction == SortDirection.Ascending ? ":asc" : ":desc")); + } + + var url = NavManager.GetUriWithQueryParameters("https://api.fda.gov/food/enforcement.json", filters); + + var response = await Http.GetFromJsonAsync(url); + + _foodRecallItems = response!.Results.AsQueryable(); + await _pagination2.SetTotalItemCountAsync(response!.Meta.Results.Total); + + _loading = false; + await InvokeAsync(StateHasChanged); + + } + + public void ClearFilters() + { + _stateFilter = null; + } + + public async Task DataGridRefreshDataAsync() + { + await _dataGrid.RefreshDataAsync(true); + } + readonly PaginationState _pagination = new () { ItemsPerPage = 4 }; + + record Person(int PersonId, string Name, DateOnly BirthDate); + + readonly IQueryable _people = new[] + { + new Person(10895, "Jean Martin", new DateOnly(1985, 3, 16)), + new Person(10944, "António Langa", new DateOnly(1991, 12, 1)), + new Person(11203, "Julie Smith", new DateOnly(1958, 10, 10)), + new Person(11205, "Nur Sari", new DateOnly(1922, 4, 27)), + new Person(11898, "Jose Hernandez", new DateOnly(2011, 5, 3)), + new Person(12130, "Kenji Sato", new DateOnly(2004, 1, 9)), + }.AsQueryable(); + + private readonly RenderFragment _template = @; + + FluentDataGrid? _grid; + FluentSwitch? _clearToggle; + + bool _clearItems = false; + public record SampleGridData(string Item1, string Item2, string Item3, string Item4); + + IQueryable? _items = Enumerable.Empty().AsQueryable(); + + private IQueryable GenerateSampleGridData(int size) + { + SampleGridData[] data = new SampleGridData[size]; + + for (int i = 0; i < size; i++) + { + data[i] = new SampleGridData($"value {i}-1", $"value {i}-2", $"value {i}-3", $"value {i}-4"); + } + return data.AsQueryable(); + } + protected override void OnInitialized() + { + _items = GenerateSampleGridData(5000); + } + + private void ToggleItems() + { + _items = _clearItems ? null : GenerateSampleGridData(5000); + } + + private async Task SimulateDataLoading() + { + _clearItems = false; + + _items = null; + _grid?.SetLoadingState(true); + + await Task.Delay(1500); + + _items = GenerateSampleGridData(5000); + _grid?.SetLoadingState(false); + } +} diff --git a/src/uis/AStar.Dev.Web/Components/Pages/Shared/Search.razor.cs b/src/uis/AStar.Dev.Web/Components/Pages/Shared/Search.razor.cs index 249553f..0c93687 100644 --- a/src/uis/AStar.Dev.Web/Components/Pages/Shared/Search.razor.cs +++ b/src/uis/AStar.Dev.Web/Components/Pages/Shared/Search.razor.cs @@ -50,7 +50,7 @@ protected override async Task OnInitializedAsync() { FileClassifications = [ - new FileClassification(new Dev.Files.Classifications.Api.FileClassification(Guid.Empty, 1, null, "-- Select (Optional) --", false, false) + new FileClassification(new Dev.Files.Classifications.Api.FileClassification(Guid.Empty, 1, "-- Select (Optional) --", null,null, false, false) ) ]; diff --git a/src/uis/AStar.Dev.Web/Models/FileClassification.cs b/src/uis/AStar.Dev.Web/Models/FileClassification.cs index f82b10d..9279649 100644 --- a/src/uis/AStar.Dev.Web/Models/FileClassification.cs +++ b/src/uis/AStar.Dev.Web/Models/FileClassification.cs @@ -21,11 +21,11 @@ public FileClassification(Files.Classifications.Api.FileClassification fc) /// Gets or sets the unique identifier for the file classification. /// This property serves as the primary key for the entity. /// - public Guid Id { get; set; } + public Guid? Id { get; set; } /// /// - public int SearchLevel { get; set; } + public int? SearchLevel { get; set; } /// /// @@ -36,18 +36,18 @@ public FileClassification(Files.Classifications.Api.FileClassification fc) /// This property represents the descriptive label for a specific classification /// and is often used to identify or categorize files within the database. /// - public string Name { get; set; } = string.Empty; + public string? Name { get; set; } /// /// Gets or sets a value indicating whether the file classification is considered a "Celebrity." /// This property is used to mark specific classifications with special significance. /// - public bool Celebrity { get; set; } + public bool? Celebrity { get; set; } /// /// Gets or sets a value indicating whether this classification should be included in search results. /// This property determines if files associated with this classification are considered searchable. /// - public bool IncludeInSearch { get; set; } + public bool? IncludeInSearch { get; set; } } diff --git a/test/modules/apis/AStar.Dev.Files.Classifications.Api.Tests.Unit/Endpoints/FileClassifications/V1/GetFileClassificationsResponseTests.cs b/test/modules/apis/AStar.Dev.Files.Classifications.Api.Tests.Unit/Endpoints/FileClassifications/V1/GetFileClassificationsResponseTests.cs index 380f52b..e10b796 100644 --- a/test/modules/apis/AStar.Dev.Files.Classifications.Api.Tests.Unit/Endpoints/FileClassifications/V1/GetFileClassificationsResponseTests.cs +++ b/test/modules/apis/AStar.Dev.Files.Classifications.Api.Tests.Unit/Endpoints/FileClassifications/V1/GetFileClassificationsResponseTests.cs @@ -21,14 +21,16 @@ public void Construction_Should_Set_Values_Correctly() public void Deconstruction_Should_Work_As_Expected() { var id = Guid.CreateVersion7(); - var dto = new GetFileClassificationsResponse(id, "Alpha", false, true); + var dto = new GetFileClassificationsResponse(id, "Alpha", true, true); - var (deId, deName, deInclude, deCeleb) = dto; + var (deId, deName, deInclude, deCeleb, deParentId, deSearchLevel) = dto; deId.ShouldBe(id); deName.ShouldBe("Alpha"); - deInclude.ShouldBeFalse(); + deInclude.ShouldBeTrue(); deCeleb.ShouldBeTrue(); + deParentId.ShouldBeNull(); + deSearchLevel.ShouldBe(2); } [Fact]