Resolve "Exporters" #71
@@ -41,6 +41,10 @@ publish:
|
||||
|
||||
publish-help:
|
||||
stage: publish-help
|
||||
image: docker:latest
|
||||
services:
|
||||
- name: docker:dind
|
||||
alias: docker
|
||||
script:
|
||||
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
|
||||
- docker login -u leon.hoppe -p ${CI_REGISTRY_PASSWORD} registry.leon-hoppe.de
|
||||
|
||||
103
.idea/.idea.HopFrame/.idea/workspace.xml
generated
103
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -13,16 +13,8 @@
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
|
||||
<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/Config/DbContextConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/HopFrameConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/HopFrameConfig.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.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$/testing/HopFrame.Testing/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/tests/HopFrame.Tests.Core/Config/TableConfiguratorTests.cs" beforeDir="false" afterPath="$PROJECT_DIR$/tests/HopFrame.Tests.Core/Config/TableConfiguratorTests.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs" beforeDir="false" afterPath="$PROJECT_DIR$/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/docs/Writerside/topics/Plugins.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/Writerside/topics/Plugins.md" 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" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -42,7 +34,7 @@
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="dev" />
|
||||
<entry key="$PROJECT_DIR$" value="feature/virtual-properties" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
@@ -68,6 +60,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/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/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/fc/6f7933d2/ICollection`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/10c66a9a1e137111895f7182a2ae246eabe06a261578c3fa495a45f6f177d35/IconVariant.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
@@ -75,6 +68,7 @@
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/24dd1164ba47541cb1d3eb011e638e16953dbea3ae3f4dc208c3bbf3e96298a/ServiceCollectionServiceExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/26c9a2fb5243863babc926e4be763daf4128d4f97c4a769cdce1e2e3e5c532/FluentButton.razor.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/2751d5afefca5424bfc4b21347f581372f7a739c0ae4df661ea557fcb97ef20/EnumExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/2a4d2ce4c06ab596b3676c5cf06066b4391ec7dd93cdf8f0334b69dc1a9de/TextReader.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/439c4ee753b23e743cc14119593bc889751f9eb0b38997577d8e4c47c4fed/ToCollection.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/4c41a7d338749915157d56585365d1693fbad6be8231d3d583b1cf10d16896d9/FluentIcon.razor.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/4ee221fd7e91e9a4c14ff82aae2ee938edecde35a934133e991aba56aa9499/Icon.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
@@ -88,12 +82,14 @@
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/7ad7d2d0ae865063993eb8a03427815ea3bdb6a774e0a2f95512e9f669a4f489/MemberEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/87c584767b46b5fd42769be76547105558e6690f785614efddca134b2d682/Type.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/8d5d6cbff46ddc7b152381f92ae1ae51d3e7b57b14dd23840a11f5aaaaed396/InternalEntityEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a882d183338544fdbcbdfc7b6d3dcb78916630765551644a221b5be9c45a121b/Int32.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/aa3ea54f92373c58ec1149fbd41215869a98bd385c30584bc6db2fa3c6e88443/Filled24.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/adcd2c45092dd8e4fc412325c8adb75d6e7d8b3e90a9523f167583fb9c60/ServiceCollectionExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/b3ccb66df3646cb51df73ad51716136ebd2eefb4edb1308dd52a7e999582d59e/IBindableColumn.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/bfff78ecaa39c818519fc918bb2d4bbdca6ad93d7170f5cf325f67ccd0b97d43/BooleanAsserts.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d0165cb640e16fb3b8fe6932c042fc2917cd7f2770ff123cf7b9d11b5bfc6/Task.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d04a416cac8afac0341a8be0e859b230f2eae64924298eef48c317ba35916/RenderTreeBuilder.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d1287462d4ec4078c61b8e92a0952fb7de3e7e877d279e390a4c136a6365126/Stream.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d39923abb31e6a6e7a9e8173e217da584c54925ce63e568126a2b89b9ab/DefaultRazorComponentsServiceOptionsConfiguration.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d858ddb35a8e36df5573b7612542f9ad50f426b8ab43818587d1ac65fab14829/DatabaseGeneratedAttribute.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/dac3553c90d47a746e7e7f02faecb1a5e581090/Components_AppBar_FluentAppBarItem_razor.g.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
@@ -123,28 +119,28 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run",
|
||||
".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
|
||||
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
|
||||
".NET Project.HopFrame.Testing.executor": "Run",
|
||||
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
||||
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
||||
"git-widget-placeholder": "!32 on feature/virtual-properties",
|
||||
"list.type.of.created.stylesheet": "CSS",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run",
|
||||
".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
|
||||
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
|
||||
".NET Project.HopFrame.Testing.executor": "Run",
|
||||
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
||||
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
||||
"git-widget-placeholder": "!33 on feature/exporters",
|
||||
"list.type.of.created.stylesheet": "CSS",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.HopFrame.Testing: https">
|
||||
<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" />
|
||||
@@ -254,7 +250,14 @@
|
||||
<workItem from="1739352479748" duration="3047000" />
|
||||
<workItem from="1739369355001" duration="1751000" />
|
||||
<workItem from="1739461452173" duration="5533000" />
|
||||
<workItem from="1739550750776" duration="3388000" />
|
||||
<workItem from="1739550750776" duration="3613000" />
|
||||
<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="3216000" />
|
||||
<workItem from="1740741585276" duration="17000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Added basic configuration">
|
||||
<option name="closed" value="true" />
|
||||
@@ -584,7 +587,31 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738775556256</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="42" />
|
||||
<task id="LOCAL-00042" summary="Added fully virtual properties">
|
||||
<option name="closed" value="true" />
|
||||
<created>1739554261551</created>
|
||||
<option name="number" value="00042" />
|
||||
<option name="presentableId" value="LOCAL-00042" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1739554261551</updated>
|
||||
</task>
|
||||
<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>
|
||||
<task id="LOCAL-00044" summary="Finished converter plugin">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740741334420</created>
|
||||
<option name="number" value="00044" />
|
||||
<option name="presentableId" value="LOCAL-00044" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740741334420</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="45" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -635,9 +662,6 @@
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||
<MESSAGE value="Created tests for the core module" />
|
||||
<MESSAGE value="Added more tests" />
|
||||
<MESSAGE value="Added web module tests" />
|
||||
<MESSAGE value="Tested login functionality" />
|
||||
<MESSAGE value="prepared project for release" />
|
||||
<MESSAGE value="Included readme file in projects" />
|
||||
@@ -660,6 +684,9 @@
|
||||
<MESSAGE value="Added plugin buttons" />
|
||||
<MESSAGE value="Added default button removal feature" />
|
||||
<MESSAGE value="Added custom search functionality" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Added custom search functionality" />
|
||||
<MESSAGE value="Added fully virtual properties" />
|
||||
<MESSAGE value="Added basic export and import feature" />
|
||||
<MESSAGE value="Finished converter plugin" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Finished converter plugin" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Welcome to the **HopFrame**! This project aims to provide a comprehensive and modular framework for easy management of your database.
|
||||
The framework is designed to be highly configurable, ensuring that developers either quickly add the framework for simple data editing or
|
||||
configure it to their needs to implement it fully in their data management pipeline.
|
||||
configure it to their needs to implement it fully in their data management pipeline. Read more in the project [docs](https://hopframe.leon-hoppe.de).
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ by using Plugins. They are registered as scoped services so you can use DI like
|
||||
|
||||
## Add a plugin
|
||||
|
||||
Create a class that extends the `HopFramePlugin` class:
|
||||
Create a class that represents the plugin:
|
||||
|
||||
```C#
|
||||
public class SearchExtension : HopFramePlugin {
|
||||
public class SearchExtension {
|
||||
|
||||
}
|
||||
```
|
||||
@@ -60,3 +60,19 @@ public void OnDelete(DeleteEntryEvent e) {
|
||||
cacheHandler.ClearCache(e.Entity);
|
||||
}
|
||||
```
|
||||
|
||||
## Useful services
|
||||
|
||||
### IFileService
|
||||
|
||||
If you want to deal with file uploading / downloading, you can use the `IFileService`:
|
||||
|
||||
```C#
|
||||
public interface IFileService {
|
||||
|
||||
public Task DownloadFile(string name, byte[] data);
|
||||
|
||||
public Task<IBrowserFile> UploadFile();
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -10,7 +10,8 @@ public interface ITableManager {
|
||||
public Task DeleteItem(object item);
|
||||
public Task EditItem(object item);
|
||||
public Task AddItem(object item);
|
||||
public Task RevertChanges(object item);
|
||||
public Task AddAll(IEnumerable<object> items);
|
||||
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)!;
|
||||
|
||||
@@ -49,6 +49,17 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddAll(IEnumerable<object> items) {
|
||||
var table = context.Set<TModel>();
|
||||
await table.AddRangeAsync(items.Cast<TModel>());
|
||||
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();
|
||||
|
||||
@@ -412,7 +412,7 @@
|
||||
_tokenSource.Dispose();
|
||||
}
|
||||
|
||||
private enum InputType {
|
||||
public enum InputType {
|
||||
Number,
|
||||
Switch,
|
||||
Date,
|
||||
|
||||
@@ -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
|
||||
@@ -184,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)) {
|
||||
@@ -254,7 +275,7 @@
|
||||
await Reload();
|
||||
}
|
||||
|
||||
private async Task Reload() {
|
||||
public async Task Reload() {
|
||||
_loading = true;
|
||||
|
||||
var eventResult = await PluginOrchestrator.DispatchEvent(new ReloadEvent(this) {
|
||||
@@ -275,7 +296,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 +404,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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -56,4 +56,9 @@ public static class HopFrameConfiguratorExtensions {
|
||||
return configurator;
|
||||
}
|
||||
|
||||
public static HopFrameConfigurator AddExporters(this HopFrameConfigurator configurator) {
|
||||
configurator.AddPlugin<ExporterPlugin>();
|
||||
return configurator;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace HopFrame.Web.Plugins;
|
||||
|
||||
public abstract class HopFramePlugin;
|
||||
197
src/HopFrame.Web/Plugins/Internal/ExporterPlugin.cs
Normal file
197
src/HopFrame.Web/Plugins/Internal/ExporterPlugin.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
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;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Internal;
|
||||
|
||||
internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService toasts, IFileService files) {
|
||||
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.IsVirtualProperty).ToArray();
|
||||
|
||||
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) {
|
||||
row.Add(FormatProperty(property, entry));
|
||||
}
|
||||
|
||||
csv.Append(string.Join(Separator, row) + '\n');
|
||||
}
|
||||
|
||||
var result = csv.ToString();
|
||||
await files.DownloadFile($"{table.DisplayName}.csv", Encoding.UTF8.GetBytes(result));
|
||||
}
|
||||
|
||||
private async Task Import(TableConfig table, HopFrameTablePage target) {
|
||||
var file = await files.UploadFile();
|
||||
|
||||
var stream = file.OpenReadStream();
|
||||
var reader = new StreamReader(stream);
|
||||
|
||||
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).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.Info.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.Info.Name == headerProps[i]);
|
||||
if (property is null) continue;
|
||||
|
||||
object? value = rowValues[i];
|
||||
|
||||
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");
|
||||
}
|
||||
else {
|
||||
value = ParseString((string)value, property.Info.PropertyType);
|
||||
}
|
||||
|
||||
property.Info.SetValue(element, value);
|
||||
}
|
||||
|
||||
elements.Add(element);
|
||||
}
|
||||
|
||||
var manager = explorer.GetTableManager(table.PropertyName);
|
||||
if (manager is null) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -52,7 +52,8 @@ builder.Services.AddHopFrame(options => {
|
||||
|
||||
context.Table<Post>()
|
||||
.Property(p => p.Author)
|
||||
.Format((user, _) => $"{user.FirstName} {user.LastName}");
|
||||
.Format((user, _) => $"{user.FirstName} {user.LastName}")
|
||||
.SetValidator((_, _) => []);
|
||||
|
||||
context.Table<Post>()
|
||||
.Property(p => p.Id)
|
||||
@@ -77,14 +78,16 @@ builder.Services.AddHopFrame(options => {
|
||||
return errors;
|
||||
})*/;
|
||||
|
||||
context.Table<Post>()
|
||||
/*context.Table<Post>()
|
||||
.SetOrderIndex(-1)
|
||||
.Ignore(true);
|
||||
.Ignore(true);*/
|
||||
});
|
||||
|
||||
options.AddCustomView("Counter", "/counter")
|
||||
.SetDescription("A custom view")
|
||||
.SetPolicy("counter.view");
|
||||
|
||||
options.AddExporters();
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
Reference in New Issue
Block a user