Finished converter plugin

This commit is contained in:
2025-02-28 12:15:32 +01:00
parent 6c42008a28
commit 86ace64618
14 changed files with 231 additions and 90 deletions

View File

@@ -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)) {

View File

@@ -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()

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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()

View 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();
}

View 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;
}
}