Resolve "Exporters" #71
@@ -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
|
||||||
|
|||||||
74
.idea/.idea.HopFrame/.idea/workspace.xml
generated
74
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -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": {
|
"keyToString": {
|
||||||
".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run",
|
".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run",
|
||||||
".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
|
".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
|
||||||
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Debug",
|
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
|
||||||
".NET Project.HopFrame.Testing.executor": "Run",
|
".NET Project.HopFrame.Testing.executor": "Run",
|
||||||
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
|
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
||||||
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
||||||
"git-widget-placeholder": "!33 on feature/exporters",
|
"git-widget-placeholder": "!33 on feature/exporters",
|
||||||
"list.type.of.created.stylesheet": "CSS",
|
"list.type.of.created.stylesheet": "CSS",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}]]></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>
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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)!;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user