Resolve "Exporters" #71

Merged
leon.hoppe merged 3 commits from feature/exporters into dev 2025-02-28 12:22:53 +01:00
17 changed files with 412 additions and 52 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

@@ -13,16 +13,8 @@
<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 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/Config/DbContextConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.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.Core/Config/HopFrameConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/HopFrameConfig.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.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" />
</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" />
@@ -42,7 +34,7 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY"> <option name="RECENT_BRANCH_BY_REPOSITORY">
<map> <map>
<entry key="$PROJECT_DIR$" value="dev" /> <entry key="$PROJECT_DIR$" value="feature/virtual-properties" />
</map> </map>
</option> </option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <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/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" />
@@ -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/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/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/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/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/4c41a7d338749915157d56585365d1693fbad6be8231d3d583b1cf10d16896d9/FluentIcon.razor.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/4ee221fd7e91e9a4c14ff82aae2ee938edecde35a934133e991aba56aa9499/Icon.cs" root0="FORCE_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/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/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/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/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/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/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/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/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/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/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/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" /> <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="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": "Run", &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": "!32 on feature/virtual-properties", &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" />
@@ -254,7 +250,14 @@
<workItem from="1739352479748" duration="3047000" /> <workItem from="1739352479748" duration="3047000" />
<workItem from="1739369355001" duration="1751000" /> <workItem from="1739369355001" duration="1751000" />
<workItem from="1739461452173" duration="5533000" /> <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>
<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" />
@@ -584,7 +587,31 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1738775556256</updated> <updated>1738775556256</updated>
</task> </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 /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -635,9 +662,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="Created tests for the core module" />
<MESSAGE value="Added more 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" />
<MESSAGE value="Included readme file in projects" /> <MESSAGE value="Included readme file in projects" />
@@ -660,6 +684,9 @@
<MESSAGE value="Added plugin buttons" /> <MESSAGE value="Added plugin buttons" />
<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" />
<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> </component>
</project> </project>

View File

@@ -4,7 +4,7 @@
Welcome to the **HopFrame**! This project aims to provide a comprehensive and modular framework for easy management of your database. 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 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 ## Features

View File

@@ -5,10 +5,10 @@ by using Plugins. They are registered as scoped services so you can use DI like
## Add a plugin ## Add a plugin
Create a class that extends the `HopFramePlugin` class: Create a class that represents the plugin:
```C# ```C#
public class SearchExtension : HopFramePlugin { public class SearchExtension {
} }
``` ```
@@ -60,3 +60,19 @@ public void OnDelete(DeleteEntryEvent e) {
cacheHandler.ClearCache(e.Entity); 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();
}
```

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

@@ -10,7 +10,8 @@ public interface ITableManager {
public Task DeleteItem(object item); public Task DeleteItem(object item);
public Task EditItem(object item); public Task EditItem(object item);
public Task AddItem(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); 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

@@ -49,6 +49,17 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
await context.SaveChangesAsync(); 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) { 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

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

View File

@@ -135,8 +135,26 @@
} }
removeBg(); 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> </script>
<FluentToastProvider MaxToastCount="10" />
<InputFile style="display: none" @ref="FileInputElement" OnChange="OnInputFiles"></InputFile>
@inject IContextExplorer Explorer @inject IContextExplorer Explorer
@inject NavigationManager Navigator @inject NavigationManager Navigator
@inject IJSRuntime Js @inject IJSRuntime Js
@@ -184,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)) {
@@ -254,7 +275,7 @@
await Reload(); await Reload();
} }
private async Task Reload() { public async Task Reload() {
_loading = true; _loading = true;
var eventResult = await PluginOrchestrator.DispatchEvent(new ReloadEvent(this) { var eventResult = await PluginOrchestrator.DispatchEvent(new ReloadEvent(this) {
@@ -275,7 +296,7 @@
_loading = false; _loading = false;
} }
private async Task ChangePage(int page) { public async Task ChangePage(int page) {
var eventResult = await PluginOrchestrator.DispatchEvent(new PageChangeEvent(this) { var eventResult = await PluginOrchestrator.DispatchEvent(new PageChangeEvent(this) {
CurrentPage = _currentPage, CurrentPage = _currentPage,
NewPage = page, NewPage = page,
@@ -383,4 +404,21 @@
return display; return display;
} }
public InputFile? FileInputElement;
public Func<IEnumerable<IBrowserFile>, Task>? OnFileUpload;
private async Task OnInputFiles(InputFileChangeEventArgs e) {
if (OnFileUpload is null) return;
if (e.FileCount == 1) {
await OnFileUpload.Invoke([e.File]);
}
else {
await OnFileUpload.Invoke(e.GetMultipleFiles());
}
}
public void RequestRender() {
StateHasChanged();
}
} }

View File

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

View File

@@ -1,3 +0,0 @@
namespace HopFrame.Web.Plugins;
public abstract class HopFramePlugin;

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

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,14 +78,16 @@ 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")
.SetDescription("A custom view") .SetDescription("A custom view")
.SetPolicy("counter.view"); .SetPolicy("counter.view");
options.AddExporters();
}); });
var app = builder.Build(); var app = builder.Build();