Finished converter plugin
This commit is contained in:
@@ -7,4 +7,5 @@ public interface IContextExplorer {
|
||||
public TableConfig? GetTable(string tableDisplayName);
|
||||
public TableConfig? GetTable(Type tableEntity);
|
||||
public ITableManager? GetTableManager(string tablePropertyName);
|
||||
public ITableManager? GetTableManager(Type tableType);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public interface ITableManager {
|
||||
public Task EditItem(object item);
|
||||
public Task AddItem(object item);
|
||||
public Task AddAll(IEnumerable<object> items);
|
||||
public Task RevertChanges(object item);
|
||||
public Task<object?> GetOne(object key);
|
||||
|
||||
public Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null);
|
||||
}
|
||||
@@ -55,6 +55,21 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
return null;
|
||||
}
|
||||
|
||||
public ITableManager? GetTableManager(Type tableType) {
|
||||
foreach (var context in config.Contexts) {
|
||||
var table = context.Tables.FirstOrDefault(table => table.TableType == tableType);
|
||||
if (table is null) continue;
|
||||
|
||||
var dbContext = provider.GetService(context.ContextType) as DbContext;
|
||||
if (dbContext is null) return null;
|
||||
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
return Activator.CreateInstance(type, dbContext, table, this, provider) as ITableManager;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SeedTableData(TableConfig table) {
|
||||
if (table.Seeded) return;
|
||||
var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!;
|
||||
|
||||
@@ -55,6 +55,11 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<object?> GetOne(object key) {
|
||||
var table = context.Set<TModel>();
|
||||
return await table.FindAsync(key);
|
||||
}
|
||||
|
||||
public async Task RevertChanges(object item) {
|
||||
var entry = context.Entry((TModel)item);
|
||||
await entry.ReloadAsync();
|
||||
|
||||
@@ -202,7 +202,10 @@
|
||||
private List<PluginButton> _pluginButtons = new();
|
||||
private DefaultButtonToggles _buttonToggles = new();
|
||||
|
||||
internal static HopFrameTablePage? CurrentInstance { get; private set; }
|
||||
|
||||
protected override void OnInitialized() {
|
||||
CurrentInstance = this;
|
||||
_config ??= Explorer.GetTable(TableDisplayName);
|
||||
|
||||
if (_config is null || (_config.Ignored && DialogData is null)) {
|
||||
|
||||
@@ -38,7 +38,7 @@ public static class HopFrameConfiguratorExtensions {
|
||||
/// </summary>
|
||||
/// <param name="configurator">The configurator for the HopFrame config that is being created</param>
|
||||
/// <typeparam name="TPlugin">The plugin that should be registered</typeparam>
|
||||
public static HopFrameConfigurator AddPlugin<TPlugin>(this HopFrameConfigurator configurator) where TPlugin : HopFramePlugin {
|
||||
public static HopFrameConfigurator AddPlugin<TPlugin>(this HopFrameConfigurator configurator) where TPlugin : class {
|
||||
PluginOrchestrator.RegisterPlugin(configurator.ServiceCollection, typeof(TPlugin));
|
||||
|
||||
var methods = typeof(TPlugin).GetMethods()
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace HopFrame.Web.Plugins;
|
||||
|
||||
public abstract class HopFramePlugin {
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a file using a helper js function
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file</param>
|
||||
/// <param name="data">The content of the file</param>
|
||||
/// <param name="runtime">The js reference for invoking the function (Injectable)</param>
|
||||
protected async Task DownloadFile(string fileName, byte[] data, IJSRuntime runtime) {
|
||||
using var stream = new DotNetStreamReference(new MemoryStream(data));
|
||||
|
||||
await runtime.InvokeVoidAsync("downloadFileFromStream", fileName, stream);
|
||||
}
|
||||
|
||||
protected Task<IBrowserFile> UploadFile(HopFrameTablePage targetPage, IJSRuntime runtime) {
|
||||
var result = new TaskCompletionSource<IBrowserFile>();
|
||||
|
||||
targetPage.OnFileUpload = files => {
|
||||
result.SetResult(files.First());
|
||||
targetPage.OnFileUpload = null;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
runtime.InvokeVoidAsync("triggerClick", targetPage.FileInputElement!.Element);
|
||||
return result.Task;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
using System.Text;
|
||||
using System.Collections;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Services;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using HopFrame.Web.Plugins.Annotations;
|
||||
using HopFrame.Web.Plugins.Events;
|
||||
using HopFrame.Web.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Internal;
|
||||
|
||||
internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runtime, IToastService toasts, IServiceProvider provider) : HopFramePlugin {
|
||||
|
||||
internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService toasts, IFileService files, IServiceProvider provider) {
|
||||
public const char Separator = ';';
|
||||
|
||||
[EventHandler]
|
||||
@@ -31,32 +32,30 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runti
|
||||
.LoadPage(0, int.MaxValue)
|
||||
.ToArrayAsync();
|
||||
|
||||
var properties = table.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order).ToArray();
|
||||
|
||||
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).ToArray();
|
||||
|
||||
var csv = new StringBuilder(string.Join(Separator, properties.Select(prop => prop.Name)) + '\n');
|
||||
var csv = new StringBuilder(string.Join(Separator, properties.Select(prop => prop.Info.Name)) + '\n');
|
||||
foreach (var entry in data) {
|
||||
var row = new List<string>();
|
||||
|
||||
foreach (var property in properties) {
|
||||
var value = await manager.DisplayProperty(entry, property);
|
||||
row.Add(value);
|
||||
row.Add(FormatProperty(property, entry));
|
||||
}
|
||||
|
||||
csv.Append(string.Join(Separator, row) + '\n');
|
||||
}
|
||||
|
||||
var result = csv.ToString();
|
||||
await DownloadFile($"{table.DisplayName}.csv", Encoding.UTF8.GetBytes(result), runtime);
|
||||
await files.DownloadFile($"{table.DisplayName}.csv", Encoding.UTF8.GetBytes(result));
|
||||
}
|
||||
|
||||
private async Task Import(TableConfig table, HopFrameTablePage target) {
|
||||
var file = await UploadFile(target, runtime);
|
||||
var file = await files.UploadFile();
|
||||
|
||||
var stream = file.OpenReadStream();
|
||||
var reader = new StreamReader(stream);
|
||||
|
||||
var properties = table.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order).ToArray();
|
||||
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).ToArray();
|
||||
var data = await reader.ReadToEndAsync();
|
||||
var rows = data.Split('\n');
|
||||
|
||||
@@ -64,7 +63,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runti
|
||||
await stream.DisposeAsync();
|
||||
|
||||
var headerProps = rows.First().Split(Separator);
|
||||
if (!headerProps.Any(h => properties.Any(prop => prop.Name == h))) {
|
||||
if (!headerProps.Any(h => properties.Any(prop => prop.Info.Name == h))) {
|
||||
toasts.ShowError("Table header in csv is not valid!");
|
||||
return;
|
||||
}
|
||||
@@ -78,22 +77,57 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runti
|
||||
|
||||
var rowValues = row.Split(Separator);
|
||||
for (int i = 0; i < headerProps.Length; i++) {
|
||||
var property = properties.FirstOrDefault(prop => prop.Name == headerProps[i]);
|
||||
var property = properties.FirstOrDefault(prop => prop.Info.Name == headerProps[i]);
|
||||
if (property is null) continue;
|
||||
if (property.IsEnumerable) continue;
|
||||
|
||||
object value = rowValues[i];
|
||||
object? value = rowValues[i];
|
||||
|
||||
if (property.Info.PropertyType == typeof(Guid)) {
|
||||
if (property.IsEnumerable) {
|
||||
if (!property.Info.PropertyType.IsGenericType) continue;
|
||||
|
||||
var formattedEnumerable = (string)value;
|
||||
if (formattedEnumerable == "[]") continue;
|
||||
var values = formattedEnumerable
|
||||
.TrimStart('[')
|
||||
.TrimEnd(']')
|
||||
.Split(',');
|
||||
|
||||
var addMethod = property.Info.PropertyType.GetMethod("Add");
|
||||
if (addMethod is null) continue;
|
||||
|
||||
var tableType = property.Info.PropertyType.GenericTypeArguments[0];
|
||||
var relationManager = explorer.GetTableManager(tableType);
|
||||
var primaryKeyType = GetPrimaryKeyType(tableType);
|
||||
if (relationManager is null || primaryKeyType is null) continue;
|
||||
|
||||
var enumerable = Activator.CreateInstance(property.Info.PropertyType);
|
||||
foreach (var key in values) {
|
||||
var entry = await relationManager.GetOne(ParseString(key, primaryKeyType));
|
||||
if (entry is null) continue;
|
||||
|
||||
addMethod.Invoke(enumerable, [entry]);
|
||||
}
|
||||
|
||||
property.Info.SetValue(element, enumerable);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.IsRelation) {
|
||||
var relationManager = explorer.GetTableManager(property.Info.PropertyType);
|
||||
var relationPrimaryKeyType = GetPrimaryKeyType(property.Info.PropertyType);
|
||||
if (relationManager is null || relationPrimaryKeyType is null) continue;
|
||||
value = await relationManager.GetOne(ParseString((string)value, relationPrimaryKeyType));
|
||||
}
|
||||
else if (property.Info.PropertyType == typeof(Guid)) {
|
||||
var success = Guid.TryParse((string)value, out var guid);
|
||||
if (success) value = guid;
|
||||
else toasts.ShowError($"'{value}' is not a valid guid");
|
||||
}
|
||||
|
||||
if (property.Parser is not null) {
|
||||
value = await property.Parser(value.ToString()!, provider);
|
||||
else {
|
||||
value = ParseString((string)value, property.Info.PropertyType);
|
||||
}
|
||||
property.SetValue(element, value, provider);
|
||||
|
||||
property.Info.SetValue(element, value);
|
||||
}
|
||||
|
||||
elements.Add(element);
|
||||
@@ -101,12 +135,63 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runti
|
||||
|
||||
var manager = explorer.GetTableManager(table.PropertyName);
|
||||
if (manager is null) {
|
||||
toasts.ShowError("Data could not be exported!");
|
||||
toasts.ShowError("Data could not be imported!");
|
||||
return;
|
||||
}
|
||||
|
||||
await manager.AddAll(elements);
|
||||
await target.Reload();
|
||||
}
|
||||
|
||||
private string FormatProperty(PropertyConfig property, object entity) {
|
||||
var value = property.Info.GetValue(entity);
|
||||
|
||||
if (value is null)
|
||||
return string.Empty;
|
||||
|
||||
if (property.IsEnumerable) {
|
||||
var enumerable = (IEnumerable)value;
|
||||
return '[' + string.Join(',', enumerable.OfType<object>().Select(o => SelectPrimaryKey(o) ?? o.ToString())) + ']';
|
||||
}
|
||||
|
||||
return SelectPrimaryKey(value) ?? value.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private string? SelectPrimaryKey(object entity) {
|
||||
return entity
|
||||
.GetType()
|
||||
.GetProperties()
|
||||
.FirstOrDefault(prop => prop
|
||||
.GetCustomAttributes(true)
|
||||
.Any(attr => attr is KeyAttribute))?
|
||||
.GetValue(entity)?
|
||||
.ToString();
|
||||
}
|
||||
|
||||
private Type? GetPrimaryKeyType(Type tableType) {
|
||||
return tableType
|
||||
.GetProperties()
|
||||
.FirstOrDefault(prop => prop
|
||||
.GetCustomAttributes(true)
|
||||
.Any(attr => attr is KeyAttribute))?
|
||||
.PropertyType;
|
||||
}
|
||||
|
||||
private object? ParseString(string input, Type targetType) {
|
||||
try {
|
||||
var parseMethod = targetType
|
||||
.GetMethods()
|
||||
.Where(method => method.Name.StartsWith("Parse"))
|
||||
.FirstOrDefault(method => method.GetParameters().SingleOrDefault()?.ParameterType == typeof(string));
|
||||
|
||||
if (parseMethod is not null)
|
||||
return parseMethod.Invoke(null, [input]);
|
||||
|
||||
return Convert.ChangeType(input, targetType);
|
||||
}
|
||||
catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using HopFrame.Web.Components;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using HopFrame.Web.Plugins;
|
||||
using HopFrame.Web.Plugins.Internal;
|
||||
using HopFrame.Web.Services;
|
||||
using HopFrame.Web.Services.Implementation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
@@ -41,6 +43,7 @@ public static class ServiceCollectionExtensions {
|
||||
services.AddFluentUIComponents(fluentUiLibraryConfiguration);
|
||||
|
||||
services.AddScoped<IPluginOrchestrator, PluginOrchestrator>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
|
||||
if (addRazorComponents) {
|
||||
services.AddRazorComponents()
|
||||
|
||||
11
src/HopFrame.Web/Services/IFileService.cs
Normal file
11
src/HopFrame.Web/Services/IFileService.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace HopFrame.Web.Services;
|
||||
|
||||
public interface IFileService {
|
||||
|
||||
public Task DownloadFile(string name, byte[] data);
|
||||
|
||||
public Task<IBrowserFile> UploadFile();
|
||||
|
||||
}
|
||||
31
src/HopFrame.Web/Services/Implementation/FileService.cs
Normal file
31
src/HopFrame.Web/Services/Implementation/FileService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace HopFrame.Web.Services.Implementation;
|
||||
|
||||
internal sealed class FileService(IJSRuntime runtime) : IFileService {
|
||||
|
||||
public async Task DownloadFile(string name, byte[] data) {
|
||||
using var stream = new DotNetStreamReference(new MemoryStream(data));
|
||||
|
||||
await runtime.InvokeVoidAsync("downloadFileFromStream", name, stream);
|
||||
}
|
||||
|
||||
public Task<IBrowserFile> UploadFile() {
|
||||
var result = new TaskCompletionSource<IBrowserFile>();
|
||||
|
||||
if (HopFrameTablePage.CurrentInstance is null)
|
||||
result.SetException(new InvalidOperationException("No table page visible"));
|
||||
|
||||
HopFrameTablePage.CurrentInstance!.OnFileUpload = files => {
|
||||
result.SetResult(files.First());
|
||||
HopFrameTablePage.CurrentInstance.OnFileUpload = null;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
runtime.InvokeVoidAsync("triggerClick", HopFrameTablePage.CurrentInstance.FileInputElement!.Element);
|
||||
return result.Task;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user