Added basic export and import feature

This commit is contained in:
2025-02-15 13:49:39 +01:00
parent 0262b3b97b
commit 6c42008a28
9 changed files with 220 additions and 19 deletions

View File

@@ -412,7 +412,7 @@
_tokenSource.Dispose();
}
private enum InputType {
public enum InputType {
Number,
Switch,
Date,

View File

@@ -135,8 +135,26 @@
}
removeBg();
window.downloadFileFromStream = async (fileName, contentStreamReference) => {
const arrayBuffer = await contentStreamReference.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
URL.revokeObjectURL(url);
}
window.triggerClick = (elt) => elt.click();
</script>
<FluentToastProvider MaxToastCount="10" />
<InputFile style="display: none" @ref="FileInputElement" OnChange="OnInputFiles"></InputFile>
@inject IContextExplorer Explorer
@inject NavigationManager Navigator
@inject IJSRuntime Js
@@ -254,7 +272,7 @@
await Reload();
}
private async Task Reload() {
public async Task Reload() {
_loading = true;
var eventResult = await PluginOrchestrator.DispatchEvent(new ReloadEvent(this) {
@@ -275,7 +293,7 @@
_loading = false;
}
private async Task ChangePage(int page) {
public async Task ChangePage(int page) {
var eventResult = await PluginOrchestrator.DispatchEvent(new PageChangeEvent(this) {
CurrentPage = _currentPage,
NewPage = page,
@@ -383,4 +401,21 @@
return display;
}
public InputFile? FileInputElement;
public Func<IEnumerable<IBrowserFile>, Task>? OnFileUpload;
private async Task OnInputFiles(InputFileChangeEventArgs e) {
if (OnFileUpload is null) return;
if (e.FileCount == 1) {
await OnFileUpload.Invoke([e.File]);
}
else {
await OnFileUpload.Invoke(e.GetMultipleFiles());
}
}
public void RequestRender() {
StateHasChanged();
}
}

View File

@@ -55,5 +55,10 @@ public static class HopFrameConfiguratorExtensions {
return configurator;
}
public static HopFrameConfigurator AddExporters(this HopFrameConfigurator configurator) {
configurator.AddPlugin<ExporterPlugin>();
return configurator;
}
}

View File

@@ -1,3 +1,34 @@
namespace HopFrame.Web.Plugins;
using HopFrame.Web.Components.Pages;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
public abstract class HopFramePlugin;
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

@@ -0,0 +1,112 @@
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 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 {
public const char Separator = ';';
[EventHandler]
public void OnInit(TableInitializedEvent e) {
e.AddPageButton("Export", () => Export(e.Table), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowUpload());
e.AddPageButton("Import", () => Import(e.Table, e.Sender), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowDownload());
}
private async Task Export(TableConfig table) {
var manager = explorer.GetTableManager(table.PropertyName);
if (manager is null) {
toasts.ShowError("Data could not be exported!");
return;
}
var data = await manager
.LoadPage(0, int.MaxValue)
.ToArrayAsync();
var properties = table.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order).ToArray();
var csv = new StringBuilder(string.Join(Separator, properties.Select(prop => prop.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);
}
csv.Append(string.Join(Separator, row) + '\n');
}
var result = csv.ToString();
await DownloadFile($"{table.DisplayName}.csv", Encoding.UTF8.GetBytes(result), runtime);
}
private async Task Import(TableConfig table, HopFrameTablePage target) {
var file = await UploadFile(target, runtime);
var stream = file.OpenReadStream();
var reader = new StreamReader(stream);
var properties = table.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order).ToArray();
var data = await reader.ReadToEndAsync();
var rows = data.Split('\n');
reader.Dispose();
await stream.DisposeAsync();
var headerProps = rows.First().Split(Separator);
if (!headerProps.Any(h => properties.Any(prop => prop.Name == h))) {
toasts.ShowError("Table header in csv is not valid!");
return;
}
var elements = new List<object>();
for (int rowIndex = 1; rowIndex < rows.Length; rowIndex++) {
var row = rows[rowIndex];
if (string.IsNullOrWhiteSpace(row)) continue;
var element = Activator.CreateInstance(table.TableType)!;
var rowValues = row.Split(Separator);
for (int i = 0; i < headerProps.Length; i++) {
var property = properties.FirstOrDefault(prop => prop.Name == headerProps[i]);
if (property is null) continue;
if (property.IsEnumerable) continue;
object value = rowValues[i];
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);
}
property.SetValue(element, value, provider);
}
elements.Add(element);
}
var manager = explorer.GetTableManager(table.PropertyName);
if (manager is null) {
toasts.ShowError("Data could not be exported!");
return;
}
await manager.AddAll(elements);
await target.Reload();
}
}