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

@@ -41,6 +41,10 @@ publish:
publish-help: publish-help:
stage: publish-help stage: publish-help
image: docker:latest
services:
- name: docker:dind
alias: docker
script: script:
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//') - export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
- docker login -u leon.hoppe -p ${CI_REGISTRY_PASSWORD} registry.leon-hoppe.de - docker login -u leon.hoppe -p ${CI_REGISTRY_PASSWORD} registry.leon-hoppe.de

View File

@@ -12,15 +12,17 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment=""> <list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/src/HopFrame.Web/Plugins/Internal/ExporterPlugin.cs" afterDir="false" /> <change afterPath="$PROJECT_DIR$/src/HopFrame.Web/Services/IFileService.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/HopFrame.Web/Services/Implementation/FileService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitlab-ci.yml" beforeDir="false" afterPath="$PROJECT_DIR$/.gitlab-ci.yml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/ITableManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/ITableManager.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/ITableManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/ITableManager.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/TableManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/TableManager.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/TableManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/TableManager.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Plugins/HopFramePlugin.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Plugins/HopFramePlugin.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Plugins/HopFramePlugin.cs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Plugins/Internal/ExporterPlugin.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Plugins/Internal/ExporterPlugin.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/ServiceCollectionExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/ServiceCollectionExtensions.cs" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -66,6 +68,7 @@
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/5b/a350be00/IEnumerable.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/5b/a350be00/IEnumerable.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/62/1fb63ed0/IDisposable.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/62/1fb63ed0/IDisposable.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/8b/db8582a3/IList`1.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/8b/db8582a3/IList`1.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/a0/0a968c53/IEnumerable`1.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/ad/ba9a50e7/ICollection.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/ad/ba9a50e7/ICollection.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/fc/6f7933d2/ICollection`1.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/fc/6f7933d2/ICollection`1.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/10c66a9a1e137111895f7182a2ae246eabe06a261578c3fa495a45f6f177d35/IconVariant.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/10c66a9a1e137111895f7182a2ae246eabe06a261578c3fa495a45f6f177d35/IconVariant.cs" root0="FORCE_HIGHLIGHTING" />
@@ -123,28 +126,28 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run", &quot;.NET Launch Settings Profile.HopFrame.Testing.Api: https.executor&quot;: &quot;Run&quot;,
".NET Launch Settings Profile.HopFrame.Testing.executor": "Run", &quot;.NET Launch Settings Profile.HopFrame.Testing.executor&quot;: &quot;Run&quot;,
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Debug", &quot;.NET Launch Settings Profile.HopFrame.Testing: https.executor&quot;: &quot;Run&quot;,
".NET Project.HopFrame.Testing.executor": "Run", &quot;.NET Project.HopFrame.Testing.executor&quot;: &quot;Run&quot;,
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug", &quot;72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor&quot;: &quot;Debug&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.git.unshallow": "true", &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug", &quot;b5f11219-dfc4-47a1-b02c-90ab603034fb.executor&quot;: &quot;Debug&quot;,
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug", &quot;dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor&quot;: &quot;Debug&quot;,
"git-widget-placeholder": "!33 on feature/exporters", &quot;git-widget-placeholder&quot;: &quot;!33 on feature/exporters&quot;,
"list.type.of.created.stylesheet": "CSS", &quot;list.type.of.created.stylesheet&quot;: &quot;CSS&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"settings.editor.selected.configurable": "preferences.pluginManager", &quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
"vue.rearranger.settings.migration": "true" &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
} }
}]]></component> }</component>
<component name="RunManager" selected=".NET Launch Settings Profile.HopFrame.Testing: https"> <component name="RunManager" selected=".NET Launch Settings Profile.HopFrame.Testing: https">
<configuration name="HopFrame.Testing: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile"> <configuration name="HopFrame.Testing: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" /> <option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
@@ -255,7 +258,12 @@
<workItem from="1739369355001" duration="1751000" /> <workItem from="1739369355001" duration="1751000" />
<workItem from="1739461452173" duration="5533000" /> <workItem from="1739461452173" duration="5533000" />
<workItem from="1739550750776" duration="3613000" /> <workItem from="1739550750776" duration="3613000" />
<workItem from="1739617785048" duration="5712000" /> <workItem from="1739617785048" duration="5992000" />
<workItem from="1739975843065" duration="1921000" />
<workItem from="1740168829540" duration="1382000" />
<workItem from="1740595969750" duration="34000" />
<workItem from="1740736919561" duration="191000" />
<workItem from="1740738257628" duration="589000" />
</task> </task>
<task id="LOCAL-00001" summary="Added basic configuration"> <task id="LOCAL-00001" summary="Added basic configuration">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -593,7 +601,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1739554261551</updated> <updated>1739554261551</updated>
</task> </task>
<option name="localTasksCounter" value="43" /> <task id="LOCAL-00043" summary="Added basic export and import feature">
<option name="closed" value="true" />
<created>1739623781007</created>
<option name="number" value="00043" />
<option name="presentableId" value="LOCAL-00043" />
<option name="project" value="LOCAL" />
<updated>1739623781007</updated>
</task>
<option name="localTasksCounter" value="44" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -644,7 +660,6 @@
<component name="UnityProjectConfiguration" hasMinimizedUI="false" /> <component name="UnityProjectConfiguration" hasMinimizedUI="false" />
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" /> <option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
<MESSAGE value="Added more tests" />
<MESSAGE value="Added web module tests" /> <MESSAGE value="Added web module tests" />
<MESSAGE value="Tested login functionality" /> <MESSAGE value="Tested login functionality" />
<MESSAGE value="prepared project for release" /> <MESSAGE value="prepared project for release" />
@@ -669,6 +684,7 @@
<MESSAGE value="Added default button removal feature" /> <MESSAGE value="Added default button removal feature" />
<MESSAGE value="Added custom search functionality" /> <MESSAGE value="Added custom search functionality" />
<MESSAGE value="Added fully virtual properties" /> <MESSAGE value="Added fully virtual properties" />
<option name="LAST_COMMIT_MESSAGE" value="Added fully virtual properties" /> <MESSAGE value="Added basic export and import feature" />
<option name="LAST_COMMIT_MESSAGE" value="Added basic export and import feature" />
</component> </component>
</project> </project>

View File

@@ -7,4 +7,5 @@ public interface IContextExplorer {
public TableConfig? GetTable(string tableDisplayName); public TableConfig? GetTable(string tableDisplayName);
public TableConfig? GetTable(Type tableEntity); public TableConfig? GetTable(Type tableEntity);
public ITableManager? GetTableManager(string tablePropertyName); public ITableManager? GetTableManager(string tablePropertyName);
public ITableManager? GetTableManager(Type tableType);
} }

View File

@@ -11,7 +11,7 @@ public interface ITableManager {
public Task EditItem(object item); public Task EditItem(object item);
public Task AddItem(object item); public Task AddItem(object item);
public Task AddAll(IEnumerable<object> items); 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); public Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null);
} }

View File

@@ -55,6 +55,21 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
return null; 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) { private void SeedTableData(TableConfig table) {
if (table.Seeded) return; if (table.Seeded) return;
var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!; var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!;

View File

@@ -55,6 +55,11 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
await context.SaveChangesAsync(); 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) { public async Task RevertChanges(object item) {
var entry = context.Entry((TModel)item); var entry = context.Entry((TModel)item);
await entry.ReloadAsync(); await entry.ReloadAsync();

View File

@@ -202,7 +202,10 @@
private List<PluginButton> _pluginButtons = new(); private List<PluginButton> _pluginButtons = new();
private DefaultButtonToggles _buttonToggles = new(); private DefaultButtonToggles _buttonToggles = new();
internal static HopFrameTablePage? CurrentInstance { get; private set; }
protected override void OnInitialized() { protected override void OnInitialized() {
CurrentInstance = this;
_config ??= Explorer.GetTable(TableDisplayName); _config ??= Explorer.GetTable(TableDisplayName);
if (_config is null || (_config.Ignored && DialogData is null)) { if (_config is null || (_config.Ignored && DialogData is null)) {

View File

@@ -38,7 +38,7 @@ public static class HopFrameConfiguratorExtensions {
/// </summary> /// </summary>
/// <param name="configurator">The configurator for the HopFrame config that is being created</param> /// <param name="configurator">The configurator for the HopFrame config that is being created</param>
/// <typeparam name="TPlugin">The plugin that should be registered</typeparam> /// <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)); PluginOrchestrator.RegisterPlugin(configurator.ServiceCollection, typeof(TPlugin));
var methods = typeof(TPlugin).GetMethods() 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.Config;
using HopFrame.Core.Services; using HopFrame.Core.Services;
using HopFrame.Web.Components.Pages; using HopFrame.Web.Components.Pages;
using HopFrame.Web.Plugins.Annotations; using HopFrame.Web.Plugins.Annotations;
using HopFrame.Web.Plugins.Events; using HopFrame.Web.Plugins.Events;
using HopFrame.Web.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;
namespace HopFrame.Web.Plugins.Internal; 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 = ';'; public const char Separator = ';';
[EventHandler] [EventHandler]
@@ -31,32 +32,30 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runti
.LoadPage(0, int.MaxValue) .LoadPage(0, int.MaxValue)
.ToArrayAsync(); .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.Info.Name)) + '\n');
var csv = new StringBuilder(string.Join(Separator, properties.Select(prop => prop.Name)) + '\n');
foreach (var entry in data) { foreach (var entry in data) {
var row = new List<string>(); var row = new List<string>();
foreach (var property in properties) { foreach (var property in properties) {
var value = await manager.DisplayProperty(entry, property); row.Add(FormatProperty(property, entry));
row.Add(value);
} }
csv.Append(string.Join(Separator, row) + '\n'); csv.Append(string.Join(Separator, row) + '\n');
} }
var result = csv.ToString(); 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) { private async Task Import(TableConfig table, HopFrameTablePage target) {
var file = await UploadFile(target, runtime); var file = await files.UploadFile();
var stream = file.OpenReadStream(); var stream = file.OpenReadStream();
var reader = new StreamReader(stream); 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 data = await reader.ReadToEndAsync();
var rows = data.Split('\n'); var rows = data.Split('\n');
@@ -64,7 +63,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runti
await stream.DisposeAsync(); await stream.DisposeAsync();
var headerProps = rows.First().Split(Separator); 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!"); toasts.ShowError("Table header in csv is not valid!");
return; return;
} }
@@ -78,22 +77,57 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runti
var rowValues = row.Split(Separator); var rowValues = row.Split(Separator);
for (int i = 0; i < headerProps.Length; i++) { 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 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); var success = Guid.TryParse((string)value, out var guid);
if (success) value = guid; if (success) value = guid;
else toasts.ShowError($"'{value}' is not a valid guid"); else toasts.ShowError($"'{value}' is not a valid guid");
} }
else {
if (property.Parser is not null) { value = ParseString((string)value, property.Info.PropertyType);
value = await property.Parser(value.ToString()!, provider);
} }
property.SetValue(element, value, provider);
property.Info.SetValue(element, value);
} }
elements.Add(element); elements.Add(element);
@@ -101,7 +135,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runti
var manager = explorer.GetTableManager(table.PropertyName); var manager = explorer.GetTableManager(table.PropertyName);
if (manager is null) { if (manager is null) {
toasts.ShowError("Data could not be exported!"); toasts.ShowError("Data could not be imported!");
return; return;
} }
@@ -109,4 +143,55 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IJSRuntime runti
await target.Reload(); 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.Components.Pages;
using HopFrame.Web.Plugins; using HopFrame.Web.Plugins;
using HopFrame.Web.Plugins.Internal; using HopFrame.Web.Plugins.Internal;
using HopFrame.Web.Services;
using HopFrame.Web.Services.Implementation;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@@ -41,6 +43,7 @@ public static class ServiceCollectionExtensions {
services.AddFluentUIComponents(fluentUiLibraryConfiguration); services.AddFluentUIComponents(fluentUiLibraryConfiguration);
services.AddScoped<IPluginOrchestrator, PluginOrchestrator>(); services.AddScoped<IPluginOrchestrator, PluginOrchestrator>();
services.AddScoped<IFileService, FileService>();
if (addRazorComponents) { if (addRazorComponents) {
services.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;
}
}

View File

@@ -52,7 +52,8 @@ builder.Services.AddHopFrame(options => {
context.Table<Post>() context.Table<Post>()
.Property(p => p.Author) .Property(p => p.Author)
.Format((user, _) => $"{user.FirstName} {user.LastName}"); .Format((user, _) => $"{user.FirstName} {user.LastName}")
.SetValidator((_, _) => []);
context.Table<Post>() context.Table<Post>()
.Property(p => p.Id) .Property(p => p.Id)
@@ -77,9 +78,9 @@ builder.Services.AddHopFrame(options => {
return errors; return errors;
})*/; })*/;
context.Table<Post>() /*context.Table<Post>()
.SetOrderIndex(-1) .SetOrderIndex(-1)
.Ignore(true); .Ignore(true);*/
}); });
options.AddCustomView("Counter", "/counter") options.AddCustomView("Counter", "/counter")