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
+
+ @if (file != null)
+ {
+
+
+ @file.Name @file.Size bytes
+
+ }
+
+
+ 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