Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e5d50b1c9 | |||
| 18937f9275 | |||
| 2fdd305cd7 | |||
| 6bc2479984 | |||
| b9c34a85df | |||
| e773871dc0 | |||
| 86ace64618 | |||
| 6c42008a28 | |||
| 0262b3b97b | |||
| 84c37012ec | |||
| 56d45575f8 | |||
| 9a66f88f3c | |||
| 93d41ad6d3 | |||
| 08d4ddb2c6 | |||
| 47f30bf33f | |||
| 8db0f84a80 | |||
| 46f14d3ddb | |||
| 43fda30a01 | |||
| 23c5115c99 | |||
| fb761c74d2 | |||
| 13e9af892c | |||
| 4cfeaab652 | |||
| 2256a59f9a | |||
| bfea4e9cff | |||
| 7ce066df7b | |||
| 39641f18a8 | |||
| 966ced57d6 | |||
| ec3ab67cb9 | |||
| d802fde7d8 | |||
| 88d843c1cb | |||
| fecbc0717b | |||
| 5a342e2c53 |
@@ -1,39 +1,20 @@
|
|||||||
image: mcr.microsoft.com/dotnet/sdk:9.0
|
image: mcr.microsoft.com/dotnet/sdk:9.0
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- build
|
- publish-help
|
||||||
- test
|
|
||||||
- publish
|
|
||||||
|
|
||||||
before_script:
|
publish-help:
|
||||||
- echo "Setting up environment"
|
stage: publish-help
|
||||||
- 'dotnet --version'
|
image: docker:latest
|
||||||
|
services:
|
||||||
build:
|
- name: docker:dind
|
||||||
stage: build
|
alias: docker
|
||||||
script:
|
|
||||||
- dotnet restore
|
|
||||||
- dotnet build --configuration Release --no-restore
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- "**/bin/Release"
|
|
||||||
expire_in: 10 minutes
|
|
||||||
|
|
||||||
test:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- dotnet test --verbosity normal
|
|
||||||
dependencies:
|
|
||||||
- build
|
|
||||||
|
|
||||||
publish:
|
|
||||||
stage: publish
|
|
||||||
script:
|
script:
|
||||||
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
|
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
|
||||||
- dotnet pack -c Release -o . /p:Version=$VERSION
|
- cd docs
|
||||||
- for nupkg in *.nupkg; do dotnet nuget push $nupkg -k ${NUGET_API_KEY} -s https://api.nuget.org/v3/index.json; done
|
- docker login -u leon.hoppe -p ${CI_REGISTRY_PASSWORD} registry.leon-hoppe.de
|
||||||
|
- docker build -t registry.leon-hoppe.de/leon.hoppe/hopframe:$VERSION -t registry.leon-hoppe.de/leon.hoppe/hopframe:latest .
|
||||||
|
- docker push registry.leon-hoppe.de/leon.hoppe/hopframe:$VERSION
|
||||||
|
- docker push registry.leon-hoppe.de/leon.hoppe/hopframe:latest
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
dependencies:
|
|
||||||
- build
|
|
||||||
- test
|
|
||||||
|
|||||||
274
.idea/.idea.HopFrame/.idea/workspace.xml
generated
274
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -11,8 +11,10 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="Added n-m relation mapping">
|
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
|
||||||
<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$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" 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>
|
</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" />
|
||||||
@@ -32,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="feature/max-length" />
|
<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$" />
|
||||||
@@ -54,25 +56,51 @@
|
|||||||
}
|
}
|
||||||
}</component>
|
}</component>
|
||||||
<component name="HighlightingSettingsPerFile">
|
<component name="HighlightingSettingsPerFile">
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/02/6ae7626a/IList.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/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" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/1b81cb3be224213a6a73519b6e340a628d9a1fb8629c351a186a26f6376669/List.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/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/558c1d46e1e21d2e78ee2ab67a674f6927bf95355b2f245f35d74bb5ec0f92/CancellationTokenSource.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/5a69b82eed595b731b82667db08722b69b82482e275cf32dfb219190e3dc49/CollectionEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/60e7b22380df80ef6fefe43138047f49ec6eff4b25c12b42ce3d6ed5aac/MethodInvokerCommon.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/60e7b22380df80ef6fefe43138047f49ec6eff4b25c12b42ce3d6ed5aac/MethodInvokerCommon.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6354a7b35d7821629924d3676acd7e67a6f7f94343e0e66ec439aa2bd6ed5/ThrowHelper.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/642391624bd5c30b3411a11434588aba4906207335166b784bf3a4325f6c7/NavigationEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6d1d64f05e7045295fa180276a8c2aef0302c9e96eb53b3431ab13db4579/FluentAppBarItem.razor.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6fe785cceb29ca2d1da78e157315815a7c4372b582a20a71c28b210f9d56e/IconsExtensions.cs" root0="SKIP_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/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/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/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/e26a4f2df232f16e374b9719f883c1b2419f6341838d94b7581db9c7d2de17/IconInfo.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/eab2d6b892f743a27cb49a139ba782855897baf1233febd2dfd2092f3/EntityEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ece8533187fe96ce67b3ef1c9cc3502ef8da5510aadb132a9b21c5605d7c2119/PropertyColumn.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ece8533187fe96ce67b3ef1c9cc3502ef8da5510aadb132a9b21c5605d7c2119/PropertyColumn.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ee4d234452e240d83e3de396c2e85cbf9ac9fb9add618b955eea196c81aaf8/IDialogContentComponent.cs" root0="SKIP_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ee4d234452e240d83e3de396c2e85cbf9ac9fb9add618b955eea196c81aaf8/IDialogContentComponent.cs" root0="SKIP_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/fc2027f7e776fc105cddb56b1a25eeb3895b3ae6f3aac854d786e63bd01f75e2/CallSiteFactory.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/fc2027f7e776fc105cddb56b1a25eeb3895b3ae6f3aac854d786e63bd01f75e2/CallSiteFactory.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/fd57398b7dc3a8ce7da2786f2c67289c3d974658a9e90d0c1e84db3d965fbf1/Console.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ff37d54b3bf4d2756237fb789635831532603376e940f63d634b869d26d74c/Regular16.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ff37d54b3bf4d2756237fb789635831532603376e940f63d634b869d26d74c/Regular16.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Core/Config/PropertyConfig.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Core/Config/PropertyConfig.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Core/Config/PropertyConfig.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" root0="SKIP_HIGHLIGHTING" root1="FORCE_HIGHLIGHTING" root2="FORCE_HIGHLIGHTING" />
|
|
||||||
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
</component>
|
</component>
|
||||||
<component name="KubernetesApiPersistence">{}</component>
|
<component name="KubernetesApiPersistence">{}</component>
|
||||||
<component name="KubernetesApiProvider">{
|
<component name="KubernetesApiProvider">{
|
||||||
@@ -84,34 +112,35 @@
|
|||||||
}</component>
|
}</component>
|
||||||
<component name="ProjectId" id="2raEUZtlphkj04rfRNtlQw6fPMU" />
|
<component name="ProjectId" id="2raEUZtlphkj04rfRNtlQw6fPMU" />
|
||||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
||||||
|
<OptionsSetting value="false" id="Update" />
|
||||||
<ConfirmationsSetting value="2" id="Add" />
|
<ConfirmationsSetting value="2" id="Add" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectViewState">
|
<component name="ProjectViewState">
|
||||||
<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": "Run",
|
".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": "!24 on fix/relations",
|
"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.environmentSetup",
|
"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" />
|
||||||
@@ -207,7 +236,28 @@
|
|||||||
<workItem from="1737390240714" duration="60000" />
|
<workItem from="1737390240714" duration="60000" />
|
||||||
<workItem from="1737390360987" duration="601000" />
|
<workItem from="1737390360987" duration="601000" />
|
||||||
<workItem from="1737993570961" duration="4163000" />
|
<workItem from="1737993570961" duration="4163000" />
|
||||||
<workItem from="1738054766160" duration="7142000" />
|
<workItem from="1738054766160" duration="7449000" />
|
||||||
|
<workItem from="1738075629332" duration="8862000" />
|
||||||
|
<workItem from="1738335286481" duration="2039000" />
|
||||||
|
<workItem from="1738403493974" duration="4231000" />
|
||||||
|
<workItem from="1738418482606" duration="2795000" />
|
||||||
|
<workItem from="1738421294144" duration="1651000" />
|
||||||
|
<workItem from="1738422949337" duration="141000" />
|
||||||
|
<workItem from="1738512801911" duration="6776000" />
|
||||||
|
<workItem from="1738769458367" duration="5256000" />
|
||||||
|
<workItem from="1738774834563" duration="728000" />
|
||||||
|
<workItem from="1739301922710" duration="33000" />
|
||||||
|
<workItem from="1739352479748" duration="3047000" />
|
||||||
|
<workItem from="1739369355001" duration="1751000" />
|
||||||
|
<workItem from="1739461452173" duration="5533000" />
|
||||||
|
<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" />
|
||||||
@@ -433,7 +483,135 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1738062559567</updated>
|
<updated>1738062559567</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="29" />
|
<task id="LOCAL-00029" summary="Fixed wrong element selection for action buttons">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738063028173</created>
|
||||||
|
<option name="number" value="00029" />
|
||||||
|
<option name="presentableId" value="LOCAL-00029" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738063028173</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00030" summary="Implemented primitive change reversion">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738079122848</created>
|
||||||
|
<option name="number" value="00030" />
|
||||||
|
<option name="presentableId" value="LOCAL-00030" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738079122848</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00031" summary="Implemented deferred entry manipulation">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738084259089</created>
|
||||||
|
<option name="number" value="00031" />
|
||||||
|
<option name="presentableId" value="LOCAL-00031" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738084259089</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00032" summary="Removed select all button">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738337068205</created>
|
||||||
|
<option name="number" value="00032" />
|
||||||
|
<option name="presentableId" value="LOCAL-00032" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738337068205</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00033" summary="Added missing installation instructions">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738337314351</created>
|
||||||
|
<option name="number" value="00033" />
|
||||||
|
<option name="presentableId" value="LOCAL-00033" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738337314351</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00034" summary="Added modular event system">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738407061976</created>
|
||||||
|
<option name="number" value="00034" />
|
||||||
|
<option name="presentableId" value="LOCAL-00034" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738407061976</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00035" summary="Fixed event emitter service scope">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738407710507</created>
|
||||||
|
<option name="number" value="00035" />
|
||||||
|
<option name="presentableId" value="LOCAL-00035" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738407710507</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00036" summary="Added custom views">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738422931038</created>
|
||||||
|
<option name="number" value="00036" />
|
||||||
|
<option name="presentableId" value="LOCAL-00036" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738422931038</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00037" summary="Added plugin events">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738519603597</created>
|
||||||
|
<option name="number" value="00037" />
|
||||||
|
<option name="presentableId" value="LOCAL-00037" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738519603597</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00038" summary="Passed cancellation tokens to event handlers if needed">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738770468949</created>
|
||||||
|
<option name="number" value="00038" />
|
||||||
|
<option name="presentableId" value="LOCAL-00038" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738770468949</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00039" summary="Added plugin buttons">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738773315593</created>
|
||||||
|
<option name="number" value="00039" />
|
||||||
|
<option name="presentableId" value="LOCAL-00039" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738773315593</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00040" summary="Added default button removal feature">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738774569657</created>
|
||||||
|
<option name="number" value="00040" />
|
||||||
|
<option name="presentableId" value="LOCAL-00040" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738774569657</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00041" summary="Added custom search functionality">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1738775556256</created>
|
||||||
|
<option name="number" value="00041" />
|
||||||
|
<option name="presentableId" value="LOCAL-00041" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1738775556256</updated>
|
||||||
|
</task>
|
||||||
|
<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">
|
||||||
@@ -484,22 +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="Started working on listing page" />
|
|
||||||
<MESSAGE value="Added entry saving support" />
|
|
||||||
<MESSAGE value="Added reload button and animation" />
|
|
||||||
<MESSAGE value="Added relation picker dialog" />
|
|
||||||
<MESSAGE value="Added automatic relation mapping" />
|
|
||||||
<MESSAGE value="Added property validation" />
|
|
||||||
<MESSAGE value="Added creation/modification confirmation" />
|
|
||||||
<MESSAGE value="Removed Template" />
|
|
||||||
<MESSAGE value="Added policy validation, ordering and virtual listing properties" />
|
|
||||||
<MESSAGE value="Added n -> m relation support" />
|
|
||||||
<MESSAGE value="Added text area support and DI support for modifier functions" />
|
|
||||||
<MESSAGE value="Addressed all build warnings" />
|
|
||||||
<MESSAGE value="Added documentation for the configurators and service extensions methods" />
|
|
||||||
<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" />
|
||||||
@@ -509,6 +671,22 @@
|
|||||||
<MESSAGE value="Added maximum display length" />
|
<MESSAGE value="Added maximum display length" />
|
||||||
<MESSAGE value="Fixed test for table view" />
|
<MESSAGE value="Fixed test for table view" />
|
||||||
<MESSAGE value="Added n-m relation mapping" />
|
<MESSAGE value="Added n-m relation mapping" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Added n-m relation mapping" />
|
<MESSAGE value="Fixed wrong element selection for action buttons" />
|
||||||
|
<MESSAGE value="Implemented primitive change reversion" />
|
||||||
|
<MESSAGE value="Implemented deferred entry manipulation" />
|
||||||
|
<MESSAGE value="Removed select all button" />
|
||||||
|
<MESSAGE value="Added missing installation instructions" />
|
||||||
|
<MESSAGE value="Added modular event system" />
|
||||||
|
<MESSAGE value="Fixed event emitter service scope" />
|
||||||
|
<MESSAGE value="Added custom views" />
|
||||||
|
<MESSAGE value="Added plugin events" />
|
||||||
|
<MESSAGE value="Passed cancellation tokens to event handlers if needed" />
|
||||||
|
<MESSAGE value="Added plugin buttons" />
|
||||||
|
<MESSAGE value="Added default button removal feature" />
|
||||||
|
<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>
|
||||||
16
README.md
16
README.md
@@ -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
|
||||||
|
|
||||||
@@ -16,6 +16,14 @@ configure it to their needs to implement it fully in their data management pipel
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Install the nuget package using the CLI or the UI of your IDE:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add package HopFrame.Web
|
||||||
|
```
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
Configuring HopFrame is straightforward and flexible. You can easily define your contexts, tables, and their properties using the provided configurators.
|
Configuring HopFrame is straightforward and flexible. You can easily define your contexts, tables, and their properties using the provided configurators.
|
||||||
@@ -72,6 +80,12 @@ builder.Services.AddHopFrame(options => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then you need to map the frontend pages in your application:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
app.MapHopFrame();
|
||||||
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
- Navigate to `/admin` to access the admin dashboard and start managing your tables.
|
- Navigate to `/admin` to access the admin dashboard and start managing your tables.
|
||||||
|
|||||||
3
docs/.idea/.gitignore
generated
vendored
Normal file
3
docs/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
8
docs/.idea/docs.iml
generated
Normal file
8
docs/.idea/docs.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="EMPTY_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
docs/.idea/modules.xml
generated
Normal file
8
docs/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/docs.iml" filepath="$PROJECT_DIR$/.idea/docs.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
docs/.idea/vcs.xml
generated
Normal file
6
docs/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
19
docs/Dockerfile
Normal file
19
docs/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM jetbrains/writerside-builder:243.22562 AS build
|
||||||
|
|
||||||
|
ARG INSTANCE=Writerside/hopframe
|
||||||
|
|
||||||
|
RUN mkdir /opt/sources
|
||||||
|
|
||||||
|
WORKDIR /opt/sources
|
||||||
|
|
||||||
|
ADD Writerside ./Writerside
|
||||||
|
|
||||||
|
RUN export DISPLAY=:99 && Xvfb :99 & /opt/builder/bin/idea.sh helpbuilderinspect --source-dir /opt/sources --product $INSTANCE --runner other --output-dir /opt/wrs-output/
|
||||||
|
|
||||||
|
WORKDIR /opt/wrs-output
|
||||||
|
|
||||||
|
RUN unzip -O UTF-8 webHelpHOPFRAME2-all.zip -d /opt/wrs-output/unzipped-artifact
|
||||||
|
|
||||||
|
FROM httpd:2.4 AS http-server
|
||||||
|
|
||||||
|
COPY --from=build /opt/wrs-output/unzipped-artifact/ /usr/local/apache2/htdocs/
|
||||||
6
docs/Writerside/c.list
Normal file
6
docs/Writerside/c.list
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE categories
|
||||||
|
SYSTEM "https://resources.jetbrains.com/writerside/1.0/categories.dtd">
|
||||||
|
<categories>
|
||||||
|
<category id="wrs" name="Writerside documentation" order="1"/>
|
||||||
|
</categories>
|
||||||
13
docs/Writerside/cfg/buildprofiles.xml
Normal file
13
docs/Writerside/cfg/buildprofiles.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE buildprofiles SYSTEM "https://resources.jetbrains.com/writerside/1.0/build-profiles.dtd">
|
||||||
|
<buildprofiles xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/build-profiles.xsd"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
|
||||||
|
<build-profile instance="hopframe">
|
||||||
|
<sitemap priority="0.35" change-frequency="monthly"/>
|
||||||
|
<variables>
|
||||||
|
<noindex-content>false</noindex-content>
|
||||||
|
</variables>
|
||||||
|
</build-profile>
|
||||||
|
|
||||||
|
</buildprofiles>
|
||||||
7
docs/Writerside/cfg/glossary.xml
Normal file
7
docs/Writerside/cfg/glossary.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE terms SYSTEM "https://resources.jetbrains.com/writerside/1.0/glossary.dtd">
|
||||||
|
<terms>
|
||||||
|
<term name="foo">
|
||||||
|
Description of what "foo" is.
|
||||||
|
</term>
|
||||||
|
</terms>
|
||||||
32
docs/Writerside/hopframe.tree
Normal file
32
docs/Writerside/hopframe.tree
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE instance-profile
|
||||||
|
SYSTEM "https://resources.jetbrains.com/writerside/1.0/product-profile.dtd">
|
||||||
|
|
||||||
|
<instance-profile id="hopframe"
|
||||||
|
name="HopFrame"
|
||||||
|
start-page="Overview.md">
|
||||||
|
|
||||||
|
<toc-element topic="Overview.md"/>
|
||||||
|
<toc-element topic="Installation.md"/>
|
||||||
|
<toc-element topic="Authentication.md"/>
|
||||||
|
<toc-element toc-title="Core Module">
|
||||||
|
<toc-element toc-title="Configurations">
|
||||||
|
<toc-element topic="HopFrameConfig.md"/>
|
||||||
|
<toc-element topic="DbContextConfig.md"/>
|
||||||
|
<toc-element topic="TableConfig.md"/>
|
||||||
|
<toc-element topic="PropertyConfig.md"/>
|
||||||
|
</toc-element>
|
||||||
|
<toc-element topic="Callbacks.md"/>
|
||||||
|
</toc-element>
|
||||||
|
<toc-element toc-title="Web Module">
|
||||||
|
<toc-element toc-title="Interface">
|
||||||
|
<toc-element topic="Custom-Views.md"/>
|
||||||
|
<toc-element topic="Table.md"/>
|
||||||
|
<toc-element topic="Dashboard.md"/>
|
||||||
|
</toc-element>
|
||||||
|
<toc-element topic="Plugins.md">
|
||||||
|
<toc-element topic="Events.md">
|
||||||
|
</toc-element>
|
||||||
|
</toc-element>
|
||||||
|
</toc-element>
|
||||||
|
</instance-profile>
|
||||||
BIN
docs/Writerside/images/dashboard.png
Normal file
BIN
docs/Writerside/images/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/Writerside/images/editor.png
Normal file
BIN
docs/Writerside/images/editor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
BIN
docs/Writerside/images/table.png
Normal file
BIN
docs/Writerside/images/table.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
9
docs/Writerside/redirection-rules.xml
Normal file
9
docs/Writerside/redirection-rules.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE rules SYSTEM "https://resources.jetbrains.com/writerside/1.0/redirection-rules.dtd">
|
||||||
|
<rules>
|
||||||
|
<!-- format is as follows
|
||||||
|
<rule id="<unique id>">
|
||||||
|
<accepts>page.html</accepts>
|
||||||
|
</rule>
|
||||||
|
-->
|
||||||
|
</rules>
|
||||||
35
docs/Writerside/topics/Authentication.md
Normal file
35
docs/Writerside/topics/Authentication.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Authentication
|
||||||
|
|
||||||
|
The HopFrame is a powerful tool to manage your backend data. So you probably don't want anybody to access the pages.
|
||||||
|
Luckily the HopFrame supports policy based authentication. By default, everybody is allowed to access the whole
|
||||||
|
HopFrame, but you can restrict that by registering a scoped service implementing the `IHopFrameAuthHandler`.
|
||||||
|
If no service is registered, the default handler gets registered, but it lets any traffic pass.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Create a service that handles authentication:
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public class AuthService(IAuthStore store) : IHopFrameAuthHandler {
|
||||||
|
|
||||||
|
public async Task<bool> IsAuthenticatedAsync(string? policy) {
|
||||||
|
var currentUser = await store.GetCurrentUser();
|
||||||
|
return await store.IsPermitted(currentUser, policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetCurrentUserDisplayNameAsync() {
|
||||||
|
var currentUser = await store.GetCurrentUser();
|
||||||
|
return currentUser.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now register it in the DI container:
|
||||||
|
|
||||||
|
```C#
|
||||||
|
builder.Services.AddScoped<IHopFrameAuthHandler, AuthService>();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hint:** You can display the current users name in the ui by enabling the feature in
|
||||||
|
the [HopFrameConfig](HopFrameConfig.md#displayuserinfo).
|
||||||
31
docs/Writerside/topics/Callbacks.md
Normal file
31
docs/Writerside/topics/Callbacks.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Callbacks
|
||||||
|
|
||||||
|
Callbacks are a way of executing actions on curtain events in the web ui.
|
||||||
|
|
||||||
|
## Registering a callback handler
|
||||||
|
|
||||||
|
You can register a callback handler using the method provided in the [](TableConfig.md):
|
||||||
|
|
||||||
|
```c#
|
||||||
|
table.AddCallbackHandler(CallbackType.DeleteEntry, (user, services) => {
|
||||||
|
var logger = services.GetRequiredService<ILogger<Program>>();
|
||||||
|
logger.LogInformation("User {user} deleted!", user.Username);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The callback handler takes the entity that's modified and a `IServiceProvider` as arguments
|
||||||
|
and can either be synchronous or asynchronous.
|
||||||
|
|
||||||
|
## Callback types
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public enum CallbackType {
|
||||||
|
CreateEntry = 0,
|
||||||
|
UpdateEntry = 1,
|
||||||
|
DeleteEntry = 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `CallbackType.CreateEntry`: The handler gets executed, when an entity is created.
|
||||||
|
- `CallbackType.UpdateEntry`: The handler gets executed, when an entity is updated.
|
||||||
|
- `CallbackType.DeleteEntry`: The handler gets executed, when an entity is deleted.
|
||||||
101
docs/Writerside/topics/Custom-Views.md
Normal file
101
docs/Writerside/topics/Custom-Views.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Custom Views
|
||||||
|
|
||||||
|
You can also add your own pages to the HopFrame UI by defining the routes as custom views.
|
||||||
|
You can do that by using the following extension methods for the [](HopFrameConfig.md):
|
||||||
|
|
||||||
|
## Configuration methods
|
||||||
|
|
||||||
|
### AddCustomView (With configurator delegate)
|
||||||
|
|
||||||
|
Creates an entry to the side menu and dashboard with a custom URL.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
HopFrameConfigurator AddCustomView(string name, string url, Action<CustomViewConfigurator> configuratorDelegate)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `configurator`: The configurator for the HopFrame config that is being created.
|
||||||
|
- `name`: The name of the navigation entry.
|
||||||
|
- `url`: The target URL of the navigation entry.
|
||||||
|
- `configuratorDelegate`: The delegate for configuring the view.
|
||||||
|
|
||||||
|
- **Returns:** `HopFrameConfigurator`
|
||||||
|
|
||||||
|
### AddCustomView (Without configurator delegate)
|
||||||
|
|
||||||
|
Creates an entry to the side menu and dashboard with a custom URL.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
CustomViewConfigurator AddCustomView(string name, string url)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `configurator`: The configurator for the HopFrame config that is being created.
|
||||||
|
- `name`: The name of the navigation entry.
|
||||||
|
- `url`: The target URL of the navigation entry.
|
||||||
|
|
||||||
|
- **Returns:** `CustomViewConfigurator`
|
||||||
|
|
||||||
|
## CustomViewConfigurator
|
||||||
|
|
||||||
|
### SetDescription
|
||||||
|
|
||||||
|
Sets the description displayed in the dashboard.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
CustomViewConfigurator SetDescription(string description)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `description`: The desired description.
|
||||||
|
|
||||||
|
- **Returns:** `CustomViewConfigurator`
|
||||||
|
|
||||||
|
### SetPolicy
|
||||||
|
|
||||||
|
Sets the policy needed in order to access the view.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
CustomViewConfigurator SetPolicy(string policy)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `policy`: The desired policy.
|
||||||
|
|
||||||
|
- **Returns:** `CustomViewConfigurator`
|
||||||
|
|
||||||
|
### SetIcon
|
||||||
|
|
||||||
|
Sets the icon displayed in the sidebar.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
CustomViewConfigurator SetIcon(string icon)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `icon`: The desired [fluent-icon](https://www.fluentui-blazor.net/Icon#explorer).
|
||||||
|
|
||||||
|
- **Returns:** `CustomViewConfigurator`
|
||||||
|
|
||||||
|
### SetLinkMatch
|
||||||
|
|
||||||
|
Sets the rule for the sidebar to determine if the link is active.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
CustomViewConfigurator SetLinkMatch(NavLinkMatch match)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `match`: The desired match rule.
|
||||||
|
|
||||||
|
- **Returns:** `CustomViewConfigurator`
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```C#
|
||||||
|
builder.Services.AddHopFrame(options => {
|
||||||
|
options.AddCustomView("Counter", "/counter")
|
||||||
|
.SetDescription("A custom view")
|
||||||
|
.SetPolicy("counter.view");
|
||||||
|
});
|
||||||
|
```
|
||||||
9
docs/Writerside/topics/Dashboard.md
Normal file
9
docs/Writerside/topics/Dashboard.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Dashboard
|
||||||
|
|
||||||
|
The dashboard gives you an overview of all pages accessible through the HopFrame interface.
|
||||||
|
|
||||||
|
An example configuration could lead to something like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You could use the sidebar or the `Open` button to open any page that you have access to.
|
||||||
38
docs/Writerside/topics/DbContextConfig.md
Normal file
38
docs/Writerside/topics/DbContextConfig.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# DbContextConfig
|
||||||
|
|
||||||
|
This config contains all configurations for the given DbContext type.
|
||||||
|
|
||||||
|
## Configuration methods
|
||||||
|
|
||||||
|
### Table (With configurator)
|
||||||
|
|
||||||
|
Configures the table of the `DbContext` using the provided configurator.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
DbContextConfigurator<TDbContext> Table<TModel>(Action<TableConfigurator<TModel>> configurator) where TModel : class
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Type Parameters:**
|
||||||
|
- `TModel`: The model of the table for identifying the correct one.
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `configurator`: Used for configuring the table.
|
||||||
|
|
||||||
|
- **Returns:** `DbContextConfigurator<TDbContext>`
|
||||||
|
|
||||||
|
- **See Also:** [](TableConfig.md)
|
||||||
|
|
||||||
|
### Table (Without configurator)
|
||||||
|
|
||||||
|
Configures the table of the `DbContext`.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> Table<TModel>() where TModel : class
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Type Parameters:**
|
||||||
|
- `TModel`: The model of the table for identifying the correct one.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
- **See Also:** [](TableConfig.md)
|
||||||
214
docs/Writerside/topics/Events.md
Normal file
214
docs/Writerside/topics/Events.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# Events
|
||||||
|
|
||||||
|
## Base event
|
||||||
|
|
||||||
|
Every event inherits from the base event, so these properties and methods are always available
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public abstract class HopFrameEventArgs {
|
||||||
|
public TSender Sender { get; }
|
||||||
|
public bool IsCanceled { get; }
|
||||||
|
public TableConfig Table { get; }
|
||||||
|
|
||||||
|
public void SetCancelled(bool canceled);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
- **Sender**: The sender of the event.
|
||||||
|
- **Type:** `TSender`
|
||||||
|
- **IsCanceled**: Indicates whether the event is canceled.
|
||||||
|
- **Type:** `bool`
|
||||||
|
- **Table**: The table configuration related to the event.
|
||||||
|
- **Type:** `TableConfig`
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
- **SetCancelled**
|
||||||
|
- **Parameters:**
|
||||||
|
- `canceled`: A boolean value to set the cancellation state.
|
||||||
|
- **Returns:** `void`
|
||||||
|
|
||||||
|
## DeleteEntryEvent
|
||||||
|
|
||||||
|
Event arguments for a delete entry event.
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public sealed class DeleteEntryEvent : HopFrameEventArgs {
|
||||||
|
public object Entity { get; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
- **Entity**: The entity being deleted.
|
||||||
|
- **Type:** `object`
|
||||||
|
|
||||||
|
## CreateEntryEvent
|
||||||
|
|
||||||
|
Event arguments for a create entry event.
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public sealed class CreateEntryEvent : HopFrameEventArgs {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## UpdateEntryEvent
|
||||||
|
|
||||||
|
Event arguments for an update entry event.
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public sealed class UpdateEntryEvent : HopFrameEventArgs {
|
||||||
|
public object Entity { get; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
- **Entity**: The entity being updated.
|
||||||
|
- **Type:** `object`
|
||||||
|
|
||||||
|
## SelectEntryEvent
|
||||||
|
|
||||||
|
Event arguments for a select entry event.
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public sealed class SelectEntryEvent : HopFrameEventArgs {
|
||||||
|
public object Entity { get; }
|
||||||
|
public bool Selected { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
- **Entity**: The entity being selected.
|
||||||
|
- **Type:** `object`
|
||||||
|
- **Selected**: Indicates whether the entity is selected.
|
||||||
|
- **Type:** `bool`
|
||||||
|
|
||||||
|
## PageChangeEvent
|
||||||
|
|
||||||
|
Event arguments for a page change event.
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public sealed class PageChangeEvent : HopFrameEventArgs {
|
||||||
|
public int CurrentPage { get; }
|
||||||
|
public int TotalPages { get; }
|
||||||
|
public int NewPage { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
- **CurrentPage**: The current page number.
|
||||||
|
- **Type:** `int`
|
||||||
|
- **TotalPages**: The total number of pages.
|
||||||
|
- **Type:** `int`
|
||||||
|
- **NewPage**: The new page number to navigate to.
|
||||||
|
- **Type:** `int`
|
||||||
|
|
||||||
|
## ReloadEvent
|
||||||
|
|
||||||
|
Event arguments for a reload event.
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public sealed class ReloadEvent : HopFrameEventArgs {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SearchEvent
|
||||||
|
|
||||||
|
Event arguments for a search event.
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public sealed class SearchEvent : HopFrameEventArgs {
|
||||||
|
public string SearchTerm { get; set; }
|
||||||
|
public int CurrentPage { get; }
|
||||||
|
|
||||||
|
public void SetSearchResult(IEnumerable result, int totalPages);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
- **SearchTerm**: The search term used.
|
||||||
|
- **Type:** `string`
|
||||||
|
- **CurrentPage**: The current page number.
|
||||||
|
- **Type:** `int`
|
||||||
|
- **SearchResult**: The search results.
|
||||||
|
- **Type:** `IEnumerable<object>?`
|
||||||
|
- **TotalPages**: The total number of pages of search results.
|
||||||
|
- **Type:** `int`
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
- **SetSearchResult**
|
||||||
|
- **Parameters:**
|
||||||
|
- `result`: The current page of search results.
|
||||||
|
- `totalPages`: The total pages of search results.
|
||||||
|
- **Returns:** `void`
|
||||||
|
|
||||||
|
## TableInitializedEvent
|
||||||
|
|
||||||
|
Event arguments for a table initialization event.
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public class TableInitializedEvent : HopFrameEventArgs {
|
||||||
|
public List<PluginButton> PluginButtons { get; };
|
||||||
|
public DefaultButtonToggles DefaultButtons { get; set; };
|
||||||
|
|
||||||
|
public void AddPageButton(string title, Func<Task> callback, bool pushRight = false, IconInfo? icon = null);
|
||||||
|
public void AddPageButton(string title, Action callback, bool pushRight = false, IconInfo? icon = null);
|
||||||
|
public void AddPageButton<TEntity>(string title, Func<Task> callback, bool pushRight = false, IconInfo? icon = null);
|
||||||
|
public void AddPageButton<TEntity>(string title, Action callback, bool pushRight = false, IconInfo? icon = null);
|
||||||
|
|
||||||
|
public void AddEntityButton(IconInfo icon, Func<object, TableConfig, Task> callback);
|
||||||
|
public void AddEntityButton(IconInfo icon, Action<object, TableConfig> callback);
|
||||||
|
public void AddEntityButton<TEntity>(IconInfo icon, Func<TEntity, TableConfig, Task> callback);
|
||||||
|
public void AddEntityButton<TEntity>(IconInfo icon, Action<TEntity, TableConfig> callback);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
- **PluginButtons**: The list of plugin buttons for the table.
|
||||||
|
- **Type:** `List<PluginButton>`
|
||||||
|
- **DefaultButtons**: The default button toggles for the table.
|
||||||
|
- **Type:** `DefaultButtonToggles`
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
- **AddPageButton**
|
||||||
|
- **Parameters:**
|
||||||
|
- `title`: The title of the button.
|
||||||
|
- `callback`: The callback function for the button.
|
||||||
|
- `pushRight`: Indicates whether to push the button to the right. (default: `false`)
|
||||||
|
- `icon`: The icon for the button. (default: `null`)
|
||||||
|
- **Returns:** `void`
|
||||||
|
|
||||||
|
- **AddEntityButton**
|
||||||
|
- **Parameters:**
|
||||||
|
- `icon`: The icon for the button.
|
||||||
|
- `callback`: The callback function for the button.
|
||||||
|
- **Returns:** `void`
|
||||||
|
|
||||||
|
## ValidationEvent
|
||||||
|
|
||||||
|
Event arguments for a validation event.
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public sealed class ValidationEvent : HopFrameEventArgs {
|
||||||
|
public IList<string> Errors { get; }
|
||||||
|
public PropertyConfig Property { get; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
- **Errors**: The list of validation errors.
|
||||||
|
- **Type:** `IList<string>`
|
||||||
|
- **Property**: The property being validated.
|
||||||
|
- **Type:** `PropertyConfig`
|
||||||
159
docs/Writerside/topics/HopFrameConfig.md
Normal file
159
docs/Writerside/topics/HopFrameConfig.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# HopFrameConfig
|
||||||
|
|
||||||
|
The HopFrame config is the global object containing all configurations made for the HopFrame.
|
||||||
|
It is registered as a singleton and can be injected by any service.
|
||||||
|
But it should be treated as a read only dependency because changing the configuration during runtime is not tested and may cause bugs.
|
||||||
|
|
||||||
|
## Changing the configuration
|
||||||
|
|
||||||
|
As already mentioned in the [](Installation.md), you configure the HopFrame using the extension method of the `IServiceCollection` named `AddHopFrame`.
|
||||||
|
This extension method eiter takes a `HopFrameConfig` or a configurator action with a `HopFrameConfigurator` as an argument.
|
||||||
|
You can optionally also provide a `LibraryConfiguration` for the Fluent UI library and a toggle named `addRazorComponents` which disables the calls for adding
|
||||||
|
Razor pages with interactive server components if you want to do this yourself.
|
||||||
|
|
||||||
|
### Mapping the HopFrame pages
|
||||||
|
|
||||||
|
In order for the HopFrame pages to be served you need to add them to your application.
|
||||||
|
You can do that in two ways:
|
||||||
|
|
||||||
|
1. Just use the HopFrame as the razor host (Useful for APIs)
|
||||||
|
Just map the HopFrame before you run your application:
|
||||||
|
```c#
|
||||||
|
app.MapHopFrame();
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add the HopFrame to your Razor container (Useful for Blazor web apps)
|
||||||
|
Add the HopFrame to the `MapRazorComponents` call:
|
||||||
|
```C#
|
||||||
|
app.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveServerRenderMode()
|
||||||
|
.AddHopFramePages();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```C#
|
||||||
|
builder.Services.AddHopFrame(options => {
|
||||||
|
options.DisplayUserInfo(false);
|
||||||
|
options.AddDbContext<DatabaseContext>(context => {
|
||||||
|
context.Table<User>(table => {
|
||||||
|
table.Property(u => u.Password)
|
||||||
|
.DisplayValue(false);
|
||||||
|
|
||||||
|
table.Property(u => u.Id)
|
||||||
|
.IsSortable(false)
|
||||||
|
.SetOrderIndex(3);
|
||||||
|
|
||||||
|
table.SetViewPolicy("policy");
|
||||||
|
|
||||||
|
table.Property(u => u.Posts)
|
||||||
|
.FormatEach<Post>((post, _) => post.Caption);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddCustomView("Counter", "/counter")
|
||||||
|
.SetDescription("A custom view")
|
||||||
|
.SetPolicy("counter.view");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration methods
|
||||||
|
|
||||||
|
### AddDbContext (With configurator)
|
||||||
|
|
||||||
|
Adds all tables defined in the `DbContext` to the HopFrame UI and configures it using the provided configurator.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
HopFrameConfigurator AddDbContext<TDbContext>(Action<DbContextConfigurator<TDbContext>> configurator) where TDbContext : DbContext
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Type Parameters:**
|
||||||
|
- `TDbContext`: The `DbContext` from which all tables should be added.
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `configurator`: Used for configuring the `DbContext`.
|
||||||
|
|
||||||
|
- **Returns:** `HopFrameConfigurator`
|
||||||
|
|
||||||
|
- **See Also:** [](DbContextConfig.md)
|
||||||
|
|
||||||
|
### AddDbContext (without configurator)
|
||||||
|
|
||||||
|
Adds all tables defined in the `DbContext` to the HopFrame UI and configures it.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
DbContextConfigurator<TDbContext> AddDbContext<TDbContext>() where TDbContext : DbContext
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Type Parameters:**
|
||||||
|
- `TDbContext`: The `DbContext` from which all tables should be added.
|
||||||
|
|
||||||
|
- **Returns:** `DbContextConfigurator<TDbContext>`
|
||||||
|
|
||||||
|
- **See Also:** [](DbContextConfig.md)
|
||||||
|
|
||||||
|
### HasDbContext
|
||||||
|
|
||||||
|
Checks if a context is already registered in the HopFrame.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
bool HasDbContext<TDbContext>() where TDbContext : DbContext
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Type Parameters:**
|
||||||
|
- `TDbContext`: The context that should be checked.
|
||||||
|
|
||||||
|
- **Returns:** `true` if the context is already registered, `false` if not.
|
||||||
|
|
||||||
|
### GetDbContext
|
||||||
|
|
||||||
|
Returns a configurator for the context if it was already defined.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext : DbContext
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Type Parameters:**
|
||||||
|
- `TDbContext`
|
||||||
|
|
||||||
|
- **Returns:** The configurator of the context if it already was defined, `null` if not.
|
||||||
|
|
||||||
|
### DisplayUserInfo
|
||||||
|
|
||||||
|
Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin UI.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
HopFrameConfigurator DisplayUserInfo(bool display)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `display`: A boolean value to set if the user info should be displayed.
|
||||||
|
|
||||||
|
- **Returns:** `HopFrameConfigurator`
|
||||||
|
|
||||||
|
### SetBasePolicy
|
||||||
|
|
||||||
|
Sets a default policy that every user needs to have in order to access the admin UI.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
HopFrameConfigurator SetBasePolicy(string basePolicy)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `basePolicy`: The default policy string.
|
||||||
|
|
||||||
|
- **Returns:** `HopFrameConfigurator`
|
||||||
|
|
||||||
|
### SetLoginPage
|
||||||
|
|
||||||
|
Sets a custom login page to redirect to if the request to the admin UI was unauthorized.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
HopFrameConfigurator SetLoginPage(string url)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `url`: The URL of the custom login page.
|
||||||
|
|
||||||
|
- **Returns:** `HopFrameConfigurator`
|
||||||
|
|
||||||
31
docs/Writerside/topics/Installation.md
Normal file
31
docs/Writerside/topics/Installation.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
Install the nuget package using the CLI or the UI of your IDE:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add package HopFrame.Web
|
||||||
|
```
|
||||||
|
|
||||||
|
## Minimal configuration
|
||||||
|
|
||||||
|
Configuring HopFrame is straightforward and flexible. You can easily define your contexts, tables, and their properties using the provided configurators.
|
||||||
|
Simply use your editors intelli-sense to find out what you can configure.
|
||||||
|
|
||||||
|
|
||||||
|
```c#
|
||||||
|
builder.Services.AddHopFrame(options => {
|
||||||
|
options.AddDbContext<DatabaseContext>();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you need to map the frontend pages in your application:
|
||||||
|
|
||||||
|
```c#
|
||||||
|
app.MapHopFrame();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- Navigate to `/admin` to access the admin dashboard and start managing your tables.
|
||||||
|
- Use the side menu to switch between different tables.
|
||||||
|
- Utilize the built-in CRUD functionality to manage your data seamlessly.
|
||||||
14
docs/Writerside/topics/Overview.md
Normal file
14
docs/Writerside/topics/Overview.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Overview
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Dynamic Table Management**: Create, edit, and delete records dynamically with support for various data types including numeric, text, boolean, dates, and relational data.
|
||||||
|
- **Role-Based Access Control (RBAC)**: Implement fine-grained access control policies for viewing, creating, updating, and deleting records.
|
||||||
|
- **Modern Design**: A modern and user-friendly interface built with Fluent UI components, ensuring easy to use and pleasing administration pages.
|
||||||
|
- **Validation and Error Handling**: Comprehensive input validation and error handling to ensure data integrity and provide feedback to users.
|
||||||
|
- **Support for Complex Data Relationships**: Manage complex relationships between data entities with ease.
|
||||||
|
|
||||||
78
docs/Writerside/topics/Plugins.md
Normal file
78
docs/Writerside/topics/Plugins.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Plugins
|
||||||
|
|
||||||
|
If the default functionality of the HopFrame does not fit your needs, you can easily extend the pages
|
||||||
|
by using Plugins. They are registered as scoped services so you can use DI like everywhere else.
|
||||||
|
|
||||||
|
## Add a plugin
|
||||||
|
|
||||||
|
Create a class that represents the plugin:
|
||||||
|
|
||||||
|
```C#
|
||||||
|
public class SearchExtension {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the plugin to the HopFrame by using the extension method on the [](HopFrameConfig.md):
|
||||||
|
|
||||||
|
```C#
|
||||||
|
builder.Services.AddHopFrame(options => {
|
||||||
|
options.AddPlugin<SearchExtension>();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuring the plugin
|
||||||
|
|
||||||
|
If you want to change the HopFrame configuration from within your plugin, you can create a static method
|
||||||
|
and decorate it with the `PluginConfigurator` attribute. Here you can inject the `HopFrameConfigurator`
|
||||||
|
as an argument and change the configuration. Keep in mind, that this function automatically gets called
|
||||||
|
when you register your plugin, so any changes after that override the changes made in the plugin.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```C#
|
||||||
|
[PluginConfigurator]
|
||||||
|
public static void Configure(HopFrameConfigurator configurator) {
|
||||||
|
configurator.AddCustomView("Counter", "/counter")
|
||||||
|
.SetDescription("A custom view")
|
||||||
|
.SetPolicy("counter.view");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
The HopFrame provides various [events](Events.md) that can change how the corresponding action behaves. You can register
|
||||||
|
event handlers similar to the [configurator method](#configuring-the-plugin). Create a method, that is **not** static
|
||||||
|
and decorate it with the `EventHandler` attribute. This method can return either `void` or a `Task`. Then declare the
|
||||||
|
Event type as an argument and the function gets automatically registered as an event handler for the corresponding event.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```C#
|
||||||
|
[EventHandler]
|
||||||
|
public async Task OnSearch(SearchEvent e) {
|
||||||
|
var result = await searchHandler.Search(e.Table, e.SearchTerm);
|
||||||
|
e.SetSearchResult(result.Items, result.TotalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
[EventHandler]
|
||||||
|
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();
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
286
docs/Writerside/topics/PropertyConfig.md
Normal file
286
docs/Writerside/topics/PropertyConfig.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# PropertyConfig
|
||||||
|
|
||||||
|
This configuration contains all configurations for the given property type on the table.
|
||||||
|
|
||||||
|
## Configuration methods
|
||||||
|
|
||||||
|
### SetDisplayName
|
||||||
|
|
||||||
|
Sets the title displayed in the table header and edit dialog.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetDisplayName(string displayName)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `displayName`: The display name for the property.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### List
|
||||||
|
|
||||||
|
Determines if the property should appear in the table, if not the property is also set to be not searchable.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> List(bool list)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `list`: A boolean value to set if the property should appear in the table.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### IsSortable
|
||||||
|
|
||||||
|
Determines if the table can be sorted by the property.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> IsSortable(bool sortable)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `sortable`: A boolean value to set if the property is sortable.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### IsSearchable
|
||||||
|
|
||||||
|
Determines if the property get taken into account for search results.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> IsSearchable(bool searchable)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `searchable`: A boolean value to set if the property is searchable.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### SetDisplayedProperty
|
||||||
|
|
||||||
|
Determines if the value that should be displayed instead of the string representation of the type.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetDisplayedProperty<TInnerProp>(Expression<Func<TProp, TInnerProp>> propertyExpression)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Type Parameters:**
|
||||||
|
- `TInnerProp`: The inner property type to display.
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `propertyExpression`: The expression to determine the property.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### Format (Synchronous)
|
||||||
|
|
||||||
|
Determines the value that's displayed in the admin UI.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> Format(Func<TProp, IServiceProvider, string> formatter)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `formatter`: The function to format the value.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
- **See Also:** [](#setdisplayedproperty)
|
||||||
|
|
||||||
|
### Format (Asynchronous)
|
||||||
|
|
||||||
|
Determines the value that's displayed in the admin UI.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> Format(Func<TProp, IServiceProvider, Task<string>> formatter)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `formatter`: The function to format the value.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### FormatEach (Synchronous)
|
||||||
|
|
||||||
|
Determines the value that's displayed for each entry in the list.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> FormatEach<TInnerProp>(Func<TInnerProp, IServiceProvider, string> formatter)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `formatter`: The function to format the value for each entry.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### FormatEach (Asynchronous)
|
||||||
|
|
||||||
|
Determines the value that's displayed for each entry in the list.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> FormatEach<TInnerProp>(Func<TInnerProp, IServiceProvider, Task<string>> formatter)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `formatter`: The function to format the value for each entry.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### SetParser (Synchronous)
|
||||||
|
|
||||||
|
Determines the function used for parsing the value provided in the editor dialog to the actual property value.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetParser(Func<string, IServiceProvider, TProp> parser)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `parser`: The function to parse the value.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### SetParser (Asynchronous)
|
||||||
|
|
||||||
|
Determines the function used for parsing the value provided in the editor dialog to the actual property value.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetParser(Func<string, IServiceProvider, Task<TProp>> parser)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `parser`: The function to parse the value.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### SetEditable
|
||||||
|
|
||||||
|
Determines if the value can be edited in the admin UI. If true, the value can still be initially set, but not modified.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetEditable(bool editable)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `editable`: A boolean value to set if the property is editable.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### SetCreatable
|
||||||
|
|
||||||
|
Determines if the initial value can be edited in the admin UI. If true the value will not be visible in the create dialog.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetCreatable(bool creatable)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `creatable`: A boolean value to set if the property is creatable.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### DisplayValue
|
||||||
|
|
||||||
|
Determines if the value should be displayed in the admin UI (useful for secrets like passwords etc.).
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> DisplayValue(bool display)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `display`: A boolean value to set if the property value is displayed.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### IsTextArea
|
||||||
|
|
||||||
|
Determines if the admin UI should use a text area for modifying the value.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> IsTextArea(bool textField)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `textField`: A boolean value to set if the property is a text area.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### SetTextAreaRows
|
||||||
|
|
||||||
|
Determines the initial size of the text area field.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetTextAreaRows(int rows)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `rows`: The number of rows for the text area.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### SetValidator (Synchronous)
|
||||||
|
|
||||||
|
Determines the validator used for the property value before saving.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetValidator(Func<TProp?, IServiceProvider, IEnumerable<string>> validator)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `validator`: The function to validate the property value.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### SetValidator (Asynchronous)
|
||||||
|
|
||||||
|
Determines the validator used for the property value before saving.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetValidator(Func<TProp?, IServiceProvider, Task<IEnumerable<string>>> validator)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `validator`: The function to validate the property value.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### SetOrderIndex
|
||||||
|
|
||||||
|
Determines the order index for the property in the admin UI.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetOrderIndex(int index)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `index`: The order index for the property.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
- **See Also:** [](TableConfig.md#setorderindex)
|
||||||
|
|
||||||
|
### SetDisplayLength
|
||||||
|
|
||||||
|
Sets the maximum character length displayed in the admin UI (not in the editor dialog).
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> SetDisplayLength(int maxLength)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `maxLength`: The maximum length of characters to be displayed.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
### ForceRelation
|
||||||
|
|
||||||
|
Forces a property to be treated as a relation.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> ForceRelation(bool isEnumerable = false, bool isRequired = true)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `isEnumerable`: Determines if it is possible to assign multiple objects to the property.
|
||||||
|
- `isRequired`: Determines if the property is nullable.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
41
docs/Writerside/topics/Table.md
Normal file
41
docs/Writerside/topics/Table.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Table
|
||||||
|
|
||||||
|
On the table page you can view, edit, delete and create entries in that table.
|
||||||
|
You can use the many configuration methods provided by the [](TableConfig.md) to modify the look
|
||||||
|
of this page.
|
||||||
|
|
||||||
|
An example configuration could look something like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here you can use the various buttons to interact with your entities.
|
||||||
|
|
||||||
|
## Data grid
|
||||||
|
|
||||||
|
The main aspect of this site is the data grid that displays all your entries of that table.
|
||||||
|
For performance reasons this data is paginated, you can **change the page** at the bottom of the screen
|
||||||
|
using either the arrow button or the page selector. You can **sort** your entries by clicking on
|
||||||
|
the name of the column. If you don't want a column to be sortable, you can configure this in the
|
||||||
|
[PropertyConfig](PropertyConfig.md#issortable).
|
||||||
|
|
||||||
|
## Search
|
||||||
|
|
||||||
|
The search bar will search through all entities in your database, not only the ones displayed.
|
||||||
|
Simply enter a search term and the search is performed automatically. You can configure the
|
||||||
|
columns that should be searchable in the [PropertyConfig](PropertyConfig.md#issearchable)
|
||||||
|
|
||||||
|
## Edit dialog
|
||||||
|
|
||||||
|
If you click on the pen button next to one of the entries, the edit dialog will appear,
|
||||||
|
it could look something like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can modify the data of the selected entity based on your [PropertyConfigs](PropertyConfig.md).
|
||||||
|
The HopFrame can handle various property types like strings, numbers, enums, relations and many more.
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
The HopFrame also supports input validation. By default, a required validation is automatically applied
|
||||||
|
to every property that's not nullable. You can change the validation behavior in the
|
||||||
|
[PropertyConfig](PropertyConfig.md#setvalidator-synchronous).
|
||||||
191
docs/Writerside/topics/TableConfig.md
Normal file
191
docs/Writerside/topics/TableConfig.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# TableConfig
|
||||||
|
|
||||||
|
This configuration contains all configurations for the given table type.
|
||||||
|
|
||||||
|
## Configuration methods
|
||||||
|
|
||||||
|
### Ignore
|
||||||
|
|
||||||
|
Determines if the table should be ignored in the admin UI.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> Ignore(bool ignore)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `ignore`: A boolean value to set if the table should be ignored.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
### Property (With configurator)
|
||||||
|
|
||||||
|
Configures the property of the table using the provided configurator.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression, Action<PropertyConfigurator<TProp>> configurator)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `propertyExpression`: Used for determining the property.
|
||||||
|
- `configurator`: Used for configuring the property.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
- **See Also:** [](PropertyConfig.md)
|
||||||
|
|
||||||
|
### Property (Without configurator)
|
||||||
|
|
||||||
|
Configures the property of the table.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<TProp> Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `propertyExpression`: Used for determining the property.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<TProp>`
|
||||||
|
|
||||||
|
- **See Also:** [](PropertyConfig.md)
|
||||||
|
|
||||||
|
### AddVirtualProperty (With configurator)
|
||||||
|
|
||||||
|
Adds a virtual property to the table view and configures it using the provided configurator (this property will not appear in the editor).
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template, Action<PropertyConfigurator<string>> configurator)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `name`: The name of the virtual property.
|
||||||
|
- `template`: The template used for generating the property value.
|
||||||
|
- `configurator`: Used for configuring the virtual property.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
- **See Also:** [](PropertyConfig.md)
|
||||||
|
|
||||||
|
### AddVirtualProperty (Synchronous)
|
||||||
|
|
||||||
|
Adds a virtual property to the table view (this property will not appear in the editor).
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `name`: The name of the virtual property.
|
||||||
|
- `template`: The template used for generating the property value.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<string>`
|
||||||
|
|
||||||
|
- **See Also:** [](PropertyConfig.md)
|
||||||
|
|
||||||
|
### AddVirtualProperty (Asynchronous)
|
||||||
|
|
||||||
|
Adds a virtual property to the table view (this property will not appear in the editor).
|
||||||
|
|
||||||
|
```c#
|
||||||
|
PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, Task<string>> template)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `name`: The name of the virtual property.
|
||||||
|
- `template`: The template used for generating the property value.
|
||||||
|
|
||||||
|
- **Returns:** `PropertyConfigurator<string>`
|
||||||
|
|
||||||
|
- **See Also:** [](PropertyConfig.md)
|
||||||
|
|
||||||
|
### SetDisplayName
|
||||||
|
|
||||||
|
Determines the name for the table used in the admin UI and URL for the table page.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> SetDisplayName(string name)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `name`: The display name for the table.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
### SetDescription
|
||||||
|
|
||||||
|
Determines the description displayed in the admin UI.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> SetDescription(string description)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `description`: The description for the table.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
### SetOrderIndex
|
||||||
|
|
||||||
|
Determines the order index for the table in the admin UI.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> SetOrderIndex(int index)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `index`: The order index for the table.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
- **See Also:** [](PropertyConfig.md#setorderindex)
|
||||||
|
|
||||||
|
### SetViewPolicy
|
||||||
|
|
||||||
|
Determines the policy needed by a user in order to view the table.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> SetViewPolicy(string policy)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `policy`: The view policy string.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
### SetUpdatePolicy
|
||||||
|
|
||||||
|
Determines the policy needed by a user in order to edit the entries.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> SetUpdatePolicy(string policy)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `policy`: The update policy string.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
### SetCreatePolicy
|
||||||
|
|
||||||
|
Determines the policy needed by a user in order to create entries.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> SetCreatePolicy(string policy)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `policy`: The create policy string.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
|
|
||||||
|
### SetDeletePolicy
|
||||||
|
|
||||||
|
Determines the policy needed by a user in order to delete entries.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> SetDeletePolicy(string policy)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `policy`: The delete policy string.
|
||||||
|
|
||||||
|
- **Returns:** `TableConfigurator<TModel>`
|
||||||
5
docs/Writerside/v.list
Normal file
5
docs/Writerside/v.list
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE vars SYSTEM "https://resources.jetbrains.com/writerside/1.0/vars.dtd">
|
||||||
|
<vars>
|
||||||
|
<var name="product" value="Writerside"/>
|
||||||
|
</vars>
|
||||||
11
docs/Writerside/writerside.cfg
Normal file
11
docs/Writerside/writerside.cfg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE ihp SYSTEM "https://resources.jetbrains.com/writerside/1.0/ihp.dtd">
|
||||||
|
|
||||||
|
<ihp version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/writerside-cfg.xsd">
|
||||||
|
<topics dir="topics"/>
|
||||||
|
<images dir="images" web-path="images"/>
|
||||||
|
<categories src="c.list"/>
|
||||||
|
<vars src="v.list"/>
|
||||||
|
<instance src="hopframe.tree"/>
|
||||||
|
</ihp>
|
||||||
31
src/HopFrame.Core/Callbacks/CallbackTypes.cs
Normal file
31
src/HopFrame.Core/Callbacks/CallbackTypes.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using HopFrame.Core.Config;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.Callbacks;
|
||||||
|
|
||||||
|
public static class CallbackTypes {
|
||||||
|
|
||||||
|
private const string Prefix = "HopFrame.";
|
||||||
|
private const string CreateEntryPrefix = Prefix + "Entry.Create.";
|
||||||
|
private const string UpdateEntryPrefix = Prefix + "Entry.Update.";
|
||||||
|
private const string DeleteEntryPrefix = Prefix + "Entry.Delete.";
|
||||||
|
|
||||||
|
public static string CreateEntry(TableConfig config) => CreateEntryPrefix + config.PropertyName;
|
||||||
|
public static string UpdateEntry(TableConfig config) => UpdateEntryPrefix + config.PropertyName;
|
||||||
|
public static string DeleteEntry(TableConfig config) => DeleteEntryPrefix + config.PropertyName;
|
||||||
|
|
||||||
|
public static string ConstructCallbackName(CallbackType type, TableConfig config) {
|
||||||
|
return type switch {
|
||||||
|
CallbackType.CreateEntry => CreateEntry(config),
|
||||||
|
CallbackType.UpdateEntry => UpdateEntry(config),
|
||||||
|
CallbackType.DeleteEntry => DeleteEntry(config),
|
||||||
|
_ => Prefix
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CallbackType {
|
||||||
|
CreateEntry = 0,
|
||||||
|
UpdateEntry = 1,
|
||||||
|
DeleteEntry = 2
|
||||||
|
}
|
||||||
7
src/HopFrame.Core/Callbacks/HopCallbackHandler.cs
Normal file
7
src/HopFrame.Core/Callbacks/HopCallbackHandler.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace HopFrame.Core.Callbacks;
|
||||||
|
|
||||||
|
public readonly struct HopCallbackHandler(string eventType, Func<object, IServiceProvider, Task> handler) {
|
||||||
|
public Guid Id { get; } = Guid.CreateVersion7();
|
||||||
|
public Func<object, IServiceProvider, Task> Handler { get; } = handler;
|
||||||
|
public string EventType { get; } = eventType;
|
||||||
|
}
|
||||||
14
src/HopFrame.Core/Callbacks/ICallbackEmitter.cs
Normal file
14
src/HopFrame.Core/Callbacks/ICallbackEmitter.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace HopFrame.Core.Callbacks;
|
||||||
|
|
||||||
|
public interface ICallbackEmitter {
|
||||||
|
|
||||||
|
Guid RegisterCallbackHandler(string @event, Func<object, IServiceProvider, Task> handler);
|
||||||
|
|
||||||
|
bool RemoveCallbackHandler(Guid id);
|
||||||
|
|
||||||
|
Task DispatchCallback(string @event, object argument = null!);
|
||||||
|
|
||||||
|
void RemoveAllCallbackHandlers(string @event);
|
||||||
|
void RemoveAllCallbackHandlers();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,9 +5,11 @@ namespace HopFrame.Core.Config;
|
|||||||
public class DbContextConfig {
|
public class DbContextConfig {
|
||||||
public Type ContextType { get; }
|
public Type ContextType { get; }
|
||||||
public List<TableConfig> Tables { get; } = new();
|
public List<TableConfig> Tables { get; } = new();
|
||||||
|
public HopFrameConfig ParentConfig { get; }
|
||||||
|
|
||||||
public DbContextConfig(Type context) {
|
public DbContextConfig(Type context, HopFrameConfig parentConfig) {
|
||||||
ContextType = context;
|
ContextType = context;
|
||||||
|
ParentConfig = parentConfig;
|
||||||
|
|
||||||
foreach (var property in ContextType.GetProperties()) {
|
foreach (var property in ContextType.GetProperties()) {
|
||||||
if (!property.PropertyType.IsGenericType) continue;
|
if (!property.PropertyType.IsGenericType) continue;
|
||||||
@@ -24,7 +26,7 @@ public class DbContextConfig {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A helper class for editing the <see cref="DbContextConfig"/>
|
/// A helper class for editing the <see cref="DbContextConfig"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DbContextConfigurator<TDbContext>(DbContextConfig config) {
|
public sealed class DbContextConfigurator<TDbContext>(DbContextConfig config) {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Internal DbContext configuration that's modified by the helper functions
|
/// The Internal DbContext configuration that's modified by the helper functions
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using HopFrame.Core.Callbacks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace HopFrame.Core.Config;
|
namespace HopFrame.Core.Config;
|
||||||
|
|
||||||
@@ -7,18 +9,21 @@ public class HopFrameConfig {
|
|||||||
public bool DisplayUserInfo { get; set; } = true;
|
public bool DisplayUserInfo { get; set; } = true;
|
||||||
public string? BasePolicy { get; set; }
|
public string? BasePolicy { get; set; }
|
||||||
public string? LoginPageRewrite { get; set; }
|
public string? LoginPageRewrite { get; set; }
|
||||||
|
public List<HopCallbackHandler> Handlers { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A helper class for editing the <see cref="HopFrameConfig"/>
|
/// A helper class for editing the <see cref="HopFrameConfig"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class HopFrameConfigurator(HopFrameConfig config) {
|
public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollection collection = null!) {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Internal HopFrame configuration that's modified by the helper functions
|
/// The Internal HopFrame configuration that's modified by the helper functions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HopFrameConfig InnerConfig { get; } = config;
|
public HopFrameConfig InnerConfig { get; } = config;
|
||||||
|
|
||||||
|
public IServiceCollection ServiceCollection { get; } = collection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds all tables defined in the DbContext to the HopFrame ui and configures it using the provided configurator
|
/// Adds all tables defined in the DbContext to the HopFrame ui and configures it using the provided configurator
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -38,11 +43,33 @@ public class HopFrameConfigurator(HopFrameConfig config) {
|
|||||||
/// <returns>The configurator used for the DbContext</returns>
|
/// <returns>The configurator used for the DbContext</returns>
|
||||||
/// <seealso cref="DbContextConfigurator{TDbContext}"/>
|
/// <seealso cref="DbContextConfigurator{TDbContext}"/>
|
||||||
public DbContextConfigurator<TDbContext> AddDbContext<TDbContext>() where TDbContext : DbContext {
|
public DbContextConfigurator<TDbContext> AddDbContext<TDbContext>() where TDbContext : DbContext {
|
||||||
var context = new DbContextConfig(typeof(TDbContext));
|
var context = new DbContextConfig(typeof(TDbContext), InnerConfig);
|
||||||
InnerConfig.Contexts.Add(context);
|
InnerConfig.Contexts.Add(context);
|
||||||
return new DbContextConfigurator<TDbContext>(context);
|
return new DbContextConfigurator<TDbContext>(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a context is already registered in the HopFrame
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TDbContext">The context that should be checked</typeparam>
|
||||||
|
/// <returns>true if the context is already registered, false if not</returns>
|
||||||
|
public bool HasDbContext<TDbContext>() where TDbContext : DbContext {
|
||||||
|
return InnerConfig.Contexts.Any(context => context.ContextType == typeof(TDbContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a configurator for the context if it was already defined
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TDbContext"></typeparam>
|
||||||
|
/// <returns>The configurator of the context if it already was defined, null if not</returns>
|
||||||
|
public DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext : DbContext {
|
||||||
|
var config = InnerConfig.Contexts
|
||||||
|
.SingleOrDefault(context => context.ContextType == typeof(TDbContext));
|
||||||
|
if (config is null) return null;
|
||||||
|
|
||||||
|
return new DbContextConfigurator<TDbContext>(config);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin ui
|
/// Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin ui
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -23,9 +23,37 @@ public class PropertyConfig(PropertyInfo info, TableConfig table, int nthPropert
|
|||||||
public bool IsRelation { get; internal set; }
|
public bool IsRelation { get; internal set; }
|
||||||
public bool IsRequired { get; internal set; }
|
public bool IsRequired { get; internal set; }
|
||||||
public bool IsEnumerable { get; internal set; }
|
public bool IsEnumerable { get; internal set; }
|
||||||
public bool IsListingProperty { get; set; }
|
public bool IsVirtualProperty { get; set; }
|
||||||
public int Order { get; set; } = nthProperty;
|
public int Order { get; set; } = nthProperty;
|
||||||
public int DisplayLength { get; set; } = 32;
|
public int DisplayLength { get; set; } = 32;
|
||||||
|
|
||||||
|
public virtual object? GetValue(object? source, IServiceProvider provider) {
|
||||||
|
return Info.GetValue(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void SetValue(object? source, object? value, IServiceProvider provider) {
|
||||||
|
Info.SetValue(source, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class VirtualPropertyConfig(TableConfig table, int nthProperty) : PropertyConfig(GetDummyProperty(), table, nthProperty) {
|
||||||
|
public string? DummyProperty { get; set; } = null;
|
||||||
|
|
||||||
|
public Func<object, string, IServiceProvider, Task>? VirtualParser { get; set; }
|
||||||
|
|
||||||
|
public override object? GetValue(object? source, IServiceProvider provider) {
|
||||||
|
return Formatter!.Invoke(source!, provider).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetValue(object? source, object? value, IServiceProvider provider) {
|
||||||
|
VirtualParser?.Invoke(source!, (string)value!, provider).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PropertyInfo GetDummyProperty() {
|
||||||
|
return typeof(VirtualPropertyConfig)
|
||||||
|
.GetProperties()
|
||||||
|
.First(prop => prop.Name == nameof(DummyProperty));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -213,3 +241,28 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class VirtualPropertyConfigurator<TModel>(VirtualPropertyConfig config) : PropertyConfigurator<string>(config) {
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the function used for parsing the value provided in the editor dialog to the actual model value
|
||||||
|
/// </summary>
|
||||||
|
public VirtualPropertyConfigurator<TModel> SetVirtualParser(Action<TModel, string, IServiceProvider> parser) {
|
||||||
|
var cfg = InnerConfig as VirtualPropertyConfig;
|
||||||
|
|
||||||
|
cfg!.VirtualParser = (model, input, services) => {
|
||||||
|
parser.Invoke((TModel)model, input, services);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="SetVirtualParser{TModel}(System.Action{TModel,string,System.IServiceProvider})"/>
|
||||||
|
public VirtualPropertyConfigurator<TModel> SetVirtualParser(Func<TModel, string, IServiceProvider, Task> parser) {
|
||||||
|
var cfg = InnerConfig as VirtualPropertyConfig;
|
||||||
|
|
||||||
|
cfg!.VirtualParser = (model, input, services) => parser.Invoke((TModel)model, input, services);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using HopFrame.Core.Callbacks;
|
||||||
|
|
||||||
namespace HopFrame.Core.Config;
|
namespace HopFrame.Core.Config;
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ public class TableConfig {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A helper class for editing the <see cref="TableConfig"/>
|
/// A helper class for editing the <see cref="TableConfig"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TableConfigurator<TModel>(TableConfig config) {
|
public sealed class TableConfigurator<TModel>(TableConfig config) {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Internal property configuration that's modified by the helper functions
|
/// The Internal property configuration that's modified by the helper functions
|
||||||
@@ -75,7 +76,7 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
|||||||
public PropertyConfigurator<TProp> Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression) {
|
public PropertyConfigurator<TProp> Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression) {
|
||||||
var info = GetPropertyInfo(propertyExpression);
|
var info = GetPropertyInfo(propertyExpression);
|
||||||
var prop = InnerConfig.Properties
|
var prop = InnerConfig.Properties
|
||||||
.Single(prop => prop.Info.Name == info.Name);
|
.Single(prop => prop.Info == info);
|
||||||
return new PropertyConfigurator<TProp>(prop);
|
return new PropertyConfigurator<TProp>(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,25 +99,25 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
|||||||
/// <param name="template">The template used for generating the property value</param>
|
/// <param name="template">The template used for generating the property value</param>
|
||||||
/// <returns>The configurator for the virtual property</returns>
|
/// <returns>The configurator for the virtual property</returns>
|
||||||
/// <seealso cref="PropertyConfigurator{TProp}"/>
|
/// <seealso cref="PropertyConfigurator{TProp}"/>
|
||||||
public PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template) {
|
public VirtualPropertyConfigurator<TModel> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template) {
|
||||||
var prop = new PropertyConfig(InnerConfig.Properties.First().Info, InnerConfig, InnerConfig.Properties.Count) {
|
var prop = new VirtualPropertyConfig(InnerConfig, InnerConfig.Properties.Count) {
|
||||||
Name = name,
|
Name = name,
|
||||||
IsListingProperty = true,
|
IsVirtualProperty = true,
|
||||||
Formatter = (obj, provider) => Task.FromResult(template.Invoke((TModel)obj, provider))
|
Formatter = (obj, provider) => Task.FromResult(template.Invoke((TModel)obj, provider))
|
||||||
};
|
};
|
||||||
InnerConfig.Properties.Add(prop);
|
InnerConfig.Properties.Add(prop);
|
||||||
return new PropertyConfigurator<string>(prop);
|
return new VirtualPropertyConfigurator<TModel>(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="AddVirtualProperty(string,System.Func{TModel,System.IServiceProvider,string})"/>
|
/// <inheritdoc cref="AddVirtualProperty(string,System.Func{TModel,System.IServiceProvider,string})"/>
|
||||||
public PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, Task<string>> template) {
|
public VirtualPropertyConfigurator<TModel> AddVirtualProperty(string name, Func<TModel, IServiceProvider, Task<string>> template) {
|
||||||
var prop = new PropertyConfig(InnerConfig.Properties.First().Info, InnerConfig, InnerConfig.Properties.Count) {
|
var prop = new VirtualPropertyConfig(InnerConfig, InnerConfig.Properties.Count) {
|
||||||
Name = name,
|
Name = name,
|
||||||
IsListingProperty = true,
|
IsVirtualProperty = true,
|
||||||
Formatter = (obj, provider) => template.Invoke((TModel)obj, provider)
|
Formatter = (obj, provider) => template.Invoke((TModel)obj, provider)
|
||||||
};
|
};
|
||||||
InnerConfig.Properties.Add(prop);
|
InnerConfig.Properties.Add(prop);
|
||||||
return new PropertyConfigurator<string>(prop);
|
return new VirtualPropertyConfigurator<TModel>(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -189,6 +190,36 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a callback handler of the provided type
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type of callback that triggers the handler</param>
|
||||||
|
/// <param name="handler">The handler delegate</param>
|
||||||
|
public TableConfigurator<TModel> AddCallbackHandler(CallbackType type, Func<TModel, IServiceProvider, Task> handler) {
|
||||||
|
var eventName = CallbackTypes.ConstructCallbackName(type, InnerConfig);
|
||||||
|
var handlerStore = new HopCallbackHandler(eventName, (o, provider) => handler.Invoke((TModel)o, provider));
|
||||||
|
InnerConfig.ContextConfig.ParentConfig.Handlers.Add(handlerStore);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a callback handler of the provided type
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type of callback that triggers the handler</param>
|
||||||
|
/// <param name="handler">The handler delegate</param>
|
||||||
|
public TableConfigurator<TModel> AddCallbackHandler(CallbackType type, Action<TModel, IServiceProvider> handler) {
|
||||||
|
var eventName = CallbackTypes.ConstructCallbackName(type, InnerConfig);
|
||||||
|
var handlerStore = new HopCallbackHandler(eventName, (o, provider) => {
|
||||||
|
handler.Invoke((TModel)o, provider);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
InnerConfig.ContextConfig.ParentConfig.Handlers.Add(handlerStore);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
internal static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) {
|
internal static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) {
|
||||||
if (propertyLambda.Body is not MemberExpression member) {
|
if (propertyLambda.Body is not MemberExpression member) {
|
||||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
|
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
|
||||||
@@ -198,7 +229,7 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
|||||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
|
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Type type = typeof(TSource);
|
var type = typeof(TSource);
|
||||||
if (propInfo.ReflectedType != null && type != propInfo.ReflectedType &&
|
if (propInfo.ReflectedType != null && type != propInfo.ReflectedType &&
|
||||||
!type.IsSubclassOf(propInfo.ReflectedType)) {
|
!type.IsSubclassOf(propInfo.ReflectedType)) {
|
||||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a property that is not from type {type}.");
|
throw new ArgumentException($"Expression '{propertyLambda}' refers to a property that is not from type {type}.");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using HopFrame.Core.Services;
|
using HopFrame.Core.Callbacks;
|
||||||
|
using HopFrame.Core.Services;
|
||||||
using HopFrame.Core.Services.Implementations;
|
using HopFrame.Core.Services.Implementations;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
@@ -15,6 +16,7 @@ public static class ServiceCollectionExtensions {
|
|||||||
public static IServiceCollection AddHopFrameServices(this IServiceCollection services) {
|
public static IServiceCollection AddHopFrameServices(this IServiceCollection services) {
|
||||||
services.AddScoped<IContextExplorer, ContextExplorer>();
|
services.AddScoped<IContextExplorer, ContextExplorer>();
|
||||||
services.TryAddScoped<IHopFrameAuthHandler, DefaultAuthHandler>();
|
services.TryAddScoped<IHopFrameAuthHandler, DefaultAuthHandler>();
|
||||||
|
services.TryAddScoped<ICallbackEmitter, CallbackEmitter>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
public Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using HopFrame.Core.Config;
|
||||||
|
using HopFrame.Core.Callbacks;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.Services.Implementations;
|
||||||
|
|
||||||
|
internal sealed class CallbackEmitter(IServiceProvider provider, HopFrameConfig config) : ICallbackEmitter {
|
||||||
|
|
||||||
|
public Guid RegisterCallbackHandler(string @event, Func<object, IServiceProvider, Task> handler) {
|
||||||
|
var handlerStore = new HopCallbackHandler(@event, handler);
|
||||||
|
config.Handlers.Add(handlerStore);
|
||||||
|
return handlerStore.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveCallbackHandler(Guid id) {
|
||||||
|
var count = config.Handlers.RemoveAll(handler => handler.Id == id);
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DispatchCallback(string @event, object argument = null!) {
|
||||||
|
var handlers = config.Handlers.Where(handler => handler.EventType == @event);
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
foreach (var handler in handlers) {
|
||||||
|
var task = handler.Handler.Invoke(argument, provider);
|
||||||
|
tasks.Add(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAllCallbackHandlers(string @event) {
|
||||||
|
config.Handlers.RemoveAll(handler => handler.EventType == @event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAllCallbackHandlers() {
|
||||||
|
config.Handlers.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -55,13 +55,28 @@ 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)!;
|
||||||
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
||||||
|
|
||||||
foreach (var propertyConfig in table.Properties) {
|
foreach (var propertyConfig in table.Properties) {
|
||||||
if (propertyConfig.IsListingProperty) continue;
|
if (propertyConfig.IsVirtualProperty) continue;
|
||||||
if (propertyConfig.IsRelation) continue;
|
if (propertyConfig.IsRelation) continue;
|
||||||
|
|
||||||
var prop = entity.FindProperty(propertyConfig.Info.Name);
|
var prop = entity.FindProperty(propertyConfig.Info.Name);
|
||||||
@@ -93,7 +108,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
|
|
||||||
foreach (var property in entity.GetProperties()) {
|
foreach (var property in entity.GetProperties()) {
|
||||||
var propConfig = table.Properties
|
var propConfig = table.Properties
|
||||||
.Where(prop => !prop.IsListingProperty)
|
.Where(prop => !prop.IsVirtualProperty)
|
||||||
.SingleOrDefault(prop => prop.Info == property.PropertyInfo);
|
.SingleOrDefault(prop => prop.Info == property.PropertyInfo);
|
||||||
if (propConfig is null || propConfig.IsRequired) continue;
|
if (propConfig is null || propConfig.IsRequired) continue;
|
||||||
propConfig.IsRequired = !property.IsNullable;
|
propConfig.IsRequired = !property.IsNullable;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using HopFrame.Core.Config;
|
using HopFrame.Core.Config;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
|
||||||
namespace HopFrame.Core.Services.Implementations;
|
namespace HopFrame.Core.Services.Implementations;
|
||||||
|
|
||||||
@@ -48,14 +49,32 @@ 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) {
|
||||||
await context.Entry((TModel)item).ReloadAsync();
|
var entry = context.Entry((TModel)item);
|
||||||
|
await entry.ReloadAsync();
|
||||||
|
|
||||||
|
if (entry.Collections.Any()) {
|
||||||
|
context.ChangeTracker.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ItemSearched(TModel item, string searchTerm) {
|
private bool ItemSearched(TModel item, string searchTerm) {
|
||||||
foreach (var property in config.Properties) {
|
foreach (var property in config.Properties) {
|
||||||
if (!property.Searchable) continue;
|
if (!property.Searchable) continue;
|
||||||
var value = property.Info.GetValue(item);
|
var value = property.GetValue(item, provider);
|
||||||
if (value is null) continue;
|
if (value is null) continue;
|
||||||
|
|
||||||
var strValue = value.ToString();
|
var strValue = value.ToString();
|
||||||
@@ -66,13 +85,13 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null) {
|
public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) {
|
||||||
if (item is null) return string.Empty;
|
if (item is null) return string.Empty;
|
||||||
|
|
||||||
if (prop.IsListingProperty)
|
if (prop.IsVirtualProperty)
|
||||||
return await prop.Formatter!.Invoke(item, provider);
|
return await prop.Formatter!.Invoke(item, provider);
|
||||||
|
|
||||||
var propValue = value ?? prop.Info.GetValue(item);
|
var propValue = value ?? prop.GetValue(item, provider);
|
||||||
if (propValue is null)
|
if (propValue is null)
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
@@ -81,12 +100,12 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prop.IsEnumerable) {
|
if (prop.IsEnumerable) {
|
||||||
if (value is not null) {
|
if (enumerableValue is not null) {
|
||||||
if (prop.EnumerableFormatter is not null) {
|
if (prop.EnumerableFormatter is not null) {
|
||||||
return await prop.EnumerableFormatter.Invoke(value, provider);
|
return await prop.EnumerableFormatter.Invoke(enumerableValue, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.ToString() ?? string.Empty;
|
return enumerableValue.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (propValue as IEnumerable)!.OfType<object>().Count().ToString();
|
return (propValue as IEnumerable)!.OfType<object>().Count().ToString();
|
||||||
@@ -104,7 +123,7 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
|||||||
if (innerConfig is null) return propValue.ToString()!;
|
if (innerConfig is null) return propValue.ToString()!;
|
||||||
|
|
||||||
var innerProp = innerConfig.Properties
|
var innerProp = innerConfig.Properties
|
||||||
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsListingProperty);
|
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsVirtualProperty);
|
||||||
|
|
||||||
if (innerProp is null) return propValue.ToString() ?? string.Empty;
|
if (innerProp is null) return propValue.ToString() ?? string.Empty;
|
||||||
return await DisplayProperty(propValue, innerProp);
|
return await DisplayProperty(propValue, innerProp);
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
@implements IDialogContentComponent<EditorDialogData>
|
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
@implements IDialogContentComponent<EditorDialogData>
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
@using System.Collections
|
@using System.Collections
|
||||||
@using HopFrame.Core.Config
|
@using HopFrame.Core.Config
|
||||||
@using HopFrame.Core.Services
|
@using HopFrame.Core.Services
|
||||||
@using HopFrame.Web.Models
|
@using HopFrame.Web.Models
|
||||||
@using HopFrame.Web.Helpers
|
@using HopFrame.Web.Helpers
|
||||||
|
@using HopFrame.Web.Plugins
|
||||||
|
@using HopFrame.Web.Plugins.Events
|
||||||
|
|
||||||
<FluentDialogBody>
|
<FluentDialogBody>
|
||||||
@foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) {
|
@foreach (var property in GetEditorProperties()) {
|
||||||
if (!_currentlyEditing && !property.Creatable) continue;
|
if (!_currentlyEditing && !property.Creatable) continue;
|
||||||
|
|
||||||
<div style="margin-bottom: 20px">
|
<div style="margin-bottom: 20px">
|
||||||
@@ -155,7 +158,7 @@
|
|||||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
|
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@foreach (var error in _validationErrors[property.Info.Name]) {
|
@foreach (var error in _validationErrors[property.Name]) {
|
||||||
<FluentLabel Color="@Color.Error">@error</FluentLabel>
|
<FluentLabel Color="@Color.Error">@error</FluentLabel>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -169,6 +172,7 @@
|
|||||||
@inject IHopFrameAuthHandler Handler
|
@inject IHopFrameAuthHandler Handler
|
||||||
@inject IToastService Toasts
|
@inject IToastService Toasts
|
||||||
@inject IServiceProvider Provider
|
@inject IServiceProvider Provider
|
||||||
|
@inject IPluginOrchestrator PluginOrchestrator
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
@@ -180,6 +184,8 @@
|
|||||||
private bool _currentlyEditing;
|
private bool _currentlyEditing;
|
||||||
private ITableManager? _manager;
|
private ITableManager? _manager;
|
||||||
private readonly Dictionary<string, List<string>> _validationErrors = new();
|
private readonly Dictionary<string, List<string>> _validationErrors = new();
|
||||||
|
private readonly List<PropertyChange> _changes = new();
|
||||||
|
private readonly CancellationTokenSource _tokenSource = new();
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override void OnInitialized() {
|
||||||
_currentlyEditing = Content.CurrentObject is not null;
|
_currentlyEditing = Content.CurrentObject is not null;
|
||||||
@@ -191,20 +197,25 @@
|
|||||||
Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType);
|
Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType);
|
||||||
|
|
||||||
foreach (var property in Content.Config.Properties) {
|
foreach (var property in Content.Config.Properties) {
|
||||||
if (property.IsListingProperty) continue;
|
_validationErrors.Add(property.Name, []);
|
||||||
_validationErrors.Add(property.Info.Name, []);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IEnumerable<PropertyConfig> GetEditorProperties() {
|
||||||
|
return Content.Config.Properties
|
||||||
|
.Where(prop => prop is not VirtualPropertyConfig { VirtualParser: null })
|
||||||
|
.OrderBy(prop => prop.Order);
|
||||||
|
}
|
||||||
|
|
||||||
private TValue? GetPropertyValue<TValue>(PropertyConfig config, object? listItem = null) {
|
private TValue? GetPropertyValue<TValue>(PropertyConfig config, object? listItem = null) {
|
||||||
if (!config.DisplayValue) return default;
|
if (!config.DisplayValue) return default;
|
||||||
if (Content.CurrentObject is null) return default;
|
if (Content.CurrentObject is null) return default;
|
||||||
|
|
||||||
if (listItem is not null) {
|
if (listItem is not null) {
|
||||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, listItem).Result;
|
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, null, listItem).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = config.Info.GetValue(Content.CurrentObject);
|
var value = GetNewestValue(config);
|
||||||
|
|
||||||
if (value is null)
|
if (value is null)
|
||||||
return default;
|
return default;
|
||||||
@@ -213,7 +224,7 @@
|
|||||||
return (TValue)value;
|
return (TValue)value;
|
||||||
|
|
||||||
if (typeof(TValue) == typeof(string))
|
if (typeof(TValue) == typeof(string))
|
||||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config).Result;
|
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, value).Result;
|
||||||
|
|
||||||
return (TValue)Convert.ChangeType(value, typeof(TValue));
|
return (TValue)Convert.ChangeType(value, typeof(TValue));
|
||||||
}
|
}
|
||||||
@@ -277,15 +288,19 @@
|
|||||||
else {
|
else {
|
||||||
needsOverride = false;
|
needsOverride = false;
|
||||||
|
|
||||||
if (!typeof(IList).IsAssignableFrom(config.Info.PropertyType)) {
|
var newItems = ((IEnumerable)value).OfType<object>();
|
||||||
throw new ArgumentException($"Invalid type of '{config.Name}' property in '{config.Table.DisplayName}' table, only list types are supported on enumerable relations.");
|
|
||||||
|
var collection = Activator.CreateInstance(config.Info.PropertyType);
|
||||||
|
var addMethod = config.Info.PropertyType.GetMethod(nameof(ICollection<object>.Add));
|
||||||
|
|
||||||
|
if (addMethod is null)
|
||||||
|
throw new ArgumentException($"Cannot modify property '{config.Name}' on table '{config.Table}' because no 'Add' method is implemented");
|
||||||
|
|
||||||
|
foreach (var item in newItems) {
|
||||||
|
addMethod.Invoke(collection, [item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var asList = (IList)config.Info.GetValue(Content.CurrentObject)!;
|
_changes.Add(new PropertyChange(config.Info, collection));
|
||||||
asList.Clear();
|
|
||||||
foreach (var element in (IEnumerable)value) {
|
|
||||||
asList.Add(element);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -299,7 +314,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (needsOverride)
|
if (needsOverride)
|
||||||
config.Info.SetValue(Content.CurrentObject, result);
|
_changes.Add(new PropertyChange(config.Info, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyChanges(object entry) {
|
||||||
|
foreach (var prop in Content.Config.Properties) {
|
||||||
|
var newValue = GetNewestValue(prop);
|
||||||
|
prop.SetValue(entry, newValue, Provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object? GetNewestValue(PropertyConfig config) {
|
||||||
|
var value = config.GetValue(Content.CurrentObject, Provider);
|
||||||
|
|
||||||
|
var change = _changes.LastOrDefault(c => c.Property == config.Info);
|
||||||
|
if (change is not null)
|
||||||
|
value = change.Value;
|
||||||
|
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenRelationalPicker(PropertyConfig config) {
|
private async Task OpenRelationalPicker(PropertyConfig config) {
|
||||||
@@ -321,7 +353,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var raw = config.Info.GetValue(Content.CurrentObject);
|
var raw = GetNewestValue(config);
|
||||||
if (raw is not null)
|
if (raw is not null)
|
||||||
currentValues.Add(raw);
|
currentValues.Add(raw);
|
||||||
}
|
}
|
||||||
@@ -339,11 +371,11 @@
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
foreach (var property in Content.Config.Properties) {
|
foreach (var property in Content.Config.Properties) {
|
||||||
if (property.IsListingProperty) continue;
|
if (property.IsVirtualProperty) continue;
|
||||||
|
|
||||||
var errorList = _validationErrors[property.Info.Name];
|
var errorList = _validationErrors[property.Name];
|
||||||
errorList.Clear();
|
errorList.Clear();
|
||||||
var value = property.Info.GetValue(Content.CurrentObject);
|
var value = GetNewestValue(property);
|
||||||
|
|
||||||
if (property.Validator is not null) {
|
if (property.Validator is not null) {
|
||||||
errorList.AddRange(await property.Validator.Invoke(value, Provider));
|
errorList.AddRange(await property.Validator.Invoke(value, Provider));
|
||||||
@@ -352,6 +384,14 @@
|
|||||||
|
|
||||||
if (value is null && property.IsRequired)
|
if (value is null && property.IsRequired)
|
||||||
errorList.Add($"{property.Name} is required");
|
errorList.Add($"{property.Name} is required");
|
||||||
|
|
||||||
|
var eventResult = await PluginOrchestrator.DispatchEvent(new ValidationEvent(this) {
|
||||||
|
Errors = errorList,
|
||||||
|
Property = property,
|
||||||
|
Table = Content.Config
|
||||||
|
}, _tokenSource.Token);
|
||||||
|
|
||||||
|
if (eventResult.IsCanceled) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
@@ -362,10 +402,17 @@
|
|||||||
if (!valid) return false;
|
if (!valid) return false;
|
||||||
var dialog = await Dialogs.ShowConfirmationAsync($"Do you really want to {(_currentlyEditing ? "edit" : "create")} this entry?");
|
var dialog = await Dialogs.ShowConfirmationAsync($"Do you really want to {(_currentlyEditing ? "edit" : "create")} this entry?");
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
return !result.Cancelled;
|
if (result.Cancelled) return false;
|
||||||
|
|
||||||
|
ApplyChanges(Content.CurrentObject!);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum InputType {
|
public void Dispose() {
|
||||||
|
_tokenSource.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum InputType {
|
||||||
Number,
|
Number,
|
||||||
Switch,
|
Switch,
|
||||||
Date,
|
Date,
|
||||||
|
|||||||
35
src/HopFrame.Web/Components/HopFrameCard.razor
Normal file
35
src/HopFrame.Web/Components/HopFrameCard.razor
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<FluentCard Width="350px" Height="200px" Style="display: flex; flex-direction: column; background-color: var(--neutral-layer-1)">
|
||||||
|
<h3 style="margin-bottom: 0; display: flex; align-items: center; gap: 5px">
|
||||||
|
@if (Icon is not null) {
|
||||||
|
<FluentIcon Value="Icon" Color="Color.Neutral" />
|
||||||
|
}
|
||||||
|
@Title
|
||||||
|
</h3>
|
||||||
|
<FluentLabel Typo="Typography.Body" Color="Color.Info" Style="margin-bottom: 0.5rem">@Subtitle</FluentLabel>
|
||||||
|
<span>@Description</span>
|
||||||
|
<FluentSpacer />
|
||||||
|
<div style="display: flex">
|
||||||
|
<FluentSpacer/>
|
||||||
|
|
||||||
|
<a href="@Href" style="display: inline-block">
|
||||||
|
<FluentButton>Open</FluentButton>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</FluentCard>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public required string Title { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? Subtitle { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public required string Description { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public required string Href { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Icon? Icon { get; set; }
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@using HopFrame.Core.Config
|
@using HopFrame.Core.Config
|
||||||
@using HopFrame.Core.Services
|
@using HopFrame.Core.Services
|
||||||
|
@using HopFrame.Web.Models
|
||||||
@using Microsoft.Extensions.DependencyInjection
|
@using Microsoft.Extensions.DependencyInjection
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
@@ -39,10 +40,28 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
|
internal static readonly List<CustomView> CustomViews = new();
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
var authorized = await Handler.IsAuthenticatedAsync(Config.BasePolicy);
|
var authorized = await Handler.IsAuthenticatedAsync(Config.BasePolicy);
|
||||||
|
|
||||||
|
var currentUri = "/" + Navigator.ToBaseRelativePath(Navigator.Uri);
|
||||||
|
|
||||||
|
if (authorized) {
|
||||||
|
foreach (var view in CustomViews.Where(view => !string.IsNullOrWhiteSpace(view.Policy))) {
|
||||||
|
switch (view.LinkMatch) {
|
||||||
|
case NavLinkMatch.All when currentUri != view.Url:
|
||||||
|
case NavLinkMatch.Prefix when !currentUri.StartsWith(view.Url):
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
authorized = await Handler.IsAuthenticatedAsync(view.Policy);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!authorized) {
|
if (!authorized) {
|
||||||
Navigator.NavigateTo((Config.LoginPageRewrite ?? "/login") + "?redirect=/" + Navigator.ToBaseRelativePath(Navigator.Uri), true);
|
Navigator.NavigateTo((Config.LoginPageRewrite ?? "/login") + "?redirect=" + currentUri, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@using HopFrame.Core.Config
|
@using HopFrame.Core.Config
|
||||||
@using HopFrame.Core.Services
|
@using HopFrame.Core.Services
|
||||||
|
@using HopFrame.Web.Models
|
||||||
|
|
||||||
<FluentAppBar Orientation="Orientation.Vertical" PopoverShowSearch="false" Style="background-color: var(--neutral-layer-2); height: auto">
|
<FluentAppBar Orientation="Orientation.Vertical" PopoverShowSearch="false" Style="background-color: var(--neutral-layer-2); height: auto">
|
||||||
<FluentAppBarItem Href="/admin"
|
<FluentAppBarItem Href="/admin"
|
||||||
@@ -11,6 +12,15 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
@foreach (var view in _views) {
|
||||||
|
<FluentAppBarItem Href="@view.Url"
|
||||||
|
Match="@view.LinkMatch"
|
||||||
|
IconActive="GetLinkIcon(view, IconVariant.Filled)"
|
||||||
|
IconRest="GetLinkIcon(view, IconVariant.Regular)"
|
||||||
|
Text="@view.Name"
|
||||||
|
Style="margin-top: 0.25rem"/>
|
||||||
|
}
|
||||||
|
|
||||||
@foreach (var table in _tables.OrderBy(t => t.Order).Select(t => t.DisplayName)) {
|
@foreach (var table in _tables.OrderBy(t => t.Order).Select(t => t.DisplayName)) {
|
||||||
<FluentAppBarItem Href="@("/admin/" + table.ToLower())"
|
<FluentAppBarItem Href="@("/admin/" + table.ToLower())"
|
||||||
Match="NavLinkMatch.All"
|
Match="NavLinkMatch.All"
|
||||||
@@ -27,6 +37,7 @@
|
|||||||
@code {
|
@code {
|
||||||
|
|
||||||
private readonly List<TableConfig> _tables = [];
|
private readonly List<TableConfig> _tables = [];
|
||||||
|
private readonly List<CustomView> _views = [];
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
foreach (var table in Explorer.GetTables()) {
|
foreach (var table in Explorer.GetTables()) {
|
||||||
@@ -34,6 +45,21 @@
|
|||||||
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
||||||
_tables.Add(table);
|
_tables.Add(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var view in HopFrameLayout.CustomViews) {
|
||||||
|
if (!await Handler.IsAuthenticatedAsync(view.Policy)) continue;
|
||||||
|
_views.Add(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Icon GetLinkIcon(CustomView view, IconVariant variant) {
|
||||||
|
var info = new IconInfo {
|
||||||
|
Name = view.Icon,
|
||||||
|
Variant = variant,
|
||||||
|
Size = IconSize.Size24
|
||||||
|
};
|
||||||
|
|
||||||
|
return info.GetInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
@page "/admin"
|
@page "/admin"
|
||||||
@using HopFrame.Core.Config
|
@using HopFrame.Core.Config
|
||||||
@using HopFrame.Core.Services
|
@using HopFrame.Core.Services
|
||||||
|
@using HopFrame.Web.Models
|
||||||
@layout HopFrameLayout
|
@layout HopFrameLayout
|
||||||
|
|
||||||
<PageTitle>HopFrame</PageTitle>
|
<PageTitle>HopFrame</PageTitle>
|
||||||
|
|
||||||
<div style="padding: 1.5rem 1.5rem;">
|
<div style="padding: 1.5rem 1.5rem;">
|
||||||
<h2>Tables</h2>
|
<h2>Pages</h2>
|
||||||
|
|
||||||
<FluentStack Orientation="Orientation.Horizontal" Wrap="true" Style="margin-top: 1.5rem">
|
<FluentStack Orientation="Orientation.Horizontal" Wrap="true" Style="margin-top: 1.5rem">
|
||||||
@foreach (var table in _tables.OrderBy(t => t.Order)) {
|
@foreach (var view in _views) {
|
||||||
<FluentCard Width="350px" Height="200px" Style="display: flex; flex-direction: column; background-color: var(--neutral-layer-1)">
|
<HopFrameCard
|
||||||
<h3 style="margin-bottom: 0;">@table.DisplayName</h3>
|
Title="@view.Name"
|
||||||
<FluentLabel Typo="Typography.Body" Color="Color.Info" Style="margin-bottom: 0.5rem">@table.ViewPolicy</FluentLabel>
|
Subtitle="@view.Policy"
|
||||||
<span>@table.Description</span>
|
Description="@view.Description"
|
||||||
<FluentSpacer />
|
Href="@view.Url"
|
||||||
<div style="display: flex">
|
Icon="HopFrameSideMenu.GetLinkIcon(view, IconVariant.Regular)"/>
|
||||||
<FluentSpacer/>
|
}
|
||||||
|
|
||||||
<a href="@("/admin/" + table.DisplayName.ToLower())" style="display: inline-block">
|
@foreach (var table in _tables.OrderBy(t => t.Order)) {
|
||||||
<FluentButton>Open</FluentButton>
|
<HopFrameCard
|
||||||
</a>
|
Title="@table.DisplayName"
|
||||||
</div>
|
Subtitle="@table.ViewPolicy"
|
||||||
</FluentCard>
|
Description="@table.Description"
|
||||||
|
Href="@("/admin/" + table.DisplayName.ToLower())"
|
||||||
|
Icon="new Icons.Regular.Size24.Database()"/>
|
||||||
}
|
}
|
||||||
</FluentStack>
|
</FluentStack>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,6 +36,7 @@
|
|||||||
@code {
|
@code {
|
||||||
|
|
||||||
private readonly List<TableConfig> _tables = [];
|
private readonly List<TableConfig> _tables = [];
|
||||||
|
private readonly List<CustomView> _views = [];
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
foreach (var table in Explorer.GetTables()) {
|
foreach (var table in Explorer.GetTables()) {
|
||||||
@@ -40,6 +44,11 @@
|
|||||||
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
||||||
_tables.Add(table);
|
_tables.Add(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var view in HopFrameLayout.CustomViews) {
|
||||||
|
if (!await Handler.IsAuthenticatedAsync(view.Policy)) continue;
|
||||||
|
_views.Add(view);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,11 @@
|
|||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
@using HopFrame.Core.Config
|
@using HopFrame.Core.Config
|
||||||
|
@using HopFrame.Core.Callbacks
|
||||||
@using HopFrame.Core.Services
|
@using HopFrame.Core.Services
|
||||||
@using HopFrame.Web.Models
|
@using HopFrame.Web.Models
|
||||||
|
@using HopFrame.Web.Plugins
|
||||||
|
@using HopFrame.Web.Plugins.Events
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@
|
|||||||
<div style="display: flex; flex-direction: column; height: 100%">
|
<div style="display: flex; flex-direction: column; height: 100%">
|
||||||
<FluentToolbar Class="hopframe-toolbar">
|
<FluentToolbar Class="hopframe-toolbar">
|
||||||
<h3>@_config?.DisplayName</h3>
|
<h3>@_config?.DisplayName</h3>
|
||||||
@if (!DisplaySelection) {
|
@if (!DisplaySelection && _buttonToggles.ShowRefreshButton) {
|
||||||
<FluentButton
|
<FluentButton
|
||||||
IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
|
IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
|
||||||
OnClick="Reload"
|
OnClick="Reload"
|
||||||
@@ -28,28 +31,43 @@
|
|||||||
</FluentButton>
|
</FluentButton>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopLeft)) {
|
||||||
|
<FluentButton
|
||||||
|
IconStart="@(button.Icon?.GetInstance())"
|
||||||
|
OnClick="() => button.Handler.Invoke(null!, _config!)">
|
||||||
|
@button.Title
|
||||||
|
</FluentButton>
|
||||||
|
}
|
||||||
|
|
||||||
<FluentSpacer />
|
<FluentSpacer />
|
||||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
||||||
|
|
||||||
@if (_hasCreatePolicy && DisplayActions) {
|
@if (_hasCreatePolicy && DisplayActions && _buttonToggles.ShowAddEntityButton) {
|
||||||
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entry</FluentButton>
|
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entity</FluentButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopRight)) {
|
||||||
|
<FluentButton
|
||||||
|
IconStart="@(button.Icon?.GetInstance())"
|
||||||
|
OnClick="() => button.Handler.Invoke(null!, _config!)">
|
||||||
|
@button.Title
|
||||||
|
</FluentButton>
|
||||||
}
|
}
|
||||||
</FluentToolbar>
|
</FluentToolbar>
|
||||||
<FluentProgress Visible="_loading" Width="100%" />
|
<FluentProgress Visible="_loading" Width="100%" />
|
||||||
|
|
||||||
<div style="display: flex; overflow-y: auto; flex-grow: 1">
|
<div style="display: flex; overflow-y: auto; flex-grow: 1">
|
||||||
<div style="flex-grow: 1">
|
<div style="flex-grow: 1">
|
||||||
<FluentDataGrid Items="_currentlyDisplayedModels.AsQueryable()">
|
<FluentDataGrid Items="CurrentlyDisplayedModels.AsQueryable()">
|
||||||
@if (DisplaySelection) {
|
@if (DisplaySelection) {
|
||||||
<SelectColumn
|
<SelectColumn
|
||||||
TGridItem="object"
|
TGridItem="object"
|
||||||
SelectMode="SelectionMode"
|
SelectMode="SelectionMode"
|
||||||
SelectFromEntireRow="true"
|
SelectFromEntireRow="true"
|
||||||
SelectedItems="DialogData?.SelectedObjects.ToArray()"
|
|
||||||
OnSelect="data => SelectItem(data.Item, data.Selected)"
|
OnSelect="data => SelectItem(data.Item, data.Selected)"
|
||||||
SelectAllChanged="SelectAll"
|
SelectAllDisabled="true"
|
||||||
SelectAll="_allSelected"
|
Property="o => DialogData!.SelectedObjects.Contains(o)"
|
||||||
Style="min-width: max-content; height: 44px; display: grid; align-items: center" @ref="_selectColumn" />
|
Style="min-width: max-content; height: 44px; display: grid; align-items: center" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
|
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
|
||||||
@@ -61,13 +79,19 @@
|
|||||||
|
|
||||||
@if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy)) {
|
@if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy)) {
|
||||||
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content">
|
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content">
|
||||||
@if (_hasUpdatePolicy) {
|
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.OnEntry)) {
|
||||||
|
<FluentButton OnClick="() => button.Handler.Invoke(context, _config!)">
|
||||||
|
<FluentIcon Value="@(button.Icon!.GetInstance())" />
|
||||||
|
</FluentButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_hasUpdatePolicy && _buttonToggles.ShowEditButton) {
|
||||||
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(context); }">
|
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(context); }">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_hasDeletePolicy) {
|
@if (_hasDeletePolicy && _buttonToggles.ShowDeleteButton) {
|
||||||
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(context); }">
|
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(context); }">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
@@ -111,13 +135,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@inject IDialogService Dialogs
|
@inject IDialogService Dialogs
|
||||||
@inject IHopFrameAuthHandler Handler
|
@inject IHopFrameAuthHandler Handler
|
||||||
|
@inject ICallbackEmitter Emitter
|
||||||
|
@inject IPluginOrchestrator PluginOrchestrator
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
@@ -142,7 +186,7 @@
|
|||||||
private TableConfig? _config;
|
private TableConfig? _config;
|
||||||
private ITableManager? _manager;
|
private ITableManager? _manager;
|
||||||
|
|
||||||
private object[] _currentlyDisplayedModels = [];
|
public object[] CurrentlyDisplayedModels = [];
|
||||||
private int _currentPage;
|
private int _currentPage;
|
||||||
private int _totalPages;
|
private int _totalPages;
|
||||||
private string? _searchTerm;
|
private string? _searchTerm;
|
||||||
@@ -152,10 +196,16 @@
|
|||||||
private bool _hasDeletePolicy;
|
private bool _hasDeletePolicy;
|
||||||
private bool _hasCreatePolicy;
|
private bool _hasCreatePolicy;
|
||||||
|
|
||||||
private SelectColumn<object>? _selectColumn;
|
|
||||||
private bool _allSelected;
|
private bool _allSelected;
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource _tokenSource = new();
|
||||||
|
private List<PluginButton> _pluginButtons = 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)) {
|
||||||
@@ -169,12 +219,19 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var eventResult = await PluginOrchestrator.DispatchEvent(new TableInitializedEvent(this) {
|
||||||
|
Table = _config!
|
||||||
|
});
|
||||||
|
if (eventResult.IsCanceled) return;
|
||||||
|
_pluginButtons = eventResult.PluginButtons;
|
||||||
|
_buttonToggles = eventResult.DefaultButtons;
|
||||||
|
|
||||||
_hasUpdatePolicy = await Handler.IsAuthenticatedAsync(_config?.UpdatePolicy);
|
_hasUpdatePolicy = await Handler.IsAuthenticatedAsync(_config?.UpdatePolicy);
|
||||||
_hasDeletePolicy = await Handler.IsAuthenticatedAsync(_config?.DeletePolicy);
|
_hasDeletePolicy = await Handler.IsAuthenticatedAsync(_config?.DeletePolicy);
|
||||||
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
||||||
|
|
||||||
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
||||||
_currentlyDisplayedModels = await _manager!.LoadPage(_currentPage, PerPage).ToArrayAsync();
|
CurrentlyDisplayedModels = await _manager!.LoadPage(_currentPage, PerPage).ToArrayAsync();
|
||||||
_totalPages = await _manager.TotalPages(PerPage);
|
_totalPages = await _manager.TotalPages(PerPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +246,7 @@
|
|||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
_searchCancel.Dispose();
|
_searchCancel.Dispose();
|
||||||
|
_tokenSource.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenSource _searchCancel = new();
|
private CancellationTokenSource _searchCancel = new();
|
||||||
@@ -199,14 +257,38 @@
|
|||||||
_searchCancel = new();
|
_searchCancel = new();
|
||||||
|
|
||||||
await Task.Delay(500, _searchCancel.Token);
|
await Task.Delay(500, _searchCancel.Token);
|
||||||
|
|
||||||
|
var eventResult = await PluginOrchestrator.DispatchEvent(new SearchEvent(this) {
|
||||||
|
SearchTerm = _searchTerm,
|
||||||
|
Table = _config!,
|
||||||
|
CurrentPage = _currentPage
|
||||||
|
}, _tokenSource.Token);
|
||||||
|
if (eventResult.IsCanceled) {
|
||||||
|
if (eventResult.SearchResult is null) return;
|
||||||
|
|
||||||
|
CurrentlyDisplayedModels = eventResult.SearchResult.ToArray();
|
||||||
|
_totalPages = eventResult.TotalPages;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_searchTerm = eventResult.SearchTerm;
|
||||||
|
|
||||||
await Reload();
|
await Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Reload() {
|
public async Task Reload() {
|
||||||
_loading = true;
|
_loading = true;
|
||||||
|
|
||||||
|
var eventResult = await PluginOrchestrator.DispatchEvent(new ReloadEvent(this) {
|
||||||
|
Table = _config!
|
||||||
|
}, _tokenSource.Token);
|
||||||
|
if (eventResult.IsCanceled) {
|
||||||
|
_loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(_searchTerm)) {
|
if (!string.IsNullOrEmpty(_searchTerm)) {
|
||||||
(var query, _totalPages) = await _manager!.Search(_searchTerm, 0, PerPage);
|
(var query, _totalPages) = await _manager!.Search(_searchTerm, _currentPage, PerPage);
|
||||||
_currentlyDisplayedModels = query.ToArray();
|
CurrentlyDisplayedModels = query.ToArray();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await OnInitializedAsync();
|
await OnInitializedAsync();
|
||||||
@@ -214,7 +296,16 @@
|
|||||||
_loading = false;
|
_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,
|
||||||
|
TotalPages = _totalPages,
|
||||||
|
Table = _config!
|
||||||
|
}, _tokenSource.Token);
|
||||||
|
if (eventResult.IsCanceled) return;
|
||||||
|
page = eventResult.NewPage;
|
||||||
|
|
||||||
if (page < 0 || page > _totalPages - 1) return;
|
if (page < 0 || page > _totalPages - 1) return;
|
||||||
_currentPage = page;
|
_currentPage = page;
|
||||||
await Reload();
|
await Reload();
|
||||||
@@ -226,11 +317,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var eventResult = await PluginOrchestrator.DispatchEvent(new DeleteEntryEvent(this) {
|
||||||
|
Entity = element,
|
||||||
|
Table = _config!
|
||||||
|
}, _tokenSource.Token);
|
||||||
|
if (eventResult.IsCanceled) return;
|
||||||
|
|
||||||
var dialog = await Dialogs.ShowConfirmationAsync("Do you really want to delete this entry?");
|
var dialog = await Dialogs.ShowConfirmationAsync("Do you really want to delete this entry?");
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
if (result.Cancelled) return;
|
if (result.Cancelled) return;
|
||||||
|
|
||||||
await _manager!.DeleteItem(element);
|
await _manager!.DeleteItem(element);
|
||||||
|
await Emitter.DispatchCallback(CallbackTypes.DeleteEntry(_config!), element);
|
||||||
await Reload();
|
await Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,38 +338,61 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HopFrameTablePageEventArgs eventArgs;
|
||||||
|
if (element is null) {
|
||||||
|
eventArgs = new CreateEntryEvent(this) {
|
||||||
|
Table = _config!
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
eventArgs = new UpdateEntryEvent(this) {
|
||||||
|
Table = _config!,
|
||||||
|
Entity = element
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventResult = await PluginOrchestrator.DispatchEvent(eventArgs, _tokenSource.Token);
|
||||||
|
if (eventResult.IsCanceled) return;
|
||||||
|
|
||||||
var panel = await Dialogs.ShowPanelAsync<HopFrameEditor>(new EditorDialogData(_config!, element), new DialogParameters {
|
var panel = await Dialogs.ShowPanelAsync<HopFrameEditor>(new EditorDialogData(_config!, element), new DialogParameters {
|
||||||
TrapFocus = false
|
TrapFocus = false
|
||||||
});
|
});
|
||||||
var result = await panel.Result;
|
var result = await panel.Result;
|
||||||
var data = result.Data as EditorDialogData;
|
var data = result.Data as EditorDialogData;
|
||||||
|
|
||||||
if (result.Cancelled) {
|
if (result.Cancelled) return;
|
||||||
if (data?.CurrentObject is not null)
|
|
||||||
await _manager!.RevertChanges(data.CurrentObject);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element is null)
|
if (element is null) {
|
||||||
await _manager!.AddItem(data!.CurrentObject!);
|
await _manager!.AddItem(data!.CurrentObject!);
|
||||||
else
|
await Emitter.DispatchCallback(CallbackTypes.CreateEntry(_config!), data.CurrentObject!);
|
||||||
|
}
|
||||||
|
else {
|
||||||
await _manager!.EditItem(data!.CurrentObject!);
|
await _manager!.EditItem(data!.CurrentObject!);
|
||||||
|
await Emitter.DispatchCallback(CallbackTypes.UpdateEntry(_config!), data.CurrentObject!);
|
||||||
|
}
|
||||||
|
|
||||||
await Reload();
|
await Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectItem(object item, bool selected) {
|
private void SelectItem(object item, bool selected) {
|
||||||
|
var eventResult = PluginOrchestrator.DispatchEvent(new SelectEntryEvent(this) {
|
||||||
|
Entity = item,
|
||||||
|
Selected = selected,
|
||||||
|
Table = _config!
|
||||||
|
}, _tokenSource.Token).Result;
|
||||||
|
if (eventResult.IsCanceled) return;
|
||||||
|
selected = eventResult.Selected;
|
||||||
|
|
||||||
if (!selected)
|
if (!selected)
|
||||||
DialogData?.SelectedObjects.Remove(item);
|
DialogData!.SelectedObjects.Remove(item);
|
||||||
else DialogData?.SelectedObjects.Add(item);
|
else DialogData!.SelectedObjects.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectAll() {
|
private void SelectAll() {
|
||||||
var selected = _currentlyDisplayedModels.Any(obj => DialogData?.SelectedObjects.Contains(obj) != true);
|
var selected = CurrentlyDisplayedModels.All(DialogData!.SelectedObjects.Contains);
|
||||||
foreach (var displayedModel in _currentlyDisplayedModels) {
|
foreach (var displayedModel in CurrentlyDisplayedModels) {
|
||||||
SelectItem(displayedModel, selected);
|
SelectItem(displayedModel, !selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
_allSelected = selected;
|
_allSelected = selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,8 +400,25 @@
|
|||||||
var display = await _manager!.DisplayProperty(entry, config);
|
var display = await _manager!.DisplayProperty(entry, config);
|
||||||
|
|
||||||
if (display.Length > config.DisplayLength)
|
if (display.Length > config.DisplayLength)
|
||||||
display = display.Substring(0, config.DisplayLength) + "...";
|
display = display[..config.DisplayLength] + "...";
|
||||||
|
|
||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
64
src/HopFrame.Web/HopFrameConfiguratorExtensions.cs
Normal file
64
src/HopFrame.Web/HopFrameConfiguratorExtensions.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using HopFrame.Core.Config;
|
||||||
|
using HopFrame.Web.Components.Layout;
|
||||||
|
using HopFrame.Web.Models;
|
||||||
|
using HopFrame.Web.Plugins;
|
||||||
|
using HopFrame.Web.Plugins.Annotations;
|
||||||
|
using HopFrame.Web.Plugins.Internal;
|
||||||
|
|
||||||
|
namespace HopFrame.Web;
|
||||||
|
|
||||||
|
public static class HopFrameConfiguratorExtensions {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an entry to the side menu and dashboard with a custom url
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configurator">The configurator for the HopFrame config that is being created</param>
|
||||||
|
/// <param name="name">The name of the navigation entry</param>
|
||||||
|
/// <param name="url">The target url of the navigation entry</param>
|
||||||
|
public static CustomViewConfigurator AddCustomView(this HopFrameConfigurator configurator, string name, string url) {
|
||||||
|
var view = new CustomView {
|
||||||
|
Name = name,
|
||||||
|
Url = url
|
||||||
|
};
|
||||||
|
HopFrameLayout.CustomViews.Add(view);
|
||||||
|
return new CustomViewConfigurator(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <param name="configuratorDelegate">The delegate for configuring the view</param>
|
||||||
|
/// <inheritdoc cref="AddCustomView(HopFrame.Core.Config.HopFrameConfigurator,string,string)"/>
|
||||||
|
public static HopFrameConfigurator AddCustomView(this HopFrameConfigurator configurator, string name, string url, Action<CustomViewConfigurator> configuratorDelegate) {
|
||||||
|
var viewConfigurator = AddCustomView(configurator, name, url);
|
||||||
|
configuratorDelegate.Invoke(viewConfigurator);
|
||||||
|
return configurator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a plugin
|
||||||
|
/// </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 : class {
|
||||||
|
PluginOrchestrator.RegisterPlugin(configurator.ServiceCollection, typeof(TPlugin));
|
||||||
|
|
||||||
|
var methods = typeof(TPlugin).GetMethods()
|
||||||
|
.Where(method => method.IsStatic)
|
||||||
|
.Where(method => method.GetCustomAttributes(true)
|
||||||
|
.Any(attr => attr is PluginConfiguratorAttribute))
|
||||||
|
.Where(method => method.GetParameters().Length < 2);
|
||||||
|
|
||||||
|
foreach (var method in methods) {
|
||||||
|
if (method.GetParameters().Length > 0)
|
||||||
|
method.Invoke(null, [configurator]);
|
||||||
|
else method.Invoke(null, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return configurator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HopFrameConfigurator AddExporters(this HopFrameConfigurator configurator) {
|
||||||
|
configurator.AddPlugin<ExporterPlugin>();
|
||||||
|
return configurator;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
54
src/HopFrame.Web/Models/CustomView.cs
Normal file
54
src/HopFrame.Web/Models/CustomView.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
|
using Microsoft.FluentUI.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Models;
|
||||||
|
|
||||||
|
public sealed class CustomView {
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? Policy { get; set; }
|
||||||
|
public required string Url { get; init; }
|
||||||
|
public string Icon { get; set; } = "Window";
|
||||||
|
public NavLinkMatch LinkMatch { get; set; } = NavLinkMatch.All;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CustomViewConfigurator(CustomView view) {
|
||||||
|
public CustomView InnerConfig { get; } = view;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the description displayed in the dashboard
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="description">The desired description</param>
|
||||||
|
public CustomViewConfigurator SetDescription(string description) {
|
||||||
|
InnerConfig.Description = description;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the policy needed in order to access the view
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="policy">The desired policy</param>
|
||||||
|
public CustomViewConfigurator SetPolicy(string policy) {
|
||||||
|
InnerConfig.Policy = policy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the icon displayed in the sidebar
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="icon">The desired <see href="https://www.fluentui-blazor.net/Icon#explorer">fluent-icon</see></param>
|
||||||
|
public CustomViewConfigurator SetIcon(string icon) {
|
||||||
|
InnerConfig.Icon = icon;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the rule for the sidebar to determine if the link is active
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="match">The desired match rule</param>
|
||||||
|
public CustomViewConfigurator SetLinkMatch(NavLinkMatch match) {
|
||||||
|
InnerConfig.LinkMatch = match;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
8
src/HopFrame.Web/Models/PropertyChange.cs
Normal file
8
src/HopFrame.Web/Models/PropertyChange.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Models;
|
||||||
|
|
||||||
|
public class PropertyChange(PropertyInfo info, object? value) {
|
||||||
|
public object? Value { get; set; } = value;
|
||||||
|
public PropertyInfo Property { get; set; } = info;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace HopFrame.Web.Plugins.Annotations;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class EventHandlerAttribute : Attribute;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace HopFrame.Web.Plugins.Annotations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the method as a plugin configurator, so the method gets called, when the plugin is registered.
|
||||||
|
/// Only works on static methods
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class PluginConfiguratorAttribute : Attribute;
|
||||||
18
src/HopFrame.Web/Plugins/Events/EntryEvent.cs
Normal file
18
src/HopFrame.Web/Plugins/Events/EntryEvent.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using HopFrame.Web.Components.Pages;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins.Events;
|
||||||
|
|
||||||
|
public sealed class DeleteEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||||
|
public required object Entity { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CreateEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender);
|
||||||
|
|
||||||
|
public sealed class UpdateEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||||
|
public required object Entity { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SelectEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||||
|
public required object Entity { get; init; }
|
||||||
|
public required bool Selected { get; set; }
|
||||||
|
}
|
||||||
27
src/HopFrame.Web/Plugins/Events/HopFrameEventArgs.cs
Normal file
27
src/HopFrame.Web/Plugins/Events/HopFrameEventArgs.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using HopFrame.Core.Config;
|
||||||
|
using HopFrame.Web.Components.Dialogs;
|
||||||
|
using HopFrame.Web.Components.Pages;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins.Events;
|
||||||
|
|
||||||
|
public abstract class HopFrameEventArgs(object internalSender) {
|
||||||
|
internal object InternalSender { get; } = internalSender;
|
||||||
|
public bool IsCanceled { get; protected set; }
|
||||||
|
|
||||||
|
|
||||||
|
public void SetCancelled(bool canceled) => IsCanceled = canceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class HopFrameEventArgs<TSender>(TSender sender) : HopFrameEventArgs(sender) where TSender : class {
|
||||||
|
public TSender Sender => (TSender)InternalSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class HopFrameTablePageEventArgs(HopFrameTablePage sender)
|
||||||
|
: HopFrameEventArgs<HopFrameTablePage>(sender) {
|
||||||
|
public required TableConfig Table { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class HopFrameEditorEventArgs(HopFrameEditor sender)
|
||||||
|
: HopFrameEventArgs<HopFrameEditor>(sender) {
|
||||||
|
public required TableConfig Table { get; init; }
|
||||||
|
}
|
||||||
9
src/HopFrame.Web/Plugins/Events/PageChangeEvent.cs
Normal file
9
src/HopFrame.Web/Plugins/Events/PageChangeEvent.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using HopFrame.Web.Components.Pages;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins.Events;
|
||||||
|
|
||||||
|
public sealed class PageChangeEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||||
|
public required int CurrentPage { get; init; }
|
||||||
|
public required int TotalPages { get; init; }
|
||||||
|
public required int NewPage { get; set; }
|
||||||
|
}
|
||||||
9
src/HopFrame.Web/Plugins/Events/PluginEventContainer.cs
Normal file
9
src/HopFrame.Web/Plugins/Events/PluginEventContainer.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins.Events;
|
||||||
|
|
||||||
|
internal sealed class PluginEventContainer {
|
||||||
|
public required MethodInfo Handler { get; init; }
|
||||||
|
public required Type EventType { get; init; }
|
||||||
|
public required bool IsAwaitable { get; init; }
|
||||||
|
}
|
||||||
7
src/HopFrame.Web/Plugins/Events/ReloadEvent.cs
Normal file
7
src/HopFrame.Web/Plugins/Events/ReloadEvent.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using HopFrame.Web.Components.Pages;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins.Events;
|
||||||
|
|
||||||
|
public sealed class ReloadEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||||
|
|
||||||
|
}
|
||||||
22
src/HopFrame.Web/Plugins/Events/SearchEvent.cs
Normal file
22
src/HopFrame.Web/Plugins/Events/SearchEvent.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using HopFrame.Web.Components.Pages;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins.Events;
|
||||||
|
|
||||||
|
public sealed class SearchEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||||
|
public required string SearchTerm { get; set; }
|
||||||
|
public required int CurrentPage { get; init; }
|
||||||
|
internal IEnumerable<object>? SearchResult { get; set; }
|
||||||
|
internal int TotalPages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the new search result that is being displayed<br />
|
||||||
|
/// The event needs to be canceled in order for the custom search results to appear
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The current page of search results</param>
|
||||||
|
/// <param name="totalPages">The total pages of search results</param>
|
||||||
|
public void SetSearchResult(IEnumerable result, int totalPages) {
|
||||||
|
SearchResult = result.OfType<object>();
|
||||||
|
TotalPages = totalPages;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/HopFrame.Web/Plugins/Events/TableInitializedEvent.cs
Normal file
101
src/HopFrame.Web/Plugins/Events/TableInitializedEvent.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using HopFrame.Core.Config;
|
||||||
|
using HopFrame.Web.Components.Pages;
|
||||||
|
using Microsoft.FluentUI.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins.Events;
|
||||||
|
|
||||||
|
public class TableInitializedEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||||
|
public List<PluginButton> PluginButtons { get; } = new();
|
||||||
|
public DefaultButtonToggles DefaultButtons { get; set; } = new();
|
||||||
|
|
||||||
|
public void AddPageButton(string title, Func<Task> callback, bool pushRight = false, IconInfo? icon = null) {
|
||||||
|
PluginButtons.Add(new() {
|
||||||
|
Title = title,
|
||||||
|
Icon = icon,
|
||||||
|
Position = pushRight ? PluginButtonPosition.TopRight : PluginButtonPosition.TopLeft,
|
||||||
|
Handler = (_, _) => callback.Invoke()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddPageButton(string title, Action callback, bool pushRight = false, IconInfo? icon = null) {
|
||||||
|
AddPageButton(title, () => {
|
||||||
|
callback.Invoke();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}, pushRight, icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEntityButton(IconInfo icon, Func<object, TableConfig, Task> callback) {
|
||||||
|
PluginButtons.Add(new() {
|
||||||
|
Icon = icon,
|
||||||
|
Position = PluginButtonPosition.OnEntry,
|
||||||
|
Handler = callback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEntityButton(IconInfo icon, Action<object, TableConfig> callback) {
|
||||||
|
AddEntityButton(icon, (obj, cfg) => {
|
||||||
|
callback.Invoke(obj, cfg);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEntityButton<TEntity>(IconInfo icon, Func<TEntity, TableConfig, Task> callback) {
|
||||||
|
PluginButtons.Add(new() {
|
||||||
|
Icon = icon,
|
||||||
|
Position = PluginButtonPosition.OnEntry,
|
||||||
|
Handler = (obj, cfg) => callback.Invoke((TEntity)obj, cfg),
|
||||||
|
TableFilter = typeof(TEntity)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEntityButton<TEntity>(IconInfo icon, Action<TEntity, TableConfig> callback) {
|
||||||
|
AddEntityButton<TEntity>(icon, (obj, cfg) => {
|
||||||
|
callback.Invoke(obj, cfg);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddPageButton<TEntity>(string title, Func<Task> callback, bool pushRight = false, IconInfo? icon = null) {
|
||||||
|
PluginButtons.Add(new() {
|
||||||
|
Title = title,
|
||||||
|
Icon = icon,
|
||||||
|
Position = pushRight ? PluginButtonPosition.TopRight : PluginButtonPosition.TopLeft,
|
||||||
|
Handler = (_, _) => callback.Invoke(),
|
||||||
|
TableFilter = typeof(TEntity)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddPageButton<TEntity>(string title, Action callback, bool pushRight = false, IconInfo? icon = null) {
|
||||||
|
AddPageButton<TEntity>(title, () => {
|
||||||
|
callback.Invoke();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}, pushRight, icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PluginButton {
|
||||||
|
public PluginButtonPosition Position { get; set; }
|
||||||
|
public Func<object, TableConfig, Task> Handler { get; set; }
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public IconInfo? Icon { get; set; }
|
||||||
|
public Type? TableFilter { get; set; }
|
||||||
|
|
||||||
|
internal bool IsForTable(TableConfig? config) {
|
||||||
|
if (config is null) return false;
|
||||||
|
if (TableFilter is null) return true;
|
||||||
|
return config.TableType == TableFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PluginButtonPosition {
|
||||||
|
TopLeft = 0,
|
||||||
|
TopRight = 1,
|
||||||
|
OnEntry = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DefaultButtonToggles() {
|
||||||
|
public bool ShowRefreshButton { get; set; } = true;
|
||||||
|
public bool ShowAddEntityButton { get; set; } = true;
|
||||||
|
public bool ShowDeleteButton { get; set; } = true;
|
||||||
|
public bool ShowEditButton { get; set; } = true;
|
||||||
|
}
|
||||||
9
src/HopFrame.Web/Plugins/Events/ValidationEvent.cs
Normal file
9
src/HopFrame.Web/Plugins/Events/ValidationEvent.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using HopFrame.Core.Config;
|
||||||
|
using HopFrame.Web.Components.Dialogs;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins.Events;
|
||||||
|
|
||||||
|
public sealed class ValidationEvent(HopFrameEditor sender) : HopFrameEditorEventArgs(sender) {
|
||||||
|
public required IList<string> Errors { get; init; }
|
||||||
|
public required PropertyConfig Property { get; init; }
|
||||||
|
}
|
||||||
7
src/HopFrame.Web/Plugins/IPluginOrchestrator.cs
Normal file
7
src/HopFrame.Web/Plugins/IPluginOrchestrator.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using HopFrame.Web.Plugins.Events;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins;
|
||||||
|
|
||||||
|
public interface IPluginOrchestrator {
|
||||||
|
public Task<TEvent> DispatchEvent<TEvent>(TEvent @event, CancellationToken ct = new()) where TEvent : HopFrameEventArgs;
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
57
src/HopFrame.Web/Plugins/Internal/PluginOrchestrator.cs
Normal file
57
src/HopFrame.Web/Plugins/Internal/PluginOrchestrator.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using HopFrame.Web.Plugins.Annotations;
|
||||||
|
using HopFrame.Web.Plugins.Events;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Plugins.Internal;
|
||||||
|
|
||||||
|
internal sealed class PluginOrchestrator(IServiceProvider services) : IPluginOrchestrator {
|
||||||
|
|
||||||
|
public static void RegisterPlugin(IServiceCollection collection, Type plugin) {
|
||||||
|
var methods = plugin.GetMethods()
|
||||||
|
.Where(method => method.GetCustomAttributes(true)
|
||||||
|
.Any(attr => attr is EventHandlerAttribute));
|
||||||
|
|
||||||
|
foreach (var method in methods) {
|
||||||
|
var awaitable = method.ReturnType.IsAssignableFrom(typeof(Task));
|
||||||
|
var eventType = method
|
||||||
|
.GetParameters()
|
||||||
|
.FirstOrDefault(param => param.ParameterType.IsAssignableTo(typeof(HopFrameEventArgs)))?.ParameterType;
|
||||||
|
|
||||||
|
if (eventType is null) continue;
|
||||||
|
var container = new PluginEventContainer {
|
||||||
|
EventType = eventType,
|
||||||
|
IsAwaitable = awaitable,
|
||||||
|
Handler = method
|
||||||
|
};
|
||||||
|
collection.AddSingleton(container);
|
||||||
|
collection.AddScoped(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TEvent> DispatchEvent<TEvent>(TEvent @event, CancellationToken ct = new()) where TEvent : HopFrameEventArgs {
|
||||||
|
var eventContainers = services.GetRequiredService<IEnumerable<PluginEventContainer>>()
|
||||||
|
.Where(container => container.EventType == typeof(TEvent));
|
||||||
|
|
||||||
|
var eventType = typeof(TEvent);
|
||||||
|
var tokenType = typeof(CancellationToken);
|
||||||
|
foreach (var container in eventContainers) {
|
||||||
|
var plugin = services.GetRequiredService(container.Handler.DeclaringType!);
|
||||||
|
var parameters = new List<object?>();
|
||||||
|
|
||||||
|
foreach (var parameter in container.Handler.GetParameters()) {
|
||||||
|
if (parameter.ParameterType == eventType)
|
||||||
|
parameters.Add(@event);
|
||||||
|
else if (parameter.ParameterType == tokenType)
|
||||||
|
parameters.Add(ct);
|
||||||
|
else parameters.Add(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = container.Handler.Invoke(plugin, parameters.ToArray());
|
||||||
|
|
||||||
|
if (container.IsAwaitable)
|
||||||
|
await (Task)result!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return @event;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
using HopFrame.Core;
|
using HopFrame.Core;
|
||||||
using HopFrame.Core.Config;
|
using HopFrame.Core.Config;
|
||||||
|
using HopFrame.Core.Callbacks;
|
||||||
using HopFrame.Web.Components;
|
using HopFrame.Web.Components;
|
||||||
using HopFrame.Web.Components.Pages;
|
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.Extensions.DependencyInjection;
|
||||||
using Microsoft.FluentUI.AspNetCore.Components;
|
using Microsoft.FluentUI.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
@@ -20,7 +25,7 @@ public static class ServiceCollectionExtensions {
|
|||||||
/// <returns>The same service collection that is passed in</returns>
|
/// <returns>The same service collection that is passed in</returns>
|
||||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator, LibraryConfiguration? fluentUiLibraryConfiguration = null, bool addRazorComponents = true) {
|
public static IServiceCollection AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator, LibraryConfiguration? fluentUiLibraryConfiguration = null, bool addRazorComponents = true) {
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
configurator.Invoke(new HopFrameConfigurator(config));
|
configurator.Invoke(new HopFrameConfigurator(config, services));
|
||||||
return AddHopFrame(services, config, fluentUiLibraryConfiguration, addRazorComponents);
|
return AddHopFrame(services, config, fluentUiLibraryConfiguration, addRazorComponents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +42,9 @@ public static class ServiceCollectionExtensions {
|
|||||||
services.AddHopFrameServices();
|
services.AddHopFrameServices();
|
||||||
services.AddFluentUIComponents(fluentUiLibraryConfiguration);
|
services.AddFluentUIComponents(fluentUiLibraryConfiguration);
|
||||||
|
|
||||||
|
services.AddScoped<IPluginOrchestrator, PluginOrchestrator>();
|
||||||
|
services.AddScoped<IFileService, FileService>();
|
||||||
|
|
||||||
if (addRazorComponents) {
|
if (addRazorComponents) {
|
||||||
services.AddRazorComponents()
|
services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -69,3 +69,7 @@ footer a:focus {
|
|||||||
footer a:hover {
|
footer a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-header.select-all > svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@page "/counter"
|
@page "/counter"
|
||||||
|
@using HopFrame.Web.Components.Layout
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
@layout HopFrameLayout
|
||||||
|
|
||||||
<PageTitle>Counter</PageTitle>
|
<PageTitle>Counter</PageTitle>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
using System.Collections;
|
|
||||||
using HopFrame.Testing;
|
using HopFrame.Testing;
|
||||||
using Microsoft.FluentUI.AspNetCore.Components;
|
using Microsoft.FluentUI.AspNetCore.Components;
|
||||||
using HopFrame.Testing.Components;
|
using HopFrame.Testing.Components;
|
||||||
using HopFrame.Testing.Models;
|
using HopFrame.Testing.Models;
|
||||||
using HopFrame.Web;
|
using HopFrame.Web;
|
||||||
using HopFrame.Web.Components.Pages;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -36,6 +34,11 @@ builder.Services.AddHopFrame(options => {
|
|||||||
.SetOrderIndex(3);
|
.SetOrderIndex(3);
|
||||||
|
|
||||||
table.AddVirtualProperty("Name", (user, _) => $"{user.FirstName} {user.LastName}")
|
table.AddVirtualProperty("Name", (user, _) => $"{user.FirstName} {user.LastName}")
|
||||||
|
/*.SetVirtualParser((model, input, _) => {
|
||||||
|
var split = input.Split(' ');
|
||||||
|
model.FirstName = split.FirstOrDefault();
|
||||||
|
model.LastName = split.LastOrDefault();
|
||||||
|
})*/
|
||||||
.SetOrderIndex(2);
|
.SetOrderIndex(2);
|
||||||
|
|
||||||
table.SetDisplayName("Benutzer");
|
table.SetDisplayName("Benutzer");
|
||||||
@@ -49,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)
|
||||||
@@ -74,9 +78,16 @@ builder.Services.AddHopFrame(options => {
|
|||||||
return errors;
|
return errors;
|
||||||
})*/;
|
})*/;
|
||||||
|
|
||||||
context.Table<Post>()
|
/*context.Table<Post>()
|
||||||
.SetOrderIndex(-1);
|
.SetOrderIndex(-1)
|
||||||
|
.Ignore(true);*/
|
||||||
});
|
});
|
||||||
|
|
||||||
|
options.AddCustomView("Counter", "/counter")
|
||||||
|
.SetDescription("A custom view")
|
||||||
|
.SetPolicy("counter.view");
|
||||||
|
|
||||||
|
options.AddExporters();
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public class DbContextConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Table_WithConfigurator_InvokesConfigurator() {
|
public void Table_WithConfigurator_InvokesConfigurator() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var dbContextConfig = new DbContextConfig(typeof(MockDbContext));
|
var dbContextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var configurator = new DbContextConfigurator<MockDbContext>(dbContextConfig);
|
var configurator = new DbContextConfigurator<MockDbContext>(dbContextConfig);
|
||||||
var mockConfigurator = new Mock<Action<TableConfigurator<MockModel>>>();
|
var mockConfigurator = new Mock<Action<TableConfigurator<MockModel>>>();
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ public class DbContextConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Table_ReturnsCorrectTableConfigurator() {
|
public void Table_ReturnsCorrectTableConfigurator() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var dbContextConfig = new DbContextConfig(typeof(MockDbContext));
|
var dbContextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var configurator = new DbContextConfigurator<MockDbContext>(dbContextConfig);
|
var configurator = new DbContextConfigurator<MockDbContext>(dbContextConfig);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void SetDisplayName_SetsNameProperty() {
|
public void SetDisplayName_SetsNameProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
var displayName = "ID";
|
var displayName = "ID";
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void List_SetsListAndSearchableProperties() {
|
public void List_SetsListAndSearchableProperties() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -39,7 +39,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void IsSortable_SetsSortableProperty() {
|
public void IsSortable_SetsSortableProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -53,7 +53,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void IsSearchable_SetsSearchableProperty() {
|
public void IsSearchable_SetsSearchableProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -67,7 +67,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void SetDisplayedProperty_SetsDisplayedProperty() {
|
public void SetDisplayedProperty_SetsDisplayedProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<MockModel>(propertyConfig);
|
var configurator = new PropertyConfigurator<MockModel>(propertyConfig);
|
||||||
Expression<Func<MockModel, int>> propertyExpression = model => model.Id;
|
Expression<Func<MockModel, int>> propertyExpression = model => model.Id;
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void Format_SetsFormatter() {
|
public void Format_SetsFormatter() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
Func<int, IServiceProvider, string> formatter = (val, _) => val.ToString();
|
Func<int, IServiceProvider, string> formatter = (val, _) => val.ToString();
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void SetParser_SetsParser() {
|
public void SetParser_SetsParser() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
Func<string, IServiceProvider, int> parser = (str, _) => int.Parse(str);
|
Func<string, IServiceProvider, int> parser = (str, _) => int.Parse(str);
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void SetEditable_SetsEditableProperty() {
|
public void SetEditable_SetsEditableProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -127,7 +127,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void SetCreatable_SetsCreatableProperty() {
|
public void SetCreatable_SetsCreatableProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -141,7 +141,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void DisplayValue_SetsDisplayValueProperty() {
|
public void DisplayValue_SetsDisplayValueProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -155,7 +155,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void IsTextArea_SetsTextAreaProperty() {
|
public void IsTextArea_SetsTextAreaProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -169,7 +169,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void SetTextAreaRows_SetsTextAreaRowsProperty() {
|
public void SetTextAreaRows_SetsTextAreaRowsProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
var rows = 10;
|
var rows = 10;
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void SetValidator_SetsValidator() {
|
public void SetValidator_SetsValidator() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
Func<int, IServiceProvider, IEnumerable<string>> validator = (_, _) => new List<string>();
|
Func<int, IServiceProvider, IEnumerable<string>> validator = (_, _) => new List<string>();
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ public class PropertyConfiguratorTests {
|
|||||||
public void SetOrderIndex_SetsOrderProperty() {
|
public void SetOrderIndex_SetsOrderProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||||
var orderIndex = 1;
|
var orderIndex = 1;
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ public class PropertyConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_SetsTableProperty() {
|
public void Constructor_SetsTableProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!, tableConfig, 0);
|
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!, tableConfig, 0);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public class TableConfiguratorTests {
|
|||||||
public void Ignore_SetsIgnoredProperty() {
|
public void Ignore_SetsIgnoredProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig =
|
var tableConfig =
|
||||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -22,7 +22,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Property_ReturnsCorrectPropertyConfigurator() {
|
public void Property_ReturnsCorrectPropertyConfigurator() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
Expression<Func<MockModel, int>> propertyExpression = model => model.Id;
|
Expression<Func<MockModel, int>> propertyExpression = model => model.Id;
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ public class TableConfiguratorTests {
|
|||||||
|
|
||||||
public void Property_WithConfigurator_ReturnsCorrectPropertyConfigurator() {
|
public void Property_WithConfigurator_ReturnsCorrectPropertyConfigurator() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
Expression<Func<MockModel, int>> propertyExpression = model => model.Id;
|
Expression<Func<MockModel, int>> propertyExpression = model => model.Id;
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void AddVirtualProperty_AddsVirtualPropertyToConfig() {
|
public void AddVirtualProperty_AddsVirtualPropertyToConfig() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
Func<MockModel, IServiceProvider, string> template = (model, _) => model.Name!;
|
Func<MockModel, IServiceProvider, string> template = (model, _) => model.Name!;
|
||||||
|
|
||||||
@@ -63,14 +63,14 @@ public class TableConfiguratorTests {
|
|||||||
var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName");
|
var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName");
|
||||||
Assert.NotNull(virtualProperty);
|
Assert.NotNull(virtualProperty);
|
||||||
Assert.NotNull(propertyConfigurator);
|
Assert.NotNull(propertyConfigurator);
|
||||||
Assert.True(virtualProperty.IsListingProperty);
|
Assert.True(virtualProperty.IsVirtualProperty);
|
||||||
Assert.Equal("VirtualName", virtualProperty.Name);
|
Assert.Equal("VirtualName", virtualProperty.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AddVirtualProperty_WithConfigurator_AddsVirtualPropertyToConfig() {
|
public void AddVirtualProperty_WithConfigurator_AddsVirtualPropertyToConfig() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
Func<MockModel, IServiceProvider, string> template = (model, _) => model.Name!;
|
Func<MockModel, IServiceProvider, string> template = (model, _) => model.Name!;
|
||||||
|
|
||||||
@@ -84,14 +84,14 @@ public class TableConfiguratorTests {
|
|||||||
var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName");
|
var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName");
|
||||||
Assert.NotNull(virtualProperty);
|
Assert.NotNull(virtualProperty);
|
||||||
Assert.NotNull(propertyConfigurator);
|
Assert.NotNull(propertyConfigurator);
|
||||||
Assert.True(virtualProperty.IsListingProperty);
|
Assert.True(virtualProperty.IsVirtualProperty);
|
||||||
Assert.Equal("VirtualName", virtualProperty.Name);
|
Assert.Equal("VirtualName", virtualProperty.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SetDisplayName_SetsDisplayNameProperty() {
|
public void SetDisplayName_SetsDisplayNameProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
var displayName = "Mock Model Display Name";
|
var displayName = "Mock Model Display Name";
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void SetDescription_SetsDescriptionProperty() {
|
public void SetDescription_SetsDescriptionProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
var description = "Mock Model Description";
|
var description = "Mock Model Description";
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void SetOrderIndex_SetsOrderIndexProperty() {
|
public void SetOrderIndex_SetsOrderIndexProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
var orderIndex = 1;
|
var orderIndex = 1;
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void SetViewPolicy_SetsViewPolicyProperty() {
|
public void SetViewPolicy_SetsViewPolicyProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
var policy = "ViewPolicy";
|
var policy = "ViewPolicy";
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void SetUpdatePolicy_SetsUpdatePolicyProperty() {
|
public void SetUpdatePolicy_SetsUpdatePolicyProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
var policy = "UpdatePolicy";
|
var policy = "UpdatePolicy";
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void SetCreatePolicy_SetsCreatePolicyProperty() {
|
public void SetCreatePolicy_SetsCreatePolicyProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
var policy = "CreatePolicy";
|
var policy = "CreatePolicy";
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void SetDeletePolicy_SetsDeletePolicyProperty() {
|
public void SetDeletePolicy_SetsDeletePolicyProperty() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||||
var policy = "DeletePolicy";
|
var policy = "DeletePolicy";
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_WithKeyProperty_DisablesEdit() {
|
public void Constructor_WithKeyProperty_DisablesEdit() {
|
||||||
// Act
|
// Act
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel2), "Models2", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel2), "Models2", 0);
|
||||||
var prop = tableConfig.Properties.SingleOrDefault(prop => prop.Info.Name == nameof(MockModel2.Id));
|
var prop = tableConfig.Properties.SingleOrDefault(prop => prop.Info.Name == nameof(MockModel2.Id));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -200,7 +200,7 @@ public class TableConfiguratorTests {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_WithGeneratedProperty_DisablesEditAndCreate() {
|
public void Constructor_WithGeneratedProperty_DisablesEditAndCreate() {
|
||||||
// Act
|
// Act
|
||||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel2), "Models2", 0);
|
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel2), "Models2", 0);
|
||||||
var prop = tableConfig.Properties.SingleOrDefault(prop => prop.Info.Name == nameof(MockModel2.Number));
|
var prop = tableConfig.Properties.SingleOrDefault(prop => prop.Info.Name == nameof(MockModel2.Number));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class ContextExplorerTests {
|
|||||||
public void GetTables_ReturnsNonIgnoredTables() {
|
public void GetTables_ReturnsNonIgnoredTables() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig1 = contextConfig.Tables[0];
|
var tableConfig1 = contextConfig.Tables[0];
|
||||||
var tableConfig2 = contextConfig.Tables[1];
|
var tableConfig2 = contextConfig.Tables[1];
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
@@ -33,7 +33,7 @@ public class ContextExplorerTests {
|
|||||||
public void GetTable_ByDisplayName_ReturnsCorrectTable() {
|
public void GetTable_ByDisplayName_ReturnsCorrectTable() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig = contextConfig.Tables[0];
|
var tableConfig = contextConfig.Tables[0];
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
tableConfig.DisplayName = "TestTable";
|
tableConfig.DisplayName = "TestTable";
|
||||||
@@ -54,7 +54,7 @@ public class ContextExplorerTests {
|
|||||||
public void GetTable_ByDisplayName_ReturnsNullIfTableNotFound() {
|
public void GetTable_ByDisplayName_ReturnsNullIfTableNotFound() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig = contextConfig.Tables[0];
|
var tableConfig = contextConfig.Tables[0];
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
tableConfig.DisplayName = "TestTable";
|
tableConfig.DisplayName = "TestTable";
|
||||||
@@ -74,7 +74,7 @@ public class ContextExplorerTests {
|
|||||||
public void GetTable_ByType_ReturnsCorrectTable() {
|
public void GetTable_ByType_ReturnsCorrectTable() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig = contextConfig.Tables[0];
|
var tableConfig = contextConfig.Tables[0];
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ public class ContextExplorerTests {
|
|||||||
public void GetTable_ByType_ReturnsNullIfTableNotFound() {
|
public void GetTable_ByType_ReturnsNullIfTableNotFound() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig = contextConfig.Tables[0];
|
var tableConfig = contextConfig.Tables[0];
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ public class ContextExplorerTests {
|
|||||||
public void GetTableManager_ReturnsCorrectTableManager() {
|
public void GetTableManager_ReturnsCorrectTableManager() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig = new TableConfig(contextConfig, typeof(MockModel), "Models", 0);
|
var tableConfig = new TableConfig(contextConfig, typeof(MockModel), "Models", 0);
|
||||||
contextConfig.Tables.Add(tableConfig);
|
contextConfig.Tables.Add(tableConfig);
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
@@ -135,7 +135,7 @@ public class ContextExplorerTests {
|
|||||||
public void GetTableManager_ReturnsNullIfDbContextNotFound() {
|
public void GetTableManager_ReturnsNullIfDbContextNotFound() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig = new TableConfig(contextConfig, typeof(MockModel), "MockModels", 0);
|
var tableConfig = new TableConfig(contextConfig, typeof(MockModel), "MockModels", 0);
|
||||||
contextConfig.Tables.Add(tableConfig);
|
contextConfig.Tables.Add(tableConfig);
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
@@ -154,7 +154,7 @@ public class ContextExplorerTests {
|
|||||||
public void GetTableManager_ReturnsNullIfTableNotFound() {
|
public void GetTableManager_ReturnsNullIfTableNotFound() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig = new TableConfig(contextConfig, typeof(MockModel), "Models", 0);
|
var tableConfig = new TableConfig(contextConfig, typeof(MockModel), "Models", 0);
|
||||||
contextConfig.Tables.Add(tableConfig);
|
contextConfig.Tables.Add(tableConfig);
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
@@ -175,7 +175,7 @@ public class ContextExplorerTests {
|
|||||||
public void SeedTableData_SetsTableSeededFlag() {
|
public void SeedTableData_SetsTableSeededFlag() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig = contextConfig.Tables[0];
|
var tableConfig = contextConfig.Tables[0];
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ public class ContextExplorerTests {
|
|||||||
public void SeedTableData_SetsTablePropertiesCorrectly() {
|
public void SeedTableData_SetsTablePropertiesCorrectly() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||||
var tableConfig = contextConfig.Tables[0];
|
var tableConfig = contextConfig.Tables[0];
|
||||||
var tableConfig2 = contextConfig.Tables[1];
|
var tableConfig2 = contextConfig.Tables[1];
|
||||||
config.Contexts.Add(contextConfig);
|
config.Contexts.Add(contextConfig);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class DisplayPropertyTests {
|
|||||||
var contextMock = new Mock<DbContext>();
|
var contextMock = new Mock<DbContext>();
|
||||||
_providerMock = new Mock<IServiceProvider>();
|
_providerMock = new Mock<IServiceProvider>();
|
||||||
_explorerMock = new Mock<IContextExplorer>();
|
_explorerMock = new Mock<IContextExplorer>();
|
||||||
_config = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0);
|
_config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
_tableManager =
|
_tableManager =
|
||||||
new TableManager<object>(contextMock.Object, _config, _explorerMock.Object, _providerMock.Object);
|
new TableManager<object>(contextMock.Object, _config, _explorerMock.Object, _providerMock.Object);
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ public class DisplayPropertyTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var item = "test";
|
var item = "test";
|
||||||
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) {
|
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) {
|
||||||
IsListingProperty = true,
|
IsVirtualProperty = true,
|
||||||
Formatter = (obj, provider) => Task.FromResult(((string)obj).ToUpper())
|
Formatter = (obj, provider) => Task.FromResult(((string)obj).ToUpper())
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ public class DisplayPropertyTests {
|
|||||||
|
|
||||||
_explorerMock
|
_explorerMock
|
||||||
.Setup(e => e.GetTable(item.Inner.GetType()))
|
.Setup(e => e.GetTable(item.Inner.GetType()))
|
||||||
.Returns(new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0) {
|
.Returns(new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0) {
|
||||||
Properties = { innerPropConfig }
|
Properties = { innerPropConfig }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ public class DisplayPropertyTests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _tableManager.DisplayProperty(item, prop, item.List);
|
var result = await _tableManager.DisplayProperty(item, prop, null, item.List);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("1,2,3", result);
|
Assert.Equal("1,2,3", result);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public class TableManagerTests {
|
|||||||
new MockModel { Id = 3, Name = "Item3" }
|
new MockModel { Id = 3, Name = "Item3" }
|
||||||
};
|
};
|
||||||
var dbContext = CreateMockDbContext(data);
|
var dbContext = CreateMockDbContext(data);
|
||||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0);
|
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
||||||
@@ -70,7 +70,7 @@ public class TableManagerTests {
|
|||||||
new MockModel { Id = 3, Name = "TestItem" }
|
new MockModel { Id = 3, Name = "TestItem" }
|
||||||
};
|
};
|
||||||
var dbContext = CreateMockDbContext(data);
|
var dbContext = CreateMockDbContext(data);
|
||||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0);
|
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
config.Properties.Add(new PropertyConfig(typeof(MockModel).GetProperty("Name")!, config, 0)
|
config.Properties.Add(new PropertyConfig(typeof(MockModel).GetProperty("Name")!, config, 0)
|
||||||
{ Searchable = true });
|
{ Searchable = true });
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
@@ -96,7 +96,7 @@ public class TableManagerTests {
|
|||||||
new MockModel { Id = 3, Name = "Item3" }
|
new MockModel { Id = 3, Name = "Item3" }
|
||||||
};
|
};
|
||||||
var dbContext = new MockDbContext();
|
var dbContext = new MockDbContext();
|
||||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0);
|
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext, config, explorer.Object, provider.Object);
|
||||||
@@ -118,7 +118,7 @@ public class TableManagerTests {
|
|||||||
new MockModel { Id = 2, Name = "Item2" }
|
new MockModel { Id = 2, Name = "Item2" }
|
||||||
};
|
};
|
||||||
var dbContext = CreateMockDbContext(data);
|
var dbContext = CreateMockDbContext(data);
|
||||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0);
|
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
||||||
@@ -139,7 +139,7 @@ public class TableManagerTests {
|
|||||||
new MockModel { Id = 1, Name = "Item1" }
|
new MockModel { Id = 1, Name = "Item1" }
|
||||||
};
|
};
|
||||||
var dbContext = CreateMockDbContext(data);
|
var dbContext = CreateMockDbContext(data);
|
||||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0);
|
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
||||||
@@ -156,7 +156,7 @@ public class TableManagerTests {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var data = new List<MockModel>();
|
var data = new List<MockModel>();
|
||||||
var dbContext = CreateMockDbContext(data);
|
var dbContext = CreateMockDbContext(data);
|
||||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0);
|
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
||||||
@@ -170,7 +170,7 @@ public class TableManagerTests {
|
|||||||
dbContext.Verify(m => m.Set<MockModel>().AddAsync(newItem, It.IsAny<CancellationToken>()), Times.Once);
|
dbContext.Verify(m => m.Set<MockModel>().AddAsync(newItem, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
/*[Fact]
|
||||||
public async Task RevertChanges_ReloadsItem() {
|
public async Task RevertChanges_ReloadsItem() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var data = new List<MockModel> {
|
var data = new List<MockModel> {
|
||||||
@@ -187,6 +187,6 @@ public class TableManagerTests {
|
|||||||
await manager.RevertChanges(item);
|
await manager.RevertChanges(item);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
dbContext.Verify(m => m.Entry(item), Times.Once);
|
dbContext.Verify(m => m.Entry(item), Times.AtLeastOnce);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ public class HopFrameEditorTests : TestContext {
|
|||||||
var dialogServiceMock = new Mock<IDialogService>();
|
var dialogServiceMock = new Mock<IDialogService>();
|
||||||
var toastServiceMock = new Mock<IToastService>();
|
var toastServiceMock = new Mock<IToastService>();
|
||||||
var serviceProviderMock = new Mock<IServiceProvider>();
|
var serviceProviderMock = new Mock<IServiceProvider>();
|
||||||
var contextConfig = new DbContextConfig(typeof(MyDbContext));
|
var contextConfig = new DbContextConfig(typeof(MyDbContext), null!);
|
||||||
var tableConfig = new TableConfig(contextConfig, typeof(MyTable), "Table1", 0) {
|
var tableConfig = new TableConfig(contextConfig, typeof(MyTable), "Table1", 0) {
|
||||||
DisplayName = "Table1",
|
DisplayName = "Table1",
|
||||||
ViewPolicy = "Policy1"
|
ViewPolicy = "Policy1"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class HopFrameSideMenuTests : TestContext {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var contextExplorerMock = new Mock<IContextExplorer>();
|
var contextExplorerMock = new Mock<IContextExplorer>();
|
||||||
var authHandlerMock = new Mock<IHopFrameAuthHandler>();
|
var authHandlerMock = new Mock<IHopFrameAuthHandler>();
|
||||||
var contextConfig = new DbContextConfig(typeof(MyDbContext));
|
var contextConfig = new DbContextConfig(typeof(MyDbContext), null!);
|
||||||
var tableConfigs = new List<TableConfig> {
|
var tableConfigs = new List<TableConfig> {
|
||||||
new (contextConfig, typeof(MyTable), "Table1", 0),
|
new (contextConfig, typeof(MyTable), "Table1", 0),
|
||||||
new (contextConfig, typeof(MyTable2), "Table2", 1)
|
new (contextConfig, typeof(MyTable2), "Table2", 1)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class HopFrameHomeTests : TestContext {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var contextExplorerMock = new Mock<IContextExplorer>();
|
var contextExplorerMock = new Mock<IContextExplorer>();
|
||||||
var authHandlerMock = new Mock<IHopFrameAuthHandler>();
|
var authHandlerMock = new Mock<IHopFrameAuthHandler>();
|
||||||
var contextConfig = new DbContextConfig(typeof(MyDbContext));
|
var contextConfig = new DbContextConfig(typeof(MyDbContext), null!);
|
||||||
var tableConfigs = new List<TableConfig> {
|
var tableConfigs = new List<TableConfig> {
|
||||||
new TableConfig(contextConfig, typeof(MyTable), "Table1", 0) {
|
new TableConfig(contextConfig, typeof(MyTable), "Table1", 0) {
|
||||||
DisplayName = "Table1",
|
DisplayName = "Table1",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class HopFrameTablePageTests : TestContext {
|
|||||||
var contextExplorerMock = new Mock<IContextExplorer>();
|
var contextExplorerMock = new Mock<IContextExplorer>();
|
||||||
var authHandlerMock = new Mock<IHopFrameAuthHandler>();
|
var authHandlerMock = new Mock<IHopFrameAuthHandler>();
|
||||||
var dialogServiceMock = new Mock<IDialogService>();
|
var dialogServiceMock = new Mock<IDialogService>();
|
||||||
var contextConfig = new DbContextConfig(typeof(MyDbContext));
|
var contextConfig = new DbContextConfig(typeof(MyDbContext), null!);
|
||||||
var managerMock = new Mock<ITableManager>();
|
var managerMock = new Mock<ITableManager>();
|
||||||
var tableConfig = new TableConfig(contextConfig, typeof(MyTable), "Table1", 0) {
|
var tableConfig = new TableConfig(contextConfig, typeof(MyTable), "Table1", 0) {
|
||||||
DisplayName = "Table1",
|
DisplayName = "Table1",
|
||||||
@@ -63,7 +63,7 @@ public class HopFrameTablePageTests : TestContext {
|
|||||||
var contextExplorerMock = new Mock<IContextExplorer>();
|
var contextExplorerMock = new Mock<IContextExplorer>();
|
||||||
var authHandlerMock = new Mock<IHopFrameAuthHandler>();
|
var authHandlerMock = new Mock<IHopFrameAuthHandler>();
|
||||||
var dialogServiceMock = new Mock<IDialogService>();
|
var dialogServiceMock = new Mock<IDialogService>();
|
||||||
var contextConfig = new DbContextConfig(typeof(MyDbContext));
|
var contextConfig = new DbContextConfig(typeof(MyDbContext), null!);
|
||||||
var tableConfig = new TableConfig(contextConfig, typeof(MyTable), "Table1", 0) {
|
var tableConfig = new TableConfig(contextConfig, typeof(MyTable), "Table1", 0) {
|
||||||
DisplayName = "Table1",
|
DisplayName = "Table1",
|
||||||
ViewPolicy = "Policy1"
|
ViewPolicy = "Policy1"
|
||||||
@@ -72,7 +72,7 @@ public class HopFrameTablePageTests : TestContext {
|
|||||||
var tableManagerMock = new Mock<ITableManager>();
|
var tableManagerMock = new Mock<ITableManager>();
|
||||||
var items = new List<object> { new MyTable(), new MyTable() };
|
var items = new List<object> { new MyTable(), new MyTable() };
|
||||||
tableManagerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(items.AsAsyncQueryable());
|
tableManagerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(items.AsAsyncQueryable());
|
||||||
tableManagerMock.Setup(t => t.DisplayProperty(It.IsAny<object>(), It.IsAny<PropertyConfig>(), null))
|
tableManagerMock.Setup(t => t.DisplayProperty(It.IsAny<object>(), It.IsAny<PropertyConfig>(), null, null))
|
||||||
.ReturnsAsync(string.Empty);
|
.ReturnsAsync(string.Empty);
|
||||||
|
|
||||||
contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig);
|
contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig);
|
||||||
|
|||||||
Reference in New Issue
Block a user