diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 095e903f9c..ffd31a5d0b 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -13,6 +13,8 @@ + + @@ -36,7 +38,7 @@ - + diff --git a/src/FSH.Starter.sln b/src/FSH.Starter.sln index 6c177dd28f..4fd350bcab 100644 --- a/src/FSH.Starter.sln +++ b/src/FSH.Starter.sln @@ -24,6 +24,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Migrations", "Migrations", "{12F8343D-20A6-4E24-B0F5-3A66F2228CF6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebApi", "WebApi", "{CE64E92B-E088-46FB-9028-7FB6B67DEC55}" + ProjectSection(SolutionItems) = preProject + Directory.Packages.props = Directory.Packages.props + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Blazor", "Blazor", "{2B1F75CE-07A6-4C19-A2E3-F9E062CFDDFB}" EndProject diff --git a/src/api/framework/Core/DataIO/IDataExport.cs b/src/api/framework/Core/DataIO/IDataExport.cs new file mode 100644 index 0000000000..d6deacc323 --- /dev/null +++ b/src/api/framework/Core/DataIO/IDataExport.cs @@ -0,0 +1,8 @@ +namespace FSH.Framework.Core.DataIO; + +public interface IDataExport +{ + byte[] ListToByteArray(IList list); + Stream WriteToStream(IList data); + Stream WriteToTemplate(T data, string templateFile, string outputFolder); +} diff --git a/src/api/framework/Core/DataIO/IDataImport.cs b/src/api/framework/Core/DataIO/IDataImport.cs new file mode 100644 index 0000000000..941344f0e3 --- /dev/null +++ b/src/api/framework/Core/DataIO/IDataImport.cs @@ -0,0 +1,10 @@ +using FSH.Framework.Core.Storage.File; +using FSH.Framework.Core.Storage.File.Features; + +namespace FSH.Framework.Core.DataIO; + +public interface IDataImport +{ + + Task> ToListAsync(FileUploadCommand request, FileType supportedFileType, string sheetName = "Sheet1"); +} diff --git a/src/api/framework/Core/DataIO/ImportResponse.cs b/src/api/framework/Core/DataIO/ImportResponse.cs new file mode 100644 index 0000000000..b9d7edb3f1 --- /dev/null +++ b/src/api/framework/Core/DataIO/ImportResponse.cs @@ -0,0 +1,8 @@ +namespace FSH.Framework.Core.DataIO; + +public class ImportResponse +{ + public int TotalRecords { get; set; } + + public string? Message { get; set; } +} diff --git a/src/api/framework/Core/Storage/File/FileType.cs b/src/api/framework/Core/Storage/File/FileType.cs index 267968aaa6..72a204bff2 100644 --- a/src/api/framework/Core/Storage/File/FileType.cs +++ b/src/api/framework/Core/Storage/File/FileType.cs @@ -5,5 +5,14 @@ namespace FSH.Framework.Core.Storage.File; public enum FileType { [Description(".jpg,.png,.jpeg")] - Image + Image, + + [Description(".xls,.xlsx")] + Excel, + + [Description(".zip")] + QuizMedia, + + [Description(".pdf,.doc,.zip,.rar")] + Doc } diff --git a/src/api/framework/Infrastructure/DataIO/DataExport.cs b/src/api/framework/Infrastructure/DataIO/DataExport.cs new file mode 100644 index 0000000000..74ccc06f8c --- /dev/null +++ b/src/api/framework/Infrastructure/DataIO/DataExport.cs @@ -0,0 +1,158 @@ +using ClosedXML.Excel; +using ClosedXML.Report; +using System.ComponentModel; +using System.Data; +using System.Reflection; +using FSH.Framework.Core.DataIO; + +namespace FSH.Framework.Infrastructure.DataIO; + +public class DataExport : IDataExport +{ + /// + /// + /// + /// + /// + /// + /// + public byte[] ListToByteArray(IList list) + { + if (list is null || list.Count is 0) + { + throw new ArgumentNullException(nameof(list)); + } + + // Create DataTable from List + + DataTable dataTable = ListToDataTable(list); + + // Create IXLWorkbook from DataTable + // IXLWorkbook workbook = DataTableToIXLWorkbook(typeof(T).Name, dataTable) + + XLWorkbook workbook = DataTableToIxlWorkbook("Sheet1", dataTable); + + // Convert IXLWorkbook to ByteArray + + using MemoryStream memoryStream = new(); + workbook.SaveAs(memoryStream); + byte[] fileByteArray = memoryStream.ToArray(); + + return fileByteArray ; + } + + /// + /// Creates a DataTable from a List of type ; using the properties of to create the DataTable Columns and the items from List of type to create the DataTables Rows. + /// + /// DataType used to create the DataTable; DataType properties are used to create the DataTable Columns. + /// List of items to create the rows of the DataTable. + /// Returns a DataTable created from the List of type + /// + private static DataTable ListToDataTable(IList list) + { + if (list is null || list.Count is 0) + { + throw new ArgumentNullException(nameof(list)); + } + + DataTable dataTable = new(typeof(T).Name); + + // Create data table columns from data model properties + PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); + foreach (PropertyInfo property in properties) + { + dataTable.Columns.Add(property.Name); + } + + // Create data table rows from list items + foreach (T item in list) + { + object?[] values = new object?[properties.Length]; + for (int i = 0; i < properties.Length; i++) + { + //inserting property values to datatable rows + values[i] = properties[i].GetValue(item, null); + } + + dataTable.Rows.Add(values); + } + + return dataTable; + } + + /// + /// Create XLWorkbook from Datatable + /// + /// + /// + /// + /// + private static XLWorkbook DataTableToIxlWorkbook(string workbookName, DataTable dataTable) + { + if (string.IsNullOrWhiteSpace(workbookName)) + { + throw new ArgumentNullException(nameof(workbookName)); + } + + if (dataTable is null || dataTable.Rows.Count is 0) + { + throw new ArgumentNullException(nameof(dataTable)); + } + + XLWorkbook workbook = new(); + workbook.Worksheets.Add(dataTable, workbookName); + return workbook; + } + + public Stream WriteToStream(IList data) + { + var properties = TypeDescriptor.GetProperties(typeof(T)); + var table = new DataTable("Sheet1", "table"); // "Sheet1" = typeof(T).Name + + foreach (PropertyDescriptor prop in properties) + table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType); + + foreach (var item in data) + { + var row = table.NewRow(); + foreach (PropertyDescriptor prop in properties) + row[prop.Name] = prop.GetValue(item) ?? DBNull.Value; + table.Rows.Add(row); + } + + using var wb = new XLWorkbook(); + wb.Worksheets.Add(table); + + Stream stream = new MemoryStream(); + wb.SaveAs(stream); + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public Stream WriteToTemplate(T data, string templateFile, string outputFolder) + { + var template = new XLTemplate(templateFile); + template.AddVariable(data); + template.Generate(); + + // save to file on API server + //const string outputFile = @".\Output\AssetDeliveryFrom.xlsx" + string outputFile = outputFolder + templateFile; + template.SaveAs(outputFile); + + // or get bytes to return excel file from web api + Stream stream = new MemoryStream(); + template.Workbook.SaveAs(stream); + stream.Seek(0, SeekOrigin.Begin); + return stream; + } +} diff --git a/src/api/framework/Infrastructure/DataIO/DataImport.cs b/src/api/framework/Infrastructure/DataIO/DataImport.cs new file mode 100644 index 0000000000..eeb139b2d3 --- /dev/null +++ b/src/api/framework/Infrastructure/DataIO/DataImport.cs @@ -0,0 +1,110 @@ +using System.Text.RegularExpressions; +using ClosedXML.Excel; +using FSH.Framework.Core.DataIO; +using FSH.Framework.Core.Exceptions; +using FSH.Framework.Core.Storage.File; +using FSH.Framework.Core.Storage.File.Features; + +namespace FSH.Framework.Infrastructure.DataIO; + +public class DataImport : IDataImport +{ + public async Task> ToListAsync(FileUploadCommand request, FileType supportedFileType, string sheetName = "Sheet1") + { + // string base64Data = Regex.Match(request.Data, string.Format("data:{0}/(?.+?),(?.+)", supportedFileType.ToString().ToLower())).Groups["data"].Value + string base64Data = Regex.Match(request.Data, + $"data:{supportedFileType.ToString().ToLower()}/(?.+?),(?.+)").Groups["data"].Value; + + var streamData = new MemoryStream(Convert.FromBase64String(base64Data)); + + List list = []; + Type typeOfObject = typeof(T); + + using (IXLWorkbook workbook = new XLWorkbook(streamData)) + { + // Read the first Sheet from Excel file. + var worksheet = workbook.Worksheets.FirstOrDefault(w => w.Name == sheetName) + ?? throw new NotFoundException($"Sheet with name {sheetName} does not exist!"); + + + var properties = typeOfObject.GetProperties(); + // header column texts + var columns = worksheet.FirstRow().Cells().Select((v, i) => new { v.Value, Index = i + 1 }); + + // indexing in closedxml starts with 1 not from 0 + // Skip first row which is used for column header texts + foreach (IXLRow row in worksheet.RowsUsed().Skip(1)) + { + T item = (T)Activator.CreateInstance(typeOfObject)!; + + foreach (var prop in properties) + { + try + { + var propertyType = prop.PropertyType; + var col = columns.SingleOrDefault(c => c.Value.ToString() == prop.Name); + if (col == null) continue; + + object? obj = GetObjectByDataType(propertyType, row.Cell(col.Index).Value); + + if(obj != null) prop.SetValue(item, obj); + } + catch + { + // if any error + // return await Task.FromResult(new List()) + } + } + + list.Add(item); + } + } + + return await Task.FromResult(list); + } + + private static object? GetObjectByDataType(Type propertyType, XLCellValue cellValue) + { + if (cellValue.ToString() == "null" || cellValue.IsBlank) + { + return null; + } + + object? val; + if (propertyType.IsEnum) + { + val = Convert.ToInt32(cellValue.GetNumber()); + return Enum.ToObject(propertyType, val); + } + else if (propertyType == typeof(Guid) || propertyType == typeof(Guid?)) + { + val = Guid.Parse(cellValue.ToString()); + } + else if (propertyType == typeof(int) || propertyType == typeof(int?)) + { + val = Convert.ToInt32(cellValue.GetNumber()); + } + else if (propertyType == typeof(decimal)) + { + val = Convert.ToDecimal(cellValue.GetNumber()); + } + else if (propertyType == typeof(long)) + { + val = Convert.ToInt64(cellValue.GetNumber()); + } + else if (propertyType == typeof(bool) || propertyType == typeof(bool?)) + { + val = Convert.ToBoolean(cellValue.GetBoolean()); + } + else if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) + { + val = Convert.ToDateTime(cellValue.GetDateTime()); + } + else + { + val = cellValue.ToString(); + } + + return Convert.ChangeType(val, Nullable.GetUnderlyingType(propertyType) ?? propertyType); + } +} diff --git a/src/api/framework/Infrastructure/DataIO/Extensions.cs b/src/api/framework/Infrastructure/DataIO/Extensions.cs new file mode 100644 index 0000000000..48f2285d0b --- /dev/null +++ b/src/api/framework/Infrastructure/DataIO/Extensions.cs @@ -0,0 +1,15 @@ +using FSH.Framework.Core.DataIO; +using Microsoft.Extensions.DependencyInjection; + +namespace FSH.Framework.Infrastructure.DataIO; + +internal static class Extensions +{ + internal static IServiceCollection ConfigureDataImportExport(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + + return services; + } +} diff --git a/src/api/framework/Infrastructure/Extensions.cs b/src/api/framework/Infrastructure/Extensions.cs index 865bce172d..ba4cf259e2 100644 --- a/src/api/framework/Infrastructure/Extensions.cs +++ b/src/api/framework/Infrastructure/Extensions.cs @@ -8,6 +8,7 @@ using FSH.Framework.Infrastructure.Behaviours; using FSH.Framework.Infrastructure.Caching; using FSH.Framework.Infrastructure.Cors; +using FSH.Framework.Infrastructure.DataIO; using FSH.Framework.Infrastructure.Exceptions; using FSH.Framework.Infrastructure.Identity; using FSH.Framework.Infrastructure.Jobs; @@ -49,6 +50,7 @@ public static WebApplicationBuilder ConfigureFshFramework(this WebApplicationBui builder.Services.AddExceptionHandler(); builder.Services.AddProblemDetails(); builder.Services.AddHealthChecks(); + builder.Services.ConfigureDataImportExport(); builder.Services.AddOptions().BindConfiguration(nameof(OriginOptions)); // Define module assemblies diff --git a/src/api/framework/Infrastructure/Infrastructure.csproj b/src/api/framework/Infrastructure/Infrastructure.csproj index 020cf21d33..a713d6f596 100644 --- a/src/api/framework/Infrastructure/Infrastructure.csproj +++ b/src/api/framework/Infrastructure/Infrastructure.csproj @@ -18,6 +18,8 @@ + + diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Export/v1/ExportProductsHandler.cs b/src/api/modules/Catalog/Catalog.Application/Products/Export/v1/ExportProductsHandler.cs new file mode 100644 index 0000000000..4d8be0801e --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/Export/v1/ExportProductsHandler.cs @@ -0,0 +1,27 @@ +using FSH.Framework.Core.DataIO; +using FSH.Framework.Core.Persistence; +using FSH.Framework.Core.Specifications; +using FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; +using FSH.Starter.WebApi.Catalog.Domain; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.IO; +using FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.Export.v1; + +public class ExportProductsHandler( + [FromKeyedServices("catalog:products")] IReadRepository repository, IDataExport dataExport) + : IRequestHandler +{ + public async Task Handle(ExportProductsRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + var spec = new ExportProductsSpecs(request); + + var items = await repository.ListAsync(spec, cancellationToken).ConfigureAwait(false); + + return dataExport.ListToByteArray(items); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Export/v1/ExportProductsRequest.cs b/src/api/modules/Catalog/Catalog.Application/Products/Export/v1/ExportProductsRequest.cs new file mode 100644 index 0000000000..7921a100fc --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/Export/v1/ExportProductsRequest.cs @@ -0,0 +1,11 @@ +using FSH.Framework.Core.Paging; +using MediatR; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.Export.v1; + +public class ExportProductsRequest : BaseFilter, IRequest +{ + public Guid? BrandId { get; set; } + public decimal? MinimumRate { get; set; } + public decimal? MaximumRate { get; set; } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Export/v1/ExportProductsSpecs.cs b/src/api/modules/Catalog/Catalog.Application/Products/Export/v1/ExportProductsSpecs.cs new file mode 100644 index 0000000000..9ac3d26f21 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/Export/v1/ExportProductsSpecs.cs @@ -0,0 +1,16 @@ +using Ardalis.Specification; +using FSH.Framework.Core.Specifications; +using FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; +using FSH.Starter.WebApi.Catalog.Domain; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.Export.v1; + +public sealed class ExportProductsSpecs : EntitiesByBaseFilterSpec +{ + public ExportProductsSpecs(ExportProductsRequest request) + : base(request) => + Query + .OrderBy(c => c.Name) + .Where(p => p.Price >= request.MinimumRate!.Value, request.MinimumRate.HasValue) + .Where(p => p.Price <= request.MaximumRate!.Value, request.MaximumRate.HasValue); +} diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductHandler.cs b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductHandler.cs index 8aea28540a..1a25eac88a 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductHandler.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductHandler.cs @@ -9,9 +9,9 @@ namespace FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; public sealed class GetProductHandler( [FromKeyedServices("catalog:products")] IReadRepository repository, ICacheService cache) - : IRequestHandler + : IRequestHandler { - public async Task Handle(GetProductRequest request, CancellationToken cancellationToken) + public async Task Handle(GetProductRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); var item = await cache.GetOrSetAsync( @@ -20,7 +20,7 @@ public async Task Handle(GetProductRequest request, Cancellatio { var productItem = await repository.GetByIdAsync(request.Id, cancellationToken); if (productItem == null) throw new ProductNotFoundException(request.Id); - return new ProductResponse(productItem.Id, productItem.Name, productItem.Description, productItem.Price); + return new GetProductResponse(productItem.Id, productItem.Name, productItem.Description, productItem.Price); }, cancellationToken: cancellationToken); return item!; diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductRequest.cs b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductRequest.cs index a85bd13fb1..c151bfb1bf 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductRequest.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductRequest.cs @@ -1,7 +1,7 @@ using MediatR; namespace FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; -public class GetProductRequest : IRequest +public class GetProductRequest : IRequest { public Guid Id { get; set; } public GetProductRequest(Guid id) => Id = id; diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductResponse.cs b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductResponse.cs new file mode 100644 index 0000000000..a616e42534 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductResponse.cs @@ -0,0 +1,2 @@ +namespace FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; +public sealed record GetProductResponse(Guid? Id, string Name, string? Description, decimal Price); diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/ProductResponse.cs b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/ProductResponse.cs deleted file mode 100644 index 2c199ef2db..0000000000 --- a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/ProductResponse.cs +++ /dev/null @@ -1,2 +0,0 @@ -namespace FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; -public sealed record ProductResponse(Guid? Id, string Name, string? Description, decimal Price); diff --git a/src/api/modules/Catalog/Catalog.Application/Products/GetList/v1/GetProductsHandler.cs b/src/api/modules/Catalog/Catalog.Application/Products/GetList/v1/GetProductsHandler.cs new file mode 100644 index 0000000000..91290d2810 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/GetList/v1/GetProductsHandler.cs @@ -0,0 +1,23 @@ +using FSH.Framework.Core.Persistence; +using FSH.Framework.Core.Specifications; +using FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; +using FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; +using FSH.Starter.WebApi.Catalog.Domain; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.GetList.v1; + +public class GetProductsHandler( + [FromKeyedServices("catalog:products")] IReadRepository repository) + : IRequestHandler> +{ + public async Task> Handle(GetProductsRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + var spec = new GetProductsSpecs(request); + + return await repository.ListAsync(spec, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Products/GetList/v1/GetProductsRequest.cs b/src/api/modules/Catalog/Catalog.Application/Products/GetList/v1/GetProductsRequest.cs new file mode 100644 index 0000000000..ed360cee4f --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/GetList/v1/GetProductsRequest.cs @@ -0,0 +1,12 @@ +using FSH.Framework.Core.Paging; +using FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; +using MediatR; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.GetList.v1; + +public class GetProductsRequest : BaseFilter, IRequest> +{ + public Guid? BrandId { get; set; } + public decimal? MinimumRate { get; set; } + public decimal? MaximumRate { get; set; } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Products/GetList/v1/GetProductsSpecs.cs b/src/api/modules/Catalog/Catalog.Application/Products/GetList/v1/GetProductsSpecs.cs new file mode 100644 index 0000000000..3022e3b8c4 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/GetList/v1/GetProductsSpecs.cs @@ -0,0 +1,17 @@ +using Ardalis.Specification; +using FSH.Framework.Core.Paging; +using FSH.Framework.Core.Specifications; +using FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; +using FSH.Starter.WebApi.Catalog.Domain; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.GetList.v1; + +public sealed class GetProductsSpecs : EntitiesByBaseFilterSpec +{ + public GetProductsSpecs(GetProductsRequest command) + : base(command) => + Query + .OrderBy(c => c.Name) + .Where(p => p.Price >= command.MinimumRate!.Value, command.MinimumRate.HasValue) + .Where(p => p.Price <= command.MaximumRate!.Value, command.MaximumRate.HasValue); +} diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Import/v1/ImportProductsCommand.cs b/src/api/modules/Catalog/Catalog.Application/Products/Import/v1/ImportProductsCommand.cs new file mode 100644 index 0000000000..bf2794ef42 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/Import/v1/ImportProductsCommand.cs @@ -0,0 +1,8 @@ +using System.Collections.Concurrent; +using FSH.Framework.Core.DataIO; +using FSH.Framework.Core.Storage.File.Features; +using MediatR; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.Import.v1; + +public record ImportProductsCommand(FileUploadCommand UploadFile, bool IsUpdate ) : IRequest; diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Import/v1/ImportProductsHandler.cs b/src/api/modules/Catalog/Catalog.Application/Products/Import/v1/ImportProductsHandler.cs new file mode 100644 index 0000000000..7df2b85b11 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/Import/v1/ImportProductsHandler.cs @@ -0,0 +1,54 @@ +using FSH.Framework.Core.DataIO; +using FSH.Framework.Core.Persistence; +using FSH.Framework.Core.Storage.File; +using FSH.Starter.WebApi.Catalog.Domain; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.Import.v1; + +public class ImportProductsHandler( + [FromKeyedServices("catalog:products")] IRepository repository, IDataImport dataImport) + : IRequestHandler +{ + public async Task Handle(ImportProductsCommand request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + var items = await dataImport.ToListAsync(request.UploadFile, FileType.Excel); + + ImportResponse response = new() + { + TotalRecords = items.Count, + Message = "" + + }; + + if (response.TotalRecords <= 0) + { + response.Message = "File is empty or Invalid format"; + return response; + } + + try + { + if (request.IsUpdate) + { + await repository.UpdateRangeAsync(items, cancellationToken); + response.Message = " Updated successful"; + } + else + { + await repository.AddRangeAsync (items, cancellationToken); + response.Message = "Added successful"; + } + } + catch (Exception) + { + response.Message = "Internal error!"; + // throw new CustomException("Internal error!") + } + + return response; + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/ProductDto.cs b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/ProductDto.cs new file mode 100644 index 0000000000..01cbd54a9e --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/ProductDto.cs @@ -0,0 +1,3 @@ +namespace FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; + +public record ProductDto(Guid? Id, string Name, string? Description, decimal Price); diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs deleted file mode 100644 index 6d9ee52a07..0000000000 --- a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Ardalis.Specification; -using FSH.Framework.Core.Paging; -using FSH.Framework.Core.Specifications; -using FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; -using FSH.Starter.WebApi.Catalog.Domain; - -namespace FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; -public class SearchProductSpecs : EntitiesByPaginationFilterSpec -{ - public SearchProductSpecs(SearchProductsCommand command) - : base(command) => - Query - .OrderBy(c => c.Name, !command.HasOrderBy()) - .Where(p => p.Price >= command.MinimumRate!.Value, command.MinimumRate.HasValue) - .Where(p => p.Price <= command.MaximumRate!.Value, command.MaximumRate.HasValue); -} diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsHandler.cs b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsHandler.cs index 7c6c290df0..f3c3052edf 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsHandler.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsHandler.cs @@ -1,6 +1,5 @@ using FSH.Framework.Core.Paging; using FSH.Framework.Core.Persistence; -using FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; using FSH.Starter.WebApi.Catalog.Domain; using MediatR; using Microsoft.Extensions.DependencyInjection; @@ -9,18 +8,18 @@ namespace FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; public sealed class SearchProductsHandler( [FromKeyedServices("catalog:products")] IReadRepository repository) - : IRequestHandler> + : IRequestHandler> { - public async Task> Handle(SearchProductsCommand request, CancellationToken cancellationToken) + public async Task> Handle(SearchProductsRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); - var spec = new SearchProductSpecs(request); + var spec = new SearchProductsSpecs(request); var items = await repository.ListAsync(spec, cancellationToken).ConfigureAwait(false); var totalCount = await repository.CountAsync(spec, cancellationToken).ConfigureAwait(false); - return new PagedList(items, request!.PageNumber, request!.PageSize, totalCount); + return new PagedList(items, request!.PageNumber, request!.PageSize, totalCount); } } diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsCommand.cs b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsRequest.cs similarity index 62% rename from src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsCommand.cs rename to src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsRequest.cs index ed19c2f958..2b14d1da6b 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsCommand.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsRequest.cs @@ -1,10 +1,9 @@ using FSH.Framework.Core.Paging; -using FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; using MediatR; namespace FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; -public class SearchProductsCommand : PaginationFilter, IRequest> +public class SearchProductsRequest : PaginationFilter, IRequest> { public Guid? BrandId { get; set; } public decimal? MinimumRate { get; set; } diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsSpecs.cs b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsSpecs.cs new file mode 100644 index 0000000000..73bf3f6e8b --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductsSpecs.cs @@ -0,0 +1,15 @@ +using Ardalis.Specification; +using FSH.Framework.Core.Paging; +using FSH.Framework.Core.Specifications; +using FSH.Starter.WebApi.Catalog.Domain; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; +public sealed class SearchProductsSpecs : EntitiesByPaginationFilterSpec +{ + public SearchProductsSpecs(SearchProductsRequest request) + : base(request) => + Query + .OrderBy(c => c.Name, !request.HasOrderBy()) + .Where(p => p.Price >= request.MinimumRate!.Value, request.MinimumRate.HasValue) + .Where(p => p.Price <= request.MaximumRate!.Value, request.MaximumRate.HasValue); +} diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/CatalogModule.cs b/src/api/modules/Catalog/Catalog.Infrastructure/CatalogModule.cs index e9f3a4eec1..0fbdf06855 100644 --- a/src/api/modules/Catalog/Catalog.Infrastructure/CatalogModule.cs +++ b/src/api/modules/Catalog/Catalog.Infrastructure/CatalogModule.cs @@ -18,11 +18,14 @@ public Endpoints() : base("catalog") { } public override void AddRoutes(IEndpointRouteBuilder app) { var productGroup = app.MapGroup("products").WithTags("products"); - productGroup.MapProductCreationEndpoint(); + productGroup.MapProductCreateEndpoint(); productGroup.MapGetProductEndpoint(); - productGroup.MapGetProductListEndpoint(); + productGroup.MapGetProductsEndpoint(); + productGroup.MapSearchProductsEndpoint(); productGroup.MapProductUpdateEndpoint(); productGroup.MapProductDeleteEndpoint(); + productGroup.MapExportProductsEndpoint(); + productGroup.MapImportProductsEndpoint(); } } public static WebApplicationBuilder RegisterCatalogServices(this WebApplicationBuilder builder) diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/CreateProductEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/CreateProductEndpoint.cs index 1e018c0ed8..cd04be2c7d 100644 --- a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/CreateProductEndpoint.cs +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/CreateProductEndpoint.cs @@ -8,7 +8,7 @@ namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; public static class CreateProductEndpoint { - internal static RouteHandlerBuilder MapProductCreationEndpoint(this IEndpointRouteBuilder endpoints) + internal static RouteHandlerBuilder MapProductCreateEndpoint(this IEndpointRouteBuilder endpoints) { return endpoints .MapPost("/", async (CreateProductCommand request, ISender mediator) => diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/ExportProductsEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/ExportProductsEndpoint.cs new file mode 100644 index 0000000000..d372985eda --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/ExportProductsEndpoint.cs @@ -0,0 +1,32 @@ +using FSH.Framework.Core.Paging; +using FSH.Framework.Infrastructure.Auth.Policy; +using FSH.Starter.WebApi.Catalog.Application.Products.Export.v1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; + +public static class ExportProductsEndpoint +{ + internal static RouteHandlerBuilder MapExportProductsEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints + .MapPost("/export", async Task (ISender mediator, [FromBody] ExportProductsRequest command) => + { + var response = await mediator.Send(command); + + return response; + }) + .WithName(nameof(ExportProductsEndpoint)) + .WithSummary("Exports a list of products") + .WithDescription("Exports a list of products with filtering support") + .Produces () + .RequirePermission("Permissions.Products.Export") + .MapToApiVersion(1); + } +} + diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetProductEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetProductEndpoint.cs index 7fd15eb1f7..e3e7e6d511 100644 --- a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetProductEndpoint.cs +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetProductEndpoint.cs @@ -18,8 +18,8 @@ internal static RouteHandlerBuilder MapGetProductEndpoint(this IEndpointRouteBui }) .WithName(nameof(GetProductEndpoint)) .WithSummary("gets product by id") - .WithDescription("gets prodct by id") - .Produces() + .WithDescription("gets product by id") + .Produces() .RequirePermission("Permissions.Products.View") .MapToApiVersion(1); } diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetProductsEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetProductsEndpoint.cs new file mode 100644 index 0000000000..08ed75929f --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetProductsEndpoint.cs @@ -0,0 +1,30 @@ +using FSH.Framework.Infrastructure.Auth.Policy; +using FSH.Starter.WebApi.Catalog.Application.Products.GetList.v1; +using FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; + +public static class GetProductsEndpoint +{ + internal static RouteHandlerBuilder MapGetProductsEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints + .MapPost("/getlist", async (ISender mediator, [FromBody] GetProductsRequest command) => + { + var response = await mediator.Send(command); + return Results.Ok(response); + }) + .WithName(nameof(GetProductsEndpoint)) + .WithSummary("Gets a list of products") + .WithDescription("Gets a list of products with filtering support") + .Produces>() + .RequirePermission("Permissions.Products.Search") + .MapToApiVersion(1); + } +} + diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/ImportProductsEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/ImportProductsEndpoint.cs new file mode 100644 index 0000000000..f0f84cfce4 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/ImportProductsEndpoint.cs @@ -0,0 +1,31 @@ +using FSH.Framework.Core.DataIO; +using FSH.Framework.Core.Storage.File.Features; +using FSH.Framework.Infrastructure.Auth.Policy; +using FSH.Starter.WebApi.Catalog.Application.Products.Import.v1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; + +public static class ImportProductsEndpoint +{ + internal static RouteHandlerBuilder MapImportProductsEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints + .MapPost("/Import", async (FileUploadCommand uploadFile, bool isUpdate, ISender mediator) => + { + var response = await mediator.Send(new ImportProductsCommand(uploadFile, isUpdate)); + return Results.Ok(response); + + }) + .WithName(nameof(ImportProductsEndpoint)) + .WithSummary("Imports a list of products") + .WithDescription("Imports a list of entities from excel files") + .Produces() + .RequirePermission("Permissions.Products.Import") + .MapToApiVersion(1); + } +} + diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/SearchProductsEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/SearchProductsEndpoint.cs index e3d058006b..31013f74b1 100644 --- a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/SearchProductsEndpoint.cs +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/SearchProductsEndpoint.cs @@ -1,6 +1,5 @@ using FSH.Framework.Core.Paging; using FSH.Framework.Infrastructure.Auth.Policy; -using FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; using FSH.Starter.WebApi.Catalog.Application.Products.Search.v1; using MediatR; using Microsoft.AspNetCore.Builder; @@ -12,19 +11,19 @@ namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; public static class SearchProductsEndpoint { - internal static RouteHandlerBuilder MapGetProductListEndpoint(this IEndpointRouteBuilder endpoints) + internal static RouteHandlerBuilder MapSearchProductsEndpoint(this IEndpointRouteBuilder endpoints) { return endpoints - .MapPost("/search", async (ISender mediator, [FromBody] SearchProductsCommand command) => + .MapPost("/search", async (ISender mediator, [FromBody] SearchProductsRequest command) => { var response = await mediator.Send(command); return Results.Ok(response); }) .WithName(nameof(SearchProductsEndpoint)) - .WithSummary("Gets a list of products") + .WithSummary("Gets a pagination of products") .WithDescription("Gets a list of products with pagination and filtering support") - .Produces>() - .RequirePermission("Permissions.Products.View") + .Produces>() + .RequirePermission("Permissions.Products.Search") .MapToApiVersion(1); } } diff --git a/src/api/modules/Shared/Authorization/FshPermissions.cs b/src/api/modules/Shared/Authorization/FshPermissions.cs index d32a1fef1d..fb36710878 100644 --- a/src/api/modules/Shared/Authorization/FshPermissions.cs +++ b/src/api/modules/Shared/Authorization/FshPermissions.cs @@ -10,6 +10,7 @@ public static class FshAction public const string Update = nameof(Update); public const string Delete = nameof(Delete); public const string Export = nameof(Export); + public const string Import = nameof(Import); public const string Generate = nameof(Generate); public const string Clean = nameof(Clean); public const string UpgradeSubscription = nameof(UpgradeSubscription); @@ -32,11 +33,16 @@ public static class FshResource public static class FshPermissions { private static readonly FshPermission[] allPermissions = - { + { //tenants new("View Tenants", FshAction.View, FshResource.Tenants, IsRoot: true), + new("Search Tenants", FshAction.Search, FshResource.Tenants, IsRoot: true), new("Create Tenants", FshAction.Create, FshResource.Tenants, IsRoot: true), new("Update Tenants", FshAction.Update, FshResource.Tenants, IsRoot: true), + new("Delete Tenants", FshAction.Delete, FshResource.Tenants, IsRoot: true), + new("Export Tenants", FshAction.Export, FshResource.Tenants, IsRoot: true), + new("Import Tenants", FshAction.Import, FshResource.Tenants, IsRoot: true), + new("Upgrade Tenant Subscription", FshAction.UpgradeSubscription, FshResource.Tenants, IsRoot: true), //identity @@ -46,12 +52,19 @@ public static class FshPermissions new("Update Users", FshAction.Update, FshResource.Users), new("Delete Users", FshAction.Delete, FshResource.Users), new("Export Users", FshAction.Export, FshResource.Users), + new("Import Users", FshAction.Import, FshResource.Users), + new("View UserRoles", FshAction.View, FshResource.UserRoles), new("Update UserRoles", FshAction.Update, FshResource.UserRoles), + new("View Roles", FshAction.View, FshResource.Roles), + new("Search Roles", FshAction.Search, FshResource.Roles), new("Create Roles", FshAction.Create, FshResource.Roles), new("Update Roles", FshAction.Update, FshResource.Roles), new("Delete Roles", FshAction.Delete, FshResource.Roles), + new("Export Roles", FshAction.Export, FshResource.Roles), + new("Import Roles", FshAction.Import, FshResource.Roles), + new("View RoleClaims", FshAction.View, FshResource.RoleClaims), new("Update RoleClaims", FshAction.Update, FshResource.RoleClaims), @@ -62,6 +75,7 @@ public static class FshPermissions new("Update Products", FshAction.Update, FshResource.Products), new("Delete Products", FshAction.Delete, FshResource.Products), new("Export Products", FshAction.Export, FshResource.Products), + new("Import Products", FshAction.Import, FshResource.Products), //todos new("View Todos", FshAction.View, FshResource.Todos, IsBasic: true), @@ -69,7 +83,9 @@ public static class FshPermissions new("Create Todos", FshAction.Create, FshResource.Todos), new("Update Todos", FshAction.Update, FshResource.Todos), new("Delete Todos", FshAction.Delete, FshResource.Todos), - + new("Export Todos", FshAction.Export, FshResource.Todos), + new("Import Todos", FshAction.Import, FshResource.Todos), + //audit new("View Audit Trails", FshAction.View, FshResource.AuditTrails), }; diff --git a/src/api/modules/Todo/Features/Create/v1/CreateTodoEndpoint.cs b/src/api/modules/Todo/Features/Create/v1/CreateTodoEndpoint.cs index 7ad7673107..45d93f6e14 100644 --- a/src/api/modules/Todo/Features/Create/v1/CreateTodoEndpoint.cs +++ b/src/api/modules/Todo/Features/Create/v1/CreateTodoEndpoint.cs @@ -8,7 +8,7 @@ namespace FSH.Starter.WebApi.Todo.Features.Create.v1; public static class CreateTodoEndpoint { - internal static RouteHandlerBuilder MapTodoItemCreationEndpoint(this IEndpointRouteBuilder endpoints) + internal static RouteHandlerBuilder MapTodoItemCreateEndpoint(this IEndpointRouteBuilder endpoints) { return endpoints.MapPost("/", async (CreateTodoCommand request, ISender mediator) => { diff --git a/src/api/modules/Todo/Features/Export/v1/ExportTodoListEndpoint.cs b/src/api/modules/Todo/Features/Export/v1/ExportTodoListEndpoint.cs new file mode 100644 index 0000000000..b032bef310 --- /dev/null +++ b/src/api/modules/Todo/Features/Export/v1/ExportTodoListEndpoint.cs @@ -0,0 +1,27 @@ +using FSH.Framework.Core.Paging; +using FSH.Framework.Infrastructure.Auth.Policy; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Todo.Features.Export.v1; + +public static class ExportTodoListEndpoint +{ + internal static RouteHandlerBuilder MapExportTodoListEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints.MapPost("/export", async (ISender mediator, [FromBody] BaseFilter filter) => + { + var response = await mediator.Send(new ExportTodoListRequest(filter)); + return Results.Ok(response); + }) + .WithName(nameof(ExportTodoListEndpoint)) + .WithSummary("Exports a list of todo items") + .WithDescription("Gets a list of todo items with filtering support") + .Produces () + .RequirePermission("Permissions.Todos.Export") + .MapToApiVersion(1); + } +} diff --git a/src/api/modules/Todo/Features/Export/v1/ExportTodoListHandler.cs b/src/api/modules/Todo/Features/Export/v1/ExportTodoListHandler.cs new file mode 100644 index 0000000000..48c8e0967a --- /dev/null +++ b/src/api/modules/Todo/Features/Export/v1/ExportTodoListHandler.cs @@ -0,0 +1,28 @@ +using FSH.Framework.Core.DataIO; +using FSH.Framework.Core.Persistence; +using FSH.Framework.Core.Specifications; +using FSH.Starter.WebApi.Todo.Domain; +using FSH.Starter.WebApi.Todo.Features.Search.v1; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace FSH.Starter.WebApi.Todo.Features.Export.v1; + +public class ExportTodoListHandler( + [FromKeyedServices("todo")] IReadRepository repository, IDataExport dataExport) + : IRequestHandler +{ + public async Task Handle(ExportTodoListRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + var spec = new EntitiesByBaseFilterSpec(request.Filter); + + var items = await repository.ListAsync(spec, cancellationToken); + + var response = dataExport.ListToByteArray(items); + + return response; + } + +} diff --git a/src/api/modules/Todo/Features/Export/v1/ExportTodoListRequest.cs b/src/api/modules/Todo/Features/Export/v1/ExportTodoListRequest.cs new file mode 100644 index 0000000000..6854cf5b69 --- /dev/null +++ b/src/api/modules/Todo/Features/Export/v1/ExportTodoListRequest.cs @@ -0,0 +1,5 @@ +using FSH.Framework.Core.Paging; +using MediatR; + +namespace FSH.Starter.WebApi.Todo.Features.Export.v1; +public record ExportTodoListRequest(BaseFilter Filter) : IRequest; diff --git a/src/api/modules/Todo/Features/GetList/v1/GetTodoListEndpoint.cs b/src/api/modules/Todo/Features/GetList/v1/GetTodoListEndpoint.cs index d183c3e33b..b9aac17da9 100644 --- a/src/api/modules/Todo/Features/GetList/v1/GetTodoListEndpoint.cs +++ b/src/api/modules/Todo/Features/GetList/v1/GetTodoListEndpoint.cs @@ -1,5 +1,6 @@ -using FSH.Framework.Core.Paging; +using FSH.Framework.Core.Paging; using FSH.Framework.Infrastructure.Auth.Policy; +using FSH.Starter.WebApi.Todo.Features.Search.v1; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -12,16 +13,18 @@ public static class GetTodoListEndpoint { internal static RouteHandlerBuilder MapGetTodoListEndpoint(this IEndpointRouteBuilder endpoints) { - return endpoints.MapPost("/search", async (ISender mediator, [FromBody] PaginationFilter filter) => - { - var response = await mediator.Send(new GetTodoListRequest(filter)); - return Results.Ok(response); - }) - .WithName(nameof(GetTodoListEndpoint)) - .WithSummary("Gets a list of todo items with paging support") - .WithDescription("Gets a list of todo items with paging support") - .Produces>() - .RequirePermission("Permissions.Todos.View") - .MapToApiVersion(1); + return endpoints + .MapPost("/getlist", async (ISender mediator, [FromBody] BaseFilter filter) => + { + var response = await mediator.Send(new GetTodoListRequest(filter)); + return Results.Ok(response); + }) + .WithName(nameof(GetTodoListEndpoint)) + .WithSummary("Gets a list of todo") + .WithDescription("Gets a list of todo with filtering support") + .Produces>() + .RequirePermission("Permissions.Todos.Search") + .MapToApiVersion(1); } } + diff --git a/src/api/modules/Todo/Features/GetList/v1/GetTodoListHandler.cs b/src/api/modules/Todo/Features/GetList/v1/GetTodoListHandler.cs index 960a3a7aad..13d27e09ef 100644 --- a/src/api/modules/Todo/Features/GetList/v1/GetTodoListHandler.cs +++ b/src/api/modules/Todo/Features/GetList/v1/GetTodoListHandler.cs @@ -1,25 +1,22 @@ -using FSH.Framework.Core.Paging; using FSH.Framework.Core.Persistence; using FSH.Framework.Core.Specifications; using FSH.Starter.WebApi.Todo.Domain; +using FSH.Starter.WebApi.Todo.Features.Search.v1; using MediatR; using Microsoft.Extensions.DependencyInjection; namespace FSH.Starter.WebApi.Todo.Features.GetList.v1; -public sealed class GetTodoListHandler( +public class GetTodoListHandler( [FromKeyedServices("todo")] IReadRepository repository) - : IRequestHandler> + : IRequestHandler> { - public async Task> Handle(GetTodoListRequest request, CancellationToken cancellationToken) + public async Task> Handle(GetTodoListRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); - var spec = new EntitiesByPaginationFilterSpec(request.filter); + var spec = new EntitiesByBaseFilterSpec(request.Filter); - var items = await repository.ListAsync(spec, cancellationToken).ConfigureAwait(false); - var totalCount = await repository.CountAsync(spec, cancellationToken).ConfigureAwait(false); - - return new PagedList(items, request.filter.PageNumber, request.filter.PageSize, totalCount); + return await repository.ListAsync(spec, cancellationToken); } } diff --git a/src/api/modules/Todo/Features/GetList/v1/GetTodoListRequest.cs b/src/api/modules/Todo/Features/GetList/v1/GetTodoListRequest.cs index 84bd7b6799..abc48534de 100644 --- a/src/api/modules/Todo/Features/GetList/v1/GetTodoListRequest.cs +++ b/src/api/modules/Todo/Features/GetList/v1/GetTodoListRequest.cs @@ -1,5 +1,7 @@ -using FSH.Framework.Core.Paging; +using FSH.Framework.Core.Paging; +using FSH.Starter.WebApi.Todo.Features.Search.v1; using MediatR; namespace FSH.Starter.WebApi.Todo.Features.GetList.v1; -public record GetTodoListRequest(PaginationFilter filter) : IRequest>; + +public record GetTodoListRequest(BaseFilter Filter) : IRequest>; diff --git a/src/api/modules/Todo/Features/Import/v1/ImportTodoListCommand.cs b/src/api/modules/Todo/Features/Import/v1/ImportTodoListCommand.cs new file mode 100644 index 0000000000..739cf9b3a9 --- /dev/null +++ b/src/api/modules/Todo/Features/Import/v1/ImportTodoListCommand.cs @@ -0,0 +1,7 @@ +using FSH.Framework.Core.DataIO; +using FSH.Framework.Core.Storage.File.Features; +using MediatR; + +namespace FSH.Starter.WebApi.Todo.Features.Import.v1; + +public record ImportTodoListCommand(FileUploadCommand UploadFile, bool IsUpdate ) : IRequest; diff --git a/src/api/modules/Todo/Features/Import/v1/ImportTodoListEndpoint.cs b/src/api/modules/Todo/Features/Import/v1/ImportTodoListEndpoint.cs new file mode 100644 index 0000000000..d048a94ded --- /dev/null +++ b/src/api/modules/Todo/Features/Import/v1/ImportTodoListEndpoint.cs @@ -0,0 +1,30 @@ +using FSH.Framework.Core.DataIO; +using FSH.Framework.Core.Storage.File.Features; +using FSH.Framework.Infrastructure.Auth.Policy; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Todo.Features.Import.v1; + +public static class ImportTodolistEndpoint +{ + internal static RouteHandlerBuilder MapImportTodoListEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints + .MapPost("/Import", async (FileUploadCommand uploadFile, bool isUpdate, ISender mediator) => + { + var response = await mediator.Send(new ImportTodoListCommand(uploadFile, isUpdate)); + return Results.Ok(response); + + }) + .WithName(nameof(ImportTodolistEndpoint)) + .WithSummary("Imports a list of Todo") + .WithDescription("Imports a list of entities from excel files") + .Produces() + .RequirePermission("Permissions.Todos.Import") + .MapToApiVersion(1); + } +} + diff --git a/src/api/modules/Todo/Features/Import/v1/ImportTodoListHandler.cs b/src/api/modules/Todo/Features/Import/v1/ImportTodoListHandler.cs new file mode 100644 index 0000000000..a5c3db6846 --- /dev/null +++ b/src/api/modules/Todo/Features/Import/v1/ImportTodoListHandler.cs @@ -0,0 +1,53 @@ +using FSH.Framework.Core.DataIO; +using FSH.Framework.Core.Persistence; +using FSH.Framework.Core.Storage.File; +using FSH.Starter.WebApi.Todo.Domain; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace FSH.Starter.WebApi.Todo.Features.Import.v1; + +public class ImportTodoListHandler( + [FromKeyedServices("todo")] IRepository repository, IDataImport dataImport) + : IRequestHandler +{ + public async Task Handle(ImportTodoListCommand request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + var items = await dataImport.ToListAsync(request.UploadFile, FileType.Excel); + + ImportResponse response = new() + { + TotalRecords = items.Count, + Message = "" + }; + + if (response.TotalRecords <= 0) + { + response.Message = "File is empty or Invalid format"; + return response; + } + + try + { + if (request.IsUpdate) + { + await repository.UpdateRangeAsync(items, cancellationToken); + response.Message = " Updated successful"; + } + else + { + await repository.AddRangeAsync (items, cancellationToken); + response.Message = "Added successful"; + } + } + catch (Exception) + { + response.Message = "Internal error!"; + // throw new CustomException("Internal error!") + } + + return response; + } +} diff --git a/src/api/modules/Todo/Features/Search/v1/SearchTodoListEndpoint.cs b/src/api/modules/Todo/Features/Search/v1/SearchTodoListEndpoint.cs new file mode 100644 index 0000000000..29cdbc1af9 --- /dev/null +++ b/src/api/modules/Todo/Features/Search/v1/SearchTodoListEndpoint.cs @@ -0,0 +1,27 @@ +using FSH.Framework.Core.Paging; +using FSH.Framework.Infrastructure.Auth.Policy; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Todo.Features.Search.v1; + +public static class SearchTodoListEndpoint +{ + internal static RouteHandlerBuilder MapSearchTodoListEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints.MapPost("/search", async (ISender mediator, [FromBody] PaginationFilter filter) => + { + var response = await mediator.Send(new SearchTodoListRequest(filter)); + return Results.Ok(response); + }) + .WithName(nameof(SearchTodoListEndpoint)) + .WithSummary("Gets a list of todo items with paging support") + .WithDescription("Gets a list of todo items with paging support") + .Produces>() + .RequirePermission("Permissions.Todos.View") + .MapToApiVersion(1); + } +} diff --git a/src/api/modules/Todo/Features/Search/v1/SearchTodoListHandler.cs b/src/api/modules/Todo/Features/Search/v1/SearchTodoListHandler.cs new file mode 100644 index 0000000000..09fb2a5677 --- /dev/null +++ b/src/api/modules/Todo/Features/Search/v1/SearchTodoListHandler.cs @@ -0,0 +1,25 @@ +using FSH.Framework.Core.Paging; +using FSH.Framework.Core.Persistence; +using FSH.Framework.Core.Specifications; +using FSH.Starter.WebApi.Todo.Domain; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace FSH.Starter.WebApi.Todo.Features.Search.v1; + +public sealed class SearchTodoListHandler( + [FromKeyedServices("todo")] IReadRepository repository) + : IRequestHandler> +{ + public async Task> Handle(SearchTodoListRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + var spec = new EntitiesByPaginationFilterSpec(request.Filter); + + var items = await repository.ListAsync(spec, cancellationToken).ConfigureAwait(false); + var totalCount = await repository.CountAsync(spec, cancellationToken).ConfigureAwait(false); + + return new PagedList(items, request.Filter.PageNumber, request.Filter.PageSize, totalCount); + } +} diff --git a/src/api/modules/Todo/Features/Search/v1/SearchTodoListRequest.cs b/src/api/modules/Todo/Features/Search/v1/SearchTodoListRequest.cs new file mode 100644 index 0000000000..d23a1d2ee3 --- /dev/null +++ b/src/api/modules/Todo/Features/Search/v1/SearchTodoListRequest.cs @@ -0,0 +1,5 @@ +using FSH.Framework.Core.Paging; +using MediatR; + +namespace FSH.Starter.WebApi.Todo.Features.Search.v1; +public record SearchTodoListRequest(PaginationFilter Filter) : IRequest>; diff --git a/src/api/modules/Todo/Features/GetList/v1/TodoDto.cs b/src/api/modules/Todo/Features/Search/v1/TodoDto.cs similarity index 50% rename from src/api/modules/Todo/Features/GetList/v1/TodoDto.cs rename to src/api/modules/Todo/Features/Search/v1/TodoDto.cs index 869d34eb99..c9e646c3e7 100644 --- a/src/api/modules/Todo/Features/GetList/v1/TodoDto.cs +++ b/src/api/modules/Todo/Features/Search/v1/TodoDto.cs @@ -1,2 +1,2 @@ -namespace FSH.Starter.WebApi.Todo.Features.GetList.v1; +namespace FSH.Starter.WebApi.Todo.Features.Search.v1; public record TodoDto(Guid? Id, string Title, string Note); diff --git a/src/api/modules/Todo/TodoModule.cs b/src/api/modules/Todo/TodoModule.cs index 558d9526f0..abbeb3ed75 100644 --- a/src/api/modules/Todo/TodoModule.cs +++ b/src/api/modules/Todo/TodoModule.cs @@ -4,8 +4,11 @@ using FSH.Starter.WebApi.Todo.Domain; using FSH.Starter.WebApi.Todo.Features.Create.v1; using FSH.Starter.WebApi.Todo.Features.Delete.v1; +using FSH.Starter.WebApi.Todo.Features.Export.v1; using FSH.Starter.WebApi.Todo.Features.Get.v1; using FSH.Starter.WebApi.Todo.Features.GetList.v1; +using FSH.Starter.WebApi.Todo.Features.Import.v1; +using FSH.Starter.WebApi.Todo.Features.Search.v1; using FSH.Starter.WebApi.Todo.Features.Update.v1; using FSH.Starter.WebApi.Todo.Persistence; using Microsoft.AspNetCore.Builder; @@ -22,11 +25,14 @@ public class Endpoints : CarterModule public override void AddRoutes(IEndpointRouteBuilder app) { var todoGroup = app.MapGroup("todos").WithTags("todos"); - todoGroup.MapTodoItemCreationEndpoint(); + todoGroup.MapTodoItemCreateEndpoint(); todoGroup.MapGetTodoEndpoint(); todoGroup.MapGetTodoListEndpoint(); + todoGroup.MapSearchTodoListEndpoint(); todoGroup.MapTodoItemUpdationEndpoint(); todoGroup.MapTodoItemDeletionEndpoint(); + todoGroup.MapExportTodoListEndpoint(); + todoGroup.MapImportTodoListEndpoint(); } } public static WebApplicationBuilder RegisterTodoServices(this WebApplicationBuilder builder) diff --git a/src/api/server/Server.csproj b/src/api/server/Server.csproj index d985782327..4bfe8f355e 100644 --- a/src/api/server/Server.csproj +++ b/src/api/server/Server.csproj @@ -23,9 +23,6 @@ - - Always - Always diff --git a/src/api/server/appsettings.Development.json b/src/api/server/appsettings.Development.json deleted file mode 100644 index e4ff07d0c1..0000000000 --- a/src/api/server/appsettings.Development.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "DatabaseOptions": { - "Provider": "postgresql", - "ConnectionString": "Server=192.168.1.110;Database=fullstackherodb;User Id=postgres;Password=password" - }, - "OriginOptions": { - "OriginUrl": "https://localhost:7000" - }, - "CacheOptions": { - "Redis": "" - }, - "HangfireOptions": { - "Username": "admin", - "Password": "Secure1234!Me", - "Route": "/jobs" - }, - "JwtOptions": { - "Key": "QsJbczCNysv/5SGh+U7sxedX8C07TPQPBdsnSDKZ/aE=", - "TokenExpirationInMinutes": 60, - "RefreshTokenExpirationInDays": 7 - }, - "MailOptions": { - "From": "mukesh@fullstackhero.net", - "Host": "smtp.ethereal.email", - "Port": 587, - "UserName": "sherman.oconnell47@ethereal.email", - "Password": "KbuTCFv4J6Fy7256vh", - "DisplayName": "Mukesh Murugan" - }, - "CorsOptions": { - "AllowedOrigins": [ - "https://localhost:7100", - "http://localhost:7100", - "http://localhost:5010" - ] - }, - "Serilog": { - "Using": [ - "Serilog.Sinks.Console" - ], - "MinimumLevel": { - "Default": "Debug" - }, - "WriteTo": [ - { - "Name": "Console" - } - ] - }, - "RateLimitOptions": { - "EnableRateLimiting": false, - "PermitLimit": 5, - "WindowInSeconds": 10, - "RejectionStatusCode": 429 - }, - "SecurityHeaderOptions": { - "Enable": true, - "Headers": { - "XContentTypeOptions": "nosniff", - "ReferrerPolicy": "no-referrer", - "XXSSProtection": "1; mode=block", - "XFrameOptions": "DENY", - "ContentSecurityPolicy": "block-all-mixed-content; style-src 'self' 'unsafe-inline'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline'", - "PermissionsPolicy": "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()", - "StrictTransportSecurity": "max-age=31536000" - } - } -} \ No newline at end of file diff --git a/src/api/server/appsettings.json b/src/api/server/appsettings.json index 76c1d234b6..0d7463514b 100644 --- a/src/api/server/appsettings.json +++ b/src/api/server/appsettings.json @@ -1,7 +1,11 @@ { +// "DatabaseOptions": { +// "Provider": "postgresql", +// "ConnectionString": "Server=localhost;Database=fullstackhero;Port=5432;User Id=pgadmin;Password=123DBP@ssw0rd;" +// }, "DatabaseOptions": { - "Provider": "postgresql", - "ConnectionString": "Server=localhost;Database=fullstackhero;Port=5433;User Id=pgadmin;Password=pgadmin;" + "Provider": "mssql", + "ConnectionString": "Server=localhost,1433;Database=FshDb2;User Id=sa;Password=123DBP@ssw0rd;TrustServerCertificate=True" }, "OriginOptions": { "OriginUrl": "https://localhost:7000" diff --git a/src/apps/blazor/client/Client.csproj b/src/apps/blazor/client/Client.csproj index e8bbccfb68..9e869bb444 100644 --- a/src/apps/blazor/client/Client.csproj +++ b/src/apps/blazor/client/Client.csproj @@ -13,6 +13,7 @@ + diff --git a/src/apps/blazor/client/Components/Dialogs/DeleteConfirmation.razor b/src/apps/blazor/client/Components/Dialogs/DeleteConfirmation.razor index da39f64132..49c3c82736 100644 --- a/src/apps/blazor/client/Components/Dialogs/DeleteConfirmation.razor +++ b/src/apps/blazor/client/Components/Dialogs/DeleteConfirmation.razor @@ -10,7 +10,7 @@ Cancel - Confirm + Confirm diff --git a/src/apps/blazor/client/Components/Dialogs/DialogComfirmation.razor b/src/apps/blazor/client/Components/Dialogs/DialogComfirmation.razor new file mode 100644 index 0000000000..50da1f60bb --- /dev/null +++ b/src/apps/blazor/client/Components/Dialogs/DialogComfirmation.razor @@ -0,0 +1,46 @@ + + + + + @TitleText + + + + @ContentText + + + Cance + @ButtonText + + + +@code { + + [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; + + /// + /// + /// + [Parameter] public string? ContentText { get; set; } + /// + /// + /// + [Parameter] public string? ButtonText { get; set; } + /// + /// + /// + [Parameter] public Color ButtonColor { get; set; } + + /// + /// + /// + [Parameter] public string? TitleIcon { get; set; } + /// + /// + /// + [Parameter] public string? TitleText { get; set; } + + private void Submit() => MudDialog.Close(DialogResult.Ok(true)); + private void Cancel() => MudDialog.Cancel(); + +} \ No newline at end of file diff --git a/src/apps/blazor/client/Components/Dialogs/DialogNotification.razor b/src/apps/blazor/client/Components/Dialogs/DialogNotification.razor new file mode 100644 index 0000000000..73eeea7080 --- /dev/null +++ b/src/apps/blazor/client/Components/Dialogs/DialogNotification.razor @@ -0,0 +1,28 @@ + + + + + @TitleText + + + + @ContentText + + + @ButtonText + + + +@code { + +[CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; + +[Parameter] public string? ContentText { get; set; } +[Parameter] public string? ButtonText { get; set; } +[Parameter] public Color ButtonColor { get; set; } + +[Parameter] public string? TitleIcon { get; set; } +[Parameter] public string? TitleText { get; set; } + +private void Submit() => MudDialog.Close(DialogResult.Ok(true)); +} \ No newline at end of file diff --git a/src/apps/blazor/client/Components/Dialogs/FileUpload.razor b/src/apps/blazor/client/Components/Dialogs/FileUpload.razor new file mode 100644 index 0000000000..3096518f61 --- /dev/null +++ b/src/apps/blazor/client/Components/Dialogs/FileUpload.razor @@ -0,0 +1,68 @@ + + + + + File Upload + + + + @ContentText + + + Cancel + Choose File + + @if (file != null) + { + Upload + } + else + { + Upload] + } + + + +@code +{ + [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; + [Parameter] public string? ContentText { get; set; } + [Parameter] public string? MimeType { get; set; } + public IBrowserFile? file { get; set; } + + void Cancel() => MudDialog.Cancel(); + private async Task Submit() + { if(file != null) + { + var buffer = new byte[file.Size]; + await file.OpenReadStream(file.Size).ReadAsync(buffer); + MudDialog.Close(DialogResult.Ok (buffer)); + } + } + + private void OnInputFileChange(InputFileChangeEventArgs e) + { + if (e.File.Size >=512000) + { + Toast.Add("File have size too big!", Severity.Error); + file = null; + } + else + { + file = e.File; + } + } +} diff --git a/src/apps/blazor/client/Components/EntityTable/AddEditModal.razor b/src/apps/blazor/client/Components/EntityTable/AddEditModal.razor index 4d18b0f3c5..d1ff8cb6be 100644 --- a/src/apps/blazor/client/Components/EntityTable/AddEditModal.razor +++ b/src/apps/blazor/client/Components/EntityTable/AddEditModal.razor @@ -28,18 +28,18 @@ - + Cancel @if (IsCreate) { - + Save } else { - + Update } diff --git a/src/apps/blazor/client/Components/EntityTable/AddEditModal.razor.cs b/src/apps/blazor/client/Components/EntityTable/AddEditModal.razor.cs index 7ac2136db8..a69ed8037a 100644 --- a/src/apps/blazor/client/Components/EntityTable/AddEditModal.razor.cs +++ b/src/apps/blazor/client/Components/EntityTable/AddEditModal.razor.cs @@ -28,6 +28,7 @@ public partial class AddEditModal : IAddEditModal private MudDialogInstance MudDialog { get; set; } = default!; private FshValidation? _customValidation; + private bool _buttonStatus; public void ForceRender() => StateHasChanged(); @@ -38,10 +39,12 @@ OnInitializedFunc is not null private async Task SaveAsync() { + _buttonStatus = true; if (await ApiHelper.ExecuteCallGuardedAsync( () => SaveFunc(RequestModel), Toast, _customValidation, SuccessMessage)) { MudDialog.Close(); } + _buttonStatus = false; } -} \ No newline at end of file +} diff --git a/src/apps/blazor/client/Components/EntityTable/EntityClientTableContext.cs b/src/apps/blazor/client/Components/EntityTable/EntityClientTableContext.cs index b9d84fa8e7..acb26f7a88 100644 --- a/src/apps/blazor/client/Components/EntityTable/EntityClientTableContext.cs +++ b/src/apps/blazor/client/Components/EntityTable/EntityClientTableContext.cs @@ -1,4 +1,7 @@ -namespace FSH.Starter.Blazor.Client.Components.EntityTable; +using System.Net; +using FSH.Starter.Blazor.Infrastructure.Api; + +namespace FSH.Starter.Blazor.Client.Components.EntityTable; /// /// Initialization Context for the EntityTable Component. @@ -17,6 +20,16 @@ public class EntityClientTableContext /// (the supplied string is the search string entered). /// public Func SearchFunc { get; } + + /// + /// A function that exports the specified data from the API. + /// + public Func>? ExportFunc { get; } + + /// + /// A function that import the specified data from the API. + /// + public Func>? ImportFunc { get; } public EntityClientTableContext( List> fields, @@ -28,6 +41,8 @@ public EntityClientTableContext( Func>? getDetailsFunc = null, Func? updateFunc = null, Func? deleteFunc = null, + Func>? exportFunc = null, + Func>? importFunc = null, string? entityName = null, string? entityNamePlural = null, string? entityResource = null, @@ -36,7 +51,9 @@ public EntityClientTableContext( string? updateAction = null, string? deleteAction = null, string? exportAction = null, + string? importAction = null, Func? editFormInitializedFunc = null, + Func? importFormInitializedFunc = null, Func? hasExtraActionsFunc = null, Func? canUpdateEntityFunc = null, Func? canDeleteEntityFunc = null) @@ -56,12 +73,16 @@ public EntityClientTableContext( updateAction, deleteAction, exportAction, + importAction, editFormInitializedFunc, + importFormInitializedFunc, hasExtraActionsFunc, canUpdateEntityFunc, canDeleteEntityFunc) { LoadDataFunc = loadDataFunc; SearchFunc = searchFunc; + ExportFunc = exportFunc; + ImportFunc = importFunc; } -} \ No newline at end of file +} diff --git a/src/apps/blazor/client/Components/EntityTable/EntityServerTableContext.cs b/src/apps/blazor/client/Components/EntityTable/EntityServerTableContext.cs index c2fb79527a..9a69babf9f 100644 --- a/src/apps/blazor/client/Components/EntityTable/EntityServerTableContext.cs +++ b/src/apps/blazor/client/Components/EntityTable/EntityServerTableContext.cs @@ -14,6 +14,16 @@ public class EntityServerTableContext /// and returns a PaginatedResult of TEntity. /// public Func>> SearchFunc { get; } + + /// + /// A function that exports the specified data from the API. + /// + public Func>? ExportFunc { get; } + + /// + /// A function that import the specified data from the API. + /// + public Func>? ImportFunc { get; } public bool EnableAdvancedSearch { get; } @@ -27,6 +37,8 @@ public EntityServerTableContext( Func>? getDetailsFunc = null, Func? updateFunc = null, Func? deleteFunc = null, + Func>? exportFunc = null, + Func>? importFunc = null, string? entityName = null, string? entityNamePlural = null, string? entityResource = null, @@ -35,7 +47,9 @@ public EntityServerTableContext( string? updateAction = null, string? deleteAction = null, string? exportAction = null, + string? importAction = null, Func? editFormInitializedFunc = null, + Func? importFormInitializedFunc = null, Func? hasExtraActionsFunc = null, Func? canUpdateEntityFunc = null, Func? canDeleteEntityFunc = null) @@ -55,12 +69,16 @@ public EntityServerTableContext( updateAction, deleteAction, exportAction, + importAction, editFormInitializedFunc, + importFormInitializedFunc, hasExtraActionsFunc, canUpdateEntityFunc, canDeleteEntityFunc) { SearchFunc = searchFunc; + ExportFunc = exportFunc; + ImportFunc = importFunc; EnableAdvancedSearch = enableAdvancedSearch; } -} \ No newline at end of file +} diff --git a/src/apps/blazor/client/Components/EntityTable/EntityTable.razor b/src/apps/blazor/client/Components/EntityTable/EntityTable.razor index cad1b9b48e..423ad2ce35 100644 --- a/src/apps/blazor/client/Components/EntityTable/EntityTable.razor +++ b/src/apps/blazor/client/Components/EntityTable/EntityTable.razor @@ -1,8 +1,9 @@ @typeparam TEntity @typeparam TId +@using FSH.Starter.Blazor.Shared @typeparam TRequest -@inject IJSRuntime JS +@inject IJSRuntime Js