Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 052eed17de | |||
| a14f17e8c3 | |||
| 10913b0a21 | |||
| 31b0b3970a | |||
| e6726037b6 | |||
| c5388fc044 | |||
| 66d03513eb | |||
| 68a4479c2d | |||
| 5dec609004 | |||
| 7d3aa6de94 | |||
| 5c6fafcd6f | |||
| 222d4276d2 | |||
| 4407d173a9 |
@@ -1,8 +1,63 @@
|
||||
image: mcr.microsoft.com/dotnet/sdk:9.0
|
||||
image: mcr.microsoft.com/dotnet/sdk:10.0
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- publish
|
||||
- publish-help
|
||||
|
||||
build:
|
||||
stage: build
|
||||
only:
|
||||
- pushes
|
||||
script:
|
||||
- dotnet restore
|
||||
- dotnet build --configuration Release --no-restore
|
||||
artifacts:
|
||||
paths:
|
||||
- "**/bin/Release"
|
||||
expire_in: 10 minutes
|
||||
|
||||
test:
|
||||
stage: test
|
||||
only:
|
||||
- pushes
|
||||
script:
|
||||
- dotnet test HopFrame.sln --logger "trx;LogFilePrefix=testresults" --collect:"XPlat Code Coverage" --results-directory TestResults
|
||||
- dotnet tool install --global trx2junit
|
||||
- dotnet tool install --global dotnet-reportgenerator-globaltool
|
||||
- export PATH="$PATH:/root/.dotnet/tools"
|
||||
- for file in TestResults/*.trx; do trx2junit "$file"; done
|
||||
- reportgenerator -reports:"TestResults/**/coverage.cobertura.xml" -targetdir:"coveragereport" -reporttypes:"Cobertura;HtmlInline"
|
||||
- echo total_coverage=$(grep -o 'line-rate="[0-9.]*"' coveragereport/Cobertura.xml | head -n1 | sed -E 's/line-rate="([0-9.]+)"/\1/' | awk '{printf "%.2f", $1 * 100}')
|
||||
- tar -cvf coveragereport.tar coveragereport/
|
||||
artifacts:
|
||||
when: always
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- TestResults/*.xml
|
||||
- TestResults/**/*.coverage.cobertura.xml
|
||||
- coveragereport.tar
|
||||
reports:
|
||||
junit:
|
||||
- TestResults/*.xml
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coveragereport/Cobertura.xml
|
||||
coverage: '/total_coverage=(\d+(\.\d+)?)/'
|
||||
|
||||
publish:
|
||||
stage: publish
|
||||
script:
|
||||
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
|
||||
- dotnet pack -c Release -o . /p:Version=$VERSION
|
||||
- for nupkg in *.nupkg; do dotnet nuget push $nupkg -k ${NUGET_API_KEY} -s https://api.nuget.org/v3/index.json; done
|
||||
only:
|
||||
- tags
|
||||
dependencies:
|
||||
- build
|
||||
- test
|
||||
|
||||
publish-help:
|
||||
stage: publish-help
|
||||
image: docker:latest
|
||||
@@ -18,3 +73,5 @@ publish-help:
|
||||
- docker push registry.leon-hoppe.de/leon.hoppe/hopframe:latest
|
||||
only:
|
||||
- tags
|
||||
dependencies:
|
||||
- publish
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="RiderProjectSettingsUpdater">
|
||||
<option name="singleClickDiffPreview" value="1" />
|
||||
<option name="unhandledExceptionsIgnoreList" value="1" />
|
||||
<option name="vcsConfiguration" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
472
.idea/.idea.HopFrame/.idea/workspace.xml
generated
472
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -11,11 +11,7 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/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 default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
@@ -34,7 +30,7 @@
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="feature/virtual-properties" />
|
||||
<entry key="$PROJECT_DIR$" value="dev" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
@@ -52,55 +48,14 @@
|
||||
<component name="GitLabMergeRequestsSettings">{
|
||||
"selectedUrlAndAccountId": {
|
||||
"first": "https://git.leon-hoppe.de/leon.hoppe/hopframe.git",
|
||||
"second": "2d65fdcb-5f13-45ad-a7ba-91dd4a88d6e4"
|
||||
"second": "f58c9371-9f54-454e-a0db-5b4bc1187bad"
|
||||
}
|
||||
}</component>
|
||||
<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/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/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/8d5d6cbff46ddc7b152381f92ae1ae51d3e7b57b14dd23840a11f5aaaaed396/InternalEntityEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a882d183338544fdbcbdfc7b6d3dcb78916630765551644a221b5be9c45a121b/Int32.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/aa3ea54f92373c58ec1149fbd41215869a98bd385c30584bc6db2fa3c6e88443/Filled24.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/adcd2c45092dd8e4fc412325c8adb75d6e7d8b3e90a9523f167583fb9c60/ServiceCollectionExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/b3ccb66df3646cb51df73ad51716136ebd2eefb4edb1308dd52a7e999582d59e/IBindableColumn.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/bfff78ecaa39c818519fc918bb2d4bbdca6ad93d7170f5cf325f67ccd0b97d43/BooleanAsserts.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d0165cb640e16fb3b8fe6932c042fc2917cd7f2770ff123cf7b9d11b5bfc6/Task.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d04a416cac8afac0341a8be0e859b230f2eae64924298eef48c317ba35916/RenderTreeBuilder.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d1287462d4ec4078c61b8e92a0952fb7de3e7e877d279e390a4c136a6365126/Stream.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d39923abb31e6a6e7a9e8173e217da584c54925ce63e568126a2b89b9ab/DefaultRazorComponentsServiceOptionsConfiguration.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d858ddb35a8e36df5573b7612542f9ad50f426b8ab43818587d1ac65fab14829/DatabaseGeneratedAttribute.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/dac3553c90d47a746e7e7f02faecb1a5e581090/Components_AppBar_FluentAppBarItem_razor.g.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<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/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/fd57398b7dc3a8ce7da2786f2c67289c3d974658a9e90d0c1e84db3d965fbf1/Console.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="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/tests/HopFrame.Tests.Web/Components/Dialogs/HopFrameEditorTests.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
</component>
|
||||
<component name="KubernetesApiPersistence">{}</component>
|
||||
<component name="KubernetesApiProvider">{
|
||||
@@ -111,7 +66,7 @@
|
||||
"associatedIndex": 3
|
||||
}</component>
|
||||
<component name="ProjectId" id="2raEUZtlphkj04rfRNtlQw6fPMU" />
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
||||
<component name="ProjectLevelVcsManager">
|
||||
<OptionsSetting value="false" id="Update" />
|
||||
<ConfirmationsSetting value="2" id="Add" />
|
||||
</component>
|
||||
@@ -127,10 +82,12 @@
|
||||
".NET Project.HopFrame.Testing.executor": "Run",
|
||||
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
||||
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
||||
"git-widget-placeholder": "!33 on feature/exporters",
|
||||
"git-widget-placeholder": "dev",
|
||||
"list.type.of.created.stylesheet": "CSS",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
@@ -146,28 +103,30 @@
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="HopFrame.Testing: https" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<configuration name="HopFrame.Testing: https" type="LaunchSettings" factoryName=".NET Launch Settings Profile" activateToolWindowBeforeRun="false">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
@@ -176,13 +135,14 @@
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
@@ -191,13 +151,14 @@
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
@@ -258,134 +219,19 @@
|
||||
<workItem from="1740736919561" duration="191000" />
|
||||
<workItem from="1740738257628" duration="3216000" />
|
||||
<workItem from="1740741585276" duration="17000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Added basic configuration">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736850899254</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736850899254</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="Added admin page navigation">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736855209077</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736855209077</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00003" summary="Added database loading logic">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736859917232</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736859917232</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00004" summary="Started working on listing page">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736885531216</created>
|
||||
<option name="number" value="00004" />
|
||||
<option name="presentableId" value="LOCAL-00004" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736885531216</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00005" summary="Added entry saving support">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736970238802</created>
|
||||
<option name="number" value="00005" />
|
||||
<option name="presentableId" value="LOCAL-00005" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736970238802</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00006" summary="Added reload button and animation">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737023058093</created>
|
||||
<option name="number" value="00006" />
|
||||
<option name="presentableId" value="LOCAL-00006" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737023058093</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00007" summary="Added relation picker dialog">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737035288104</created>
|
||||
<option name="number" value="00007" />
|
||||
<option name="presentableId" value="LOCAL-00007" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737035288104</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00008" summary="Added automatic relation mapping">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737037853482</created>
|
||||
<option name="number" value="00008" />
|
||||
<option name="presentableId" value="LOCAL-00008" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737037853482</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00009" summary="Added property validation">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737040612038</created>
|
||||
<option name="number" value="00009" />
|
||||
<option name="presentableId" value="LOCAL-00009" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737040612038</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00010" summary="Added creation/modification confirmation">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737040946489</created>
|
||||
<option name="number" value="00010" />
|
||||
<option name="presentableId" value="LOCAL-00010" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737040946489</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00011" summary="Removed Template">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737042229086</created>
|
||||
<option name="number" value="00011" />
|
||||
<option name="presentableId" value="LOCAL-00011" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737042229086</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00012" summary="Added policy validation, ordering and virtual listing properties">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737055409534</created>
|
||||
<option name="number" value="00012" />
|
||||
<option name="presentableId" value="LOCAL-00012" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737055409535</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00013" summary="Added n -> m relation support">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737129518866</created>
|
||||
<option name="number" value="00013" />
|
||||
<option name="presentableId" value="LOCAL-00013" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737129518866</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00014" summary="Added text area support and DI support for modifier functions">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737202192471</created>
|
||||
<option name="number" value="00014" />
|
||||
<option name="presentableId" value="LOCAL-00014" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737202192471</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00015" summary="Addressed all build warnings">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737203441319</created>
|
||||
<option name="number" value="00015" />
|
||||
<option name="presentableId" value="LOCAL-00015" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737203441319</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00016" summary="Added documentation for the configurators and service extensions methods">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737208088933</created>
|
||||
<option name="number" value="00016" />
|
||||
<option name="presentableId" value="LOCAL-00016" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737208088933</updated>
|
||||
<workItem from="1740742098571" duration="78000" />
|
||||
<workItem from="1740742471317" duration="672000" />
|
||||
<workItem from="1741974241977" duration="10854000" />
|
||||
<workItem from="1742038098473" duration="990000" />
|
||||
<workItem from="1742059898156" duration="3488000" />
|
||||
<workItem from="1744725284649" duration="60000" />
|
||||
<workItem from="1744916016381" duration="66000" />
|
||||
<workItem from="1744916106166" duration="49000" />
|
||||
<workItem from="1744966207145" duration="5231000" />
|
||||
<workItem from="1751713720880" duration="8243000" />
|
||||
<workItem from="1751741813788" duration="4623000" />
|
||||
<workItem from="1764520233816" duration="726000" />
|
||||
<workItem from="1764520962745" duration="260000" />
|
||||
</task>
|
||||
<task id="LOCAL-00017" summary="Created tests for the core module">
|
||||
<option name="closed" value="true" />
|
||||
@@ -611,7 +457,175 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740741334420</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="45" />
|
||||
<task id="LOCAL-00045" summary="Patched CI">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740742170465</created>
|
||||
<option name="number" value="00045" />
|
||||
<option name="presentableId" value="LOCAL-00045" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740742170465</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00046" summary="Prepared CI for v3.2.0">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740742538991</created>
|
||||
<option name="number" value="00046" />
|
||||
<option name="presentableId" value="LOCAL-00046" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740742538991</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00047" summary="Removed unused dependency">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740742606152</created>
|
||||
<option name="number" value="00047" />
|
||||
<option name="presentableId" value="LOCAL-00047" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740742606152</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00048" summary="Fixed directory in pipeline">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740742749325</created>
|
||||
<option name="number" value="00048" />
|
||||
<option name="presentableId" value="LOCAL-00048" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740742749325</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00049" summary="Reverted pipeline to include all jobs">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740743139064</created>
|
||||
<option name="number" value="00049" />
|
||||
<option name="presentableId" value="LOCAL-00049" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740743139064</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00050" summary="Added support for custom repositories">
|
||||
<option name="closed" value="true" />
|
||||
<created>1741985203179</created>
|
||||
<option name="number" value="00050" />
|
||||
<option name="presentableId" value="LOCAL-00050" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1741985203179</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00051" summary="Added documentation for custom repos and exporter plugin">
|
||||
<option name="closed" value="true" />
|
||||
<created>1742038459077</created>
|
||||
<option name="number" value="00051" />
|
||||
<option name="presentableId" value="LOCAL-00051" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1742038459077</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00052" summary="Implemented sql search + negatable searches">
|
||||
<option name="closed" value="true" />
|
||||
<created>1742063374318</created>
|
||||
<option name="number" value="00052" />
|
||||
<option name="presentableId" value="LOCAL-00052" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1742063374318</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00053" summary="Started working on search suggestions">
|
||||
<option name="closed" value="true" />
|
||||
<created>1744971440348</created>
|
||||
<option name="number" value="00053" />
|
||||
<option name="presentableId" value="LOCAL-00053" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1744971440348</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00054" summary="Finished advanced search functionality">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751721064458</created>
|
||||
<option name="number" value="00054" />
|
||||
<option name="presentableId" value="LOCAL-00054" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751721064458</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00055" summary="Made search suggestions togglable">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751721576913</created>
|
||||
<option name="number" value="00055" />
|
||||
<option name="presentableId" value="LOCAL-00055" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751721576913</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00056" summary="Updated test pipeline">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751747279843</created>
|
||||
<option name="number" value="00056" />
|
||||
<option name="presentableId" value="LOCAL-00056" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751747279844</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00057" summary="Fixed typo in .gitlab-ci.yml">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751747347169</created>
|
||||
<option name="number" value="00057" />
|
||||
<option name="presentableId" value="LOCAL-00057" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751747347169</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00058" summary="updated test job">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751747836791</created>
|
||||
<option name="number" value="00058" />
|
||||
<option name="presentableId" value="LOCAL-00058" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751747836791</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00059" summary="Combined coverage reports in test job">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751748307061</created>
|
||||
<option name="number" value="00059" />
|
||||
<option name="presentableId" value="LOCAL-00059" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751748307061</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00060" summary="combined test results">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751748936783</created>
|
||||
<option name="number" value="00060" />
|
||||
<option name="presentableId" value="LOCAL-00060" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751748936783</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00061" summary="added coverage to test job">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751749638139</created>
|
||||
<option name="number" value="00061" />
|
||||
<option name="presentableId" value="LOCAL-00061" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751749638139</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00062" summary="Updated coverage extraction">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751750081208</created>
|
||||
<option name="number" value="00062" />
|
||||
<option name="presentableId" value="LOCAL-00062" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751750081208</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00063" summary="fixed coverage percentage printing">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751750341741</created>
|
||||
<option name="number" value="00063" />
|
||||
<option name="presentableId" value="LOCAL-00063" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751750341742</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00064" summary="fixed echo cmd">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751750495636</created>
|
||||
<option name="number" value="00064" />
|
||||
<option name="presentableId" value="LOCAL-00064" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751750495636</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00065" summary="Upgraded to dotnet 10">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764521217886</created>
|
||||
<option name="number" value="00065" />
|
||||
<option name="presentableId" value="LOCAL-00065" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764521217886</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="66" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -621,38 +635,17 @@
|
||||
<option name="coveragePercentColumnWidth" value="129" />
|
||||
<option name="sortOrder" value="DESCENDING" />
|
||||
<option name="sortedColumn" value="1" />
|
||||
<option name="symbolColumnWidth" value="451" />
|
||||
<option name="symbolColumnWidth" value="559" />
|
||||
<coverage-tree-state>
|
||||
<expand>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 0% 1657/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Total 53% 1150/2443" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 0% 1657/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 0% 1657/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 0% 1657/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 0% 1657/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Config 0% 228/228" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Total 53% 1150/2443" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 40% 862/1439" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
</expand>
|
||||
<select />
|
||||
@@ -660,33 +653,44 @@
|
||||
</component>
|
||||
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||
<MESSAGE value="Tested login functionality" />
|
||||
<MESSAGE value="prepared project for release" />
|
||||
<MESSAGE value="Included readme file in projects" />
|
||||
<MESSAGE value="Added missing files" />
|
||||
<MESSAGE value="Added a simple web api abstraction method" />
|
||||
<MESSAGE value="Implemented async delegates" />
|
||||
<MESSAGE value="Added maximum display length" />
|
||||
<MESSAGE value="Fixed test for table view" />
|
||||
<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" />
|
||||
<MESSAGE value="Patched CI" />
|
||||
<MESSAGE value="Prepared CI for v3.2.0" />
|
||||
<MESSAGE value="Removed unused dependency" />
|
||||
<MESSAGE value="Fixed directory in pipeline" />
|
||||
<MESSAGE value="Reverted pipeline to include all jobs" />
|
||||
<MESSAGE value="Added support for custom repositories" />
|
||||
<MESSAGE value="Added documentation for custom repos and exporter plugin" />
|
||||
<MESSAGE value="Implemented sql search + negatable searches" />
|
||||
<MESSAGE value="Started working on search suggestions" />
|
||||
<MESSAGE value="Finished advanced search functionality" />
|
||||
<MESSAGE value="Made search suggestions togglable" />
|
||||
<MESSAGE value="Updated test pipeline" />
|
||||
<MESSAGE value="Fixed typo in .gitlab-ci.yml" />
|
||||
<MESSAGE value="updated test job" />
|
||||
<MESSAGE value="Combined coverage reports in test job" />
|
||||
<MESSAGE value="combined test results" />
|
||||
<MESSAGE value="added coverage to test job" />
|
||||
<MESSAGE value="Updated coverage extraction" />
|
||||
<MESSAGE value="fixed coverage percentage printing" />
|
||||
<MESSAGE value="fixed echo cmd" />
|
||||
<MESSAGE value="Upgraded to dotnet 10" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Upgraded to dotnet 10" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -17,6 +17,7 @@
|
||||
<toc-element topic="PropertyConfig.md"/>
|
||||
</toc-element>
|
||||
<toc-element topic="Callbacks.md"/>
|
||||
<toc-element topic="Custom-Repositories.md"/>
|
||||
</toc-element>
|
||||
<toc-element toc-title="Web Module">
|
||||
<toc-element toc-title="Interface">
|
||||
@@ -27,6 +28,10 @@
|
||||
<toc-element topic="Plugins.md">
|
||||
<toc-element topic="Events.md">
|
||||
</toc-element>
|
||||
<toc-element topic="Exporter-Plugin.md"/>
|
||||
</toc-element>
|
||||
</toc-element>
|
||||
<toc-element toc-title="Services">
|
||||
<toc-element topic="IFileService.md"/>
|
||||
</toc-element>
|
||||
</instance-profile>
|
||||
132
docs/Writerside/topics/Custom-Repositories.md
Normal file
132
docs/Writerside/topics/Custom-Repositories.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Custom Repositories
|
||||
|
||||
Custom repositories in HopFrame allow you to define and integrate custom logic for managing database entities. By implementing the `IHopFrameRepository<TModel, TKey>` interface, you can gain full control over how data is retrieved, modified, and managed. This feature is ideal for scenarios where the default behavior does not meet specific business requirements.
|
||||
|
||||
## IHopFrameRepository<TModel, TKey> Interface
|
||||
|
||||
The `IHopFrameRepository<TModel, TKey>` interface defines a contract for a repository that works with a specific model (`TModel`) and its primary key (`TKey`). The interface provides the following methods:
|
||||
|
||||
- **LoadPage**
|
||||
Loads a paginated set of items.
|
||||
|
||||
```c#
|
||||
Task<IEnumerable<TModel>> LoadPage(int page, int perPage);
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `page`: The page number to load.
|
||||
- `perPage`: The number of items per page.
|
||||
- **Returns:** A collection of items for the specified page.
|
||||
|
||||
- **Search**
|
||||
Performs a search query on the repository.
|
||||
|
||||
```c#
|
||||
Task<SearchResult<TModel>> Search(string searchTerm, int page, int perPage);
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `searchTerm`: The term to search for.
|
||||
- `page`: The page number to load.
|
||||
- `perPage`: The number of items per page.
|
||||
- **Returns:** A `SearchResult` containing matching items and the total number of pages.
|
||||
|
||||
- **GetTotalPageCount**
|
||||
Retrieves the total number of pages based on the items per page.
|
||||
|
||||
```c#
|
||||
Task<int> GetTotalPageCount(int perPage);
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `perPage`: The number of items per page.
|
||||
- **Returns:** The total number of pages.
|
||||
|
||||
- **CreateItem**
|
||||
Adds a new item to the repository.
|
||||
|
||||
```c#
|
||||
Task CreateItem(TModel item);
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `item`: The item to create.
|
||||
|
||||
- **EditItem**
|
||||
Updates an existing item in the repository.
|
||||
|
||||
```c#
|
||||
Task EditItem(TModel item);
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `item`: The item to update.
|
||||
|
||||
- **DeleteItem**
|
||||
Removes an item from the repository.
|
||||
|
||||
```c#
|
||||
Task DeleteItem(TModel item);
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `item`: The item to delete.
|
||||
|
||||
- **GetOne**
|
||||
Retrieves a single item based on its primary key.
|
||||
|
||||
```c#
|
||||
Task<TModel?> GetOne(TKey key);
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `key`: The primary key of the item to retrieve.
|
||||
- **Returns:** The item if found, or `null` if not.
|
||||
|
||||
## `SearchResult<TModel>` Struct
|
||||
|
||||
The `SearchResult<TModel>` struct is used to encapsulate the results of a search query.
|
||||
|
||||
- **Properties:**
|
||||
- `Items`: The items retrieved from the search query.
|
||||
- `PageCount`: The total number of pages based on the search results.
|
||||
|
||||
```c#
|
||||
public readonly struct SearchResult<TModel>(IEnumerable<TModel> items, int pageCount) {
|
||||
public IEnumerable<TModel> Items { get; init; }
|
||||
public int PageCount { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
## Adding Custom Repositories
|
||||
|
||||
To add and configure a custom repository in HopFrame, use the `AddCustomRepository` methods. These methods allow you to specify a repository class (`TRepository`) implementing `IHopFrameRepository<TModel, TKey>` and define configurations for the associated table.
|
||||
|
||||
- **With Configurator**
|
||||
|
||||
```c#
|
||||
HopFrameConfigurator AddCustomRepository<TRepository, TModel, TKey>(
|
||||
Expression<Func<TModel, TKey>> keyExpression,
|
||||
Action<TableConfigurator<TModel>> configurator
|
||||
)
|
||||
where TRepository : IHopFrameRepository<TModel, TKey>;
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `keyExpression`: The key of the model.
|
||||
- `configurator`: Configures the table page.
|
||||
|
||||
- **Without Configurator**
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> AddCustomRepository<TRepository, TModel, TKey>(
|
||||
Expression<Func<TModel, TKey>> keyExpression
|
||||
)
|
||||
where TRepository : IHopFrameRepository<TModel, TKey>;
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `keyExpression`: The key of the model.
|
||||
- **Returns:** A `TableConfigurator` to configure the table.
|
||||
|
||||
By implementing custom repositories and using these methods, you can fully leverage the flexibility of HopFrame for your data management needs. Let me know if you'd like further elaboration!
|
||||
51
docs/Writerside/topics/Exporter-Plugin.md
Normal file
51
docs/Writerside/topics/Exporter-Plugin.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Exporter Plugin
|
||||
|
||||
The Exporter Plugin is a tool for managing the import and export of data from the HopFrame UI. It provides functionality for exporting table data into a CSV file and importing data back into the system, making data manipulation and backups more seamless.
|
||||
|
||||
## What the Exporter Plugin Does
|
||||
|
||||
1. **Export Table Data to CSV**
|
||||
- The plugin allows users to export all data from a table as a CSV file.
|
||||
- The exported file includes all non-virtual properties as table headers.
|
||||
- The export process dynamically constructs rows for each entry in the table.
|
||||
|
||||
2. **Import Data from CSV**
|
||||
- Users can import a CSV file to populate or update a table.
|
||||
- The import process reads the file, validates the headers, and creates new entries or updates existing ones.
|
||||
- Relationships and enumerable properties are also resolved using the appropriate managers.
|
||||
|
||||
3. **User Interface Integration**
|
||||
- Adds two buttons, "Export" and "Import," to the page header of each table.
|
||||
- **Export Button:** Initiates the export functionality.
|
||||
- **Import Button:** Allows users to upload a CSV file for import.
|
||||
|
||||
4. **Error Handling**
|
||||
- Ensures errors during import or export (e.g., invalid file format, missing data, or system issues) are shown to the user as toast messages.
|
||||
|
||||
## Adding the Exporter Plugin
|
||||
|
||||
To include the Exporter Plugin in your HopFrame setup, use the `AddExporters` method provided by the `HopFrameConfiguratorExtensions`.
|
||||
|
||||
Here’s how to register the Exporter Plugin in your application configuration:
|
||||
|
||||
```c#
|
||||
builder.Services.AddHopFrame(options => {
|
||||
options.AddExporters();
|
||||
});
|
||||
```
|
||||
|
||||
The `AddExporters` method internally registers the `ExporterPlugin` and attaches its functionality to the HopFrame.
|
||||
|
||||
## Key Features of the Export Process
|
||||
|
||||
- **Dynamic Header Creation:** Automatically generates headers based on the table's non-virtual properties.
|
||||
- **Data Transformation:** Transforms property values into CSV-compatible formats.
|
||||
- **File Download:** Saves the generated CSV file with the table’s display name.
|
||||
|
||||
## Key Features of the Import Process
|
||||
|
||||
- **Header Validation:** Validates that the CSV file headers match the table's properties.
|
||||
- **Type Conversion:** Converts values in the CSV file to their respective data types.
|
||||
- **Relationship Management:** Resolves relationships and enumerable properties during import.
|
||||
|
||||
This plugin streamlines data operations, reducing manual effort and enabling quick data migration or updates. Let me know if you’d like to dive deeper into any specific aspect!
|
||||
@@ -118,6 +118,50 @@ DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext :
|
||||
|
||||
- **Returns:** The configurator of the context if it already was defined, `null` if not.
|
||||
|
||||
### AddCustomRepository (With configurator)
|
||||
|
||||
Adds a table of the desired type and configures it to use a custom repository.
|
||||
|
||||
```c#
|
||||
HopFrameConfigurator AddCustomRepository<TRepository, TModel, TKey>(
|
||||
Expression<Func<TModel, TKey>> keyExpression,
|
||||
Action<TableConfigurator<TModel>> configurator
|
||||
)
|
||||
where TRepository : IHopFrameRepository<TModel, TKey>
|
||||
```
|
||||
|
||||
- **Type Parameters:**
|
||||
- `TRepository`: The repository class that inherits from `IHopFrameRepository<TModel, TKey>` (needs to be registered as a service).
|
||||
- `TModel`: The model of the table.
|
||||
- `TKey`: The type of the primary key.
|
||||
|
||||
- **Parameters:**
|
||||
- `keyExpression`: The key of the model.
|
||||
- `configurator`: The configurator used for configuring the table page.
|
||||
|
||||
- **Returns:** `HopFrameConfigurator`
|
||||
|
||||
### AddCustomRepository (Without configurator)
|
||||
|
||||
Adds a table of the desired type and configures it to use a custom repository.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> AddCustomRepository<TRepository, TModel, TKey>(
|
||||
Expression<Func<TModel, TKey>> keyExpression
|
||||
)
|
||||
where TRepository : IHopFrameRepository<TModel, TKey>
|
||||
```
|
||||
|
||||
- **Type Parameters:**
|
||||
- `TRepository`: The repository class that inherits from `IHopFrameRepository<TModel, TKey>` (needs to be registered as a service).
|
||||
- `TModel`: The model of the table.
|
||||
- `TKey`: The type of the primary key.
|
||||
|
||||
- **Parameters:**
|
||||
- `keyExpression`: The key of the model.
|
||||
|
||||
- **Returns:** The configurator used for configuring the table page: `TableConfigurator<TModel>`.
|
||||
|
||||
### DisplayUserInfo
|
||||
|
||||
Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin UI.
|
||||
|
||||
41
docs/Writerside/topics/IFileService.md
Normal file
41
docs/Writerside/topics/IFileService.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# IFileService
|
||||
|
||||
The `IFileService` interface provides methods for handling file operations, such as downloading and uploading files within the HopFrame web application. It abstracts file-related operations to ensure a smooth and consistent user experience.
|
||||
|
||||
## Methods
|
||||
|
||||
1. **DownloadFile**
|
||||
- Initiates the download of a file with the given name and data.
|
||||
- Suitable for dynamically generating and offering files to the user, such as CSV exports or reports.
|
||||
|
||||
```c#
|
||||
Task DownloadFile(string name, byte[] data);
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `name`: The name of the file to be downloaded (including the extension, e.g., "example.csv").
|
||||
- `data`: The byte array representing the content of the file.
|
||||
- **Usage Example:** Exporting table data as a CSV file for download.
|
||||
|
||||
2. **UploadFile**
|
||||
- Allows the user to upload a file through the web interface and returns the uploaded file for further processing.
|
||||
- This method provides integration with Blazor's `IBrowserFile` for easy file handling.
|
||||
|
||||
```c#
|
||||
Task<IBrowserFile> UploadFile();
|
||||
```
|
||||
|
||||
- **Returns:** An `IBrowserFile` instance representing the uploaded file.
|
||||
- **Usage Example:** Importing data from a CSV file to populate or update a table.
|
||||
|
||||
## Integration
|
||||
|
||||
The `IFileService` is commonly used in conjunction with plugins or components that require file operations, such as the Exporter Plugin, which leverages this service to enable data export and import functionality.
|
||||
|
||||
## Key Features
|
||||
|
||||
- Streamlines file handling for web applications.
|
||||
- Simplifies both download and upload processes with minimal code.
|
||||
- Ensures compatibility with Blazor's file-handling capabilities.
|
||||
|
||||
By implementing or extending the `IFileService`, developers can customize the file-handling behavior to suit specific application needs. Let me know if you'd like more examples or details!
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
public class DbContextConfig {
|
||||
public interface ITableGroupConfig {
|
||||
public Type ContextType { get; }
|
||||
public List<TableConfig> Tables { get; }
|
||||
public HopFrameConfig ParentConfig { get; }
|
||||
}
|
||||
|
||||
public class DbContextConfig : ITableGroupConfig {
|
||||
public Type ContextType { get; }
|
||||
public List<TableConfig> Tables { get; } = new();
|
||||
public HopFrameConfig ParentConfig { get; }
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using HopFrame.Core.Callbacks;
|
||||
using System.Linq.Expressions;
|
||||
using HopFrame.Core.Callbacks;
|
||||
using HopFrame.Core.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
public class HopFrameConfig {
|
||||
public List<DbContextConfig> Contexts { get; } = new();
|
||||
public List<ITableGroupConfig> Contexts { get; } = new();
|
||||
public bool DisplayUserInfo { get; set; } = true;
|
||||
public string? BasePolicy { get; set; }
|
||||
public string? LoginPageRewrite { get; set; }
|
||||
@@ -48,6 +50,36 @@ public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollecti
|
||||
return new DbContextConfigurator<TDbContext>(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a table of the desired type and configures it to use a custom repository
|
||||
/// </summary>
|
||||
/// <param name="keyExpression">The key of the model</param>
|
||||
/// <param name="configurator">The configurator used for configuring the table page</param>
|
||||
/// <typeparam name="TRepository">The repository class that inherits from the <see cref="IHopFrameRepository{TModel,TKey}"/> (needs to be registered as a service)</typeparam>
|
||||
/// <typeparam name="TModel">The model of the table</typeparam>
|
||||
/// <typeparam name="TKey">The type of the primary key</typeparam>
|
||||
public HopFrameConfigurator AddCustomRepository<TRepository, TModel, TKey>(Expression<Func<TModel, TKey>> keyExpression, Action<TableConfigurator<TModel>> configurator) {
|
||||
var context = AddCustomRepository<TRepository, TModel, TKey>(keyExpression);
|
||||
configurator.Invoke(context);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a table of the desired type and configures it to use a custom repository
|
||||
/// </summary>
|
||||
/// <param name="keyExpression">The key of the model</param>
|
||||
/// <typeparam name="TRepository">The repository class that inherits from the <see cref="IHopFrameRepository{TModel,TKey}"/> (needs to be registered as a service)</typeparam>
|
||||
/// <typeparam name="TModel">The model of the table</typeparam>
|
||||
/// <typeparam name="TKey">The type of the primary key</typeparam>
|
||||
/// <returns>The configurator used for configuring the table page</returns>
|
||||
public TableConfigurator<TModel> AddCustomRepository<TRepository, TModel, TKey>(Expression<Func<TModel, TKey>> keyExpression) {
|
||||
var keyProperty = TableConfigurator<TModel>.GetPropertyInfo(keyExpression);
|
||||
var context = new RepositoryGroupConfig(typeof(TRepository), keyProperty, InnerConfig);
|
||||
context.Tables.Add(new TableConfig(context, typeof(TModel), typeof(TRepository).Name, 0));
|
||||
InnerConfig.Contexts.Add(context);
|
||||
return new TableConfigurator<TModel>(context.Tables[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a context is already registered in the HopFrame
|
||||
/// </summary>
|
||||
@@ -64,6 +96,7 @@ public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollecti
|
||||
/// <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
|
||||
.OfType<DbContextConfig>()
|
||||
.SingleOrDefault(context => context.ContextType == typeof(TDbContext));
|
||||
if (config is null) return null;
|
||||
|
||||
|
||||
13
src/HopFrame.Core/Config/RepositoryGroupConfig.cs
Normal file
13
src/HopFrame.Core/Config/RepositoryGroupConfig.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
public class RepositoryGroupConfig(Type repoType, PropertyInfo keyProperty, HopFrameConfig config) : ITableGroupConfig {
|
||||
public Type ContextType { get; } = repoType;
|
||||
|
||||
public List<TableConfig> Tables { get; } = new();
|
||||
|
||||
public HopFrameConfig ParentConfig { get; } = config;
|
||||
|
||||
public PropertyInfo KeyProperty { get; } = keyProperty;
|
||||
}
|
||||
@@ -11,10 +11,11 @@ public class TableConfig {
|
||||
public string PropertyName { get; }
|
||||
public string DisplayName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DbContextConfig ContextConfig { get; }
|
||||
public ITableGroupConfig ContextConfig { get; }
|
||||
public bool Ignored { get; set; }
|
||||
public int Order { get; set; }
|
||||
internal bool Seeded { get; set; }
|
||||
public bool ShowSearchSuggestions { get; set; } = true;
|
||||
|
||||
public string? ViewPolicy { get; set; }
|
||||
public string? CreatePolicy { get; set; }
|
||||
@@ -23,7 +24,7 @@ public class TableConfig {
|
||||
|
||||
public List<PropertyConfig> Properties { get; } = new();
|
||||
|
||||
public TableConfig(DbContextConfig config, Type tableType, string propertyName, int nthTable) {
|
||||
public TableConfig(ITableGroupConfig config, Type tableType, string propertyName, int nthTable) {
|
||||
TableType = tableType;
|
||||
PropertyName = propertyName;
|
||||
ContextConfig = config;
|
||||
@@ -66,6 +67,13 @@ public sealed class TableConfigurator<TModel>(TableConfig config) {
|
||||
InnerConfig.Ignored = ignore;
|
||||
return this;
|
||||
}
|
||||
/// <summary>
|
||||
/// Determines if search suggestions should be displayed in the ui (Advanced Search)
|
||||
/// </summary>
|
||||
public TableConfigurator<TModel> ShowSearchSuggestions(bool show = true) {
|
||||
InnerConfig.ShowSearchSuggestions = show;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the property of the table
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
24
src/HopFrame.Core/Repositories/IHopFrameRepository.cs
Normal file
24
src/HopFrame.Core/Repositories/IHopFrameRepository.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace HopFrame.Core.Repositories;
|
||||
|
||||
public interface IHopFrameRepository<TModel, in TKey> where TModel : class {
|
||||
|
||||
Task<IEnumerable<TModel>> LoadPage(int page, int perPage);
|
||||
|
||||
Task<SearchResult<TModel>> Search(string searchTerm, int page, int perPage);
|
||||
|
||||
Task<int> GetTotalPageCount(int perPage);
|
||||
|
||||
Task CreateItem(TModel item);
|
||||
|
||||
Task EditItem(TModel item);
|
||||
|
||||
Task DeleteItem(TModel item);
|
||||
|
||||
Task<TModel?> GetOne(TKey key);
|
||||
|
||||
}
|
||||
|
||||
public readonly struct SearchResult<TModel>(IEnumerable<TModel> items, int pageCount) {
|
||||
public IEnumerable<TModel> Items { get; init; } = items;
|
||||
public int PageCount { get; init; } = pageCount;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public static class ServiceCollectionExtensions {
|
||||
services.AddScoped<IContextExplorer, ContextExplorer>();
|
||||
services.TryAddScoped<IHopFrameAuthHandler, DefaultAuthHandler>();
|
||||
services.TryAddScoped<ICallbackEmitter, CallbackEmitter>();
|
||||
services.AddScoped<ISearchExpressionBuilder, SearchExpressionBuilder>();
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
9
src/HopFrame.Core/Services/ISearchExpressionBuilder.cs
Normal file
9
src/HopFrame.Core/Services/ISearchExpressionBuilder.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using HopFrame.Core.Config;
|
||||
|
||||
namespace HopFrame.Core.Services;
|
||||
|
||||
public interface ISearchExpressionBuilder {
|
||||
Expression? BuildSearchExpression(TableConfig table, string searchTerm, ParameterExpression parameter);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using HopFrame.Core.Config;
|
||||
namespace HopFrame.Core.Services;
|
||||
|
||||
public interface ITableManager {
|
||||
public IQueryable<object> LoadPage(int page, int perPage = 20);
|
||||
public Task<IEnumerable<object>> LoadPage(int page, int perPage = 20);
|
||||
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20);
|
||||
public Task<int> TotalPages(int perPage = 20);
|
||||
public Task DeleteItem(object item);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Config;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -45,11 +44,18 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tablePropertyName);
|
||||
if (table is null) continue;
|
||||
|
||||
var dbContext = provider.GetService(context.ContextType) as DbContext;
|
||||
if (dbContext is null) return null;
|
||||
var repo = provider.GetService(context.ContextType);
|
||||
if (repo is null) return null;
|
||||
|
||||
if (context is DbContextConfig) {
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
return Activator.CreateInstance(type, dbContext, table, this, provider) as ITableManager;
|
||||
return Activator.CreateInstance(type, (DbContext)repo, table, this, provider, provider.GetRequiredService<ISearchExpressionBuilder>()) as ITableManager;
|
||||
}
|
||||
|
||||
if (context is RepositoryGroupConfig repoConfig) {
|
||||
var type = typeof(RepositoryTableManager<,>).MakeGenericType(table.TableType, repoConfig.KeyProperty.PropertyType);
|
||||
return Activator.CreateInstance(type, repo, this, provider, provider.GetRequiredService<ISearchExpressionBuilder>()) as ITableManager;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -60,11 +66,18 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
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 repo = provider.GetService(context.ContextType);
|
||||
if (repo is null) return null;
|
||||
|
||||
if (context is DbContextConfig) {
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
return Activator.CreateInstance(type, dbContext, table, this, provider) as ITableManager;
|
||||
return Activator.CreateInstance(type, (DbContext)repo, table, this, provider, provider.GetRequiredService<ISearchExpressionBuilder>()) as ITableManager;
|
||||
}
|
||||
|
||||
if (context is RepositoryGroupConfig repoConfig) {
|
||||
var type = typeof(RepositoryTableManager<,>).MakeGenericType(table.TableType, repoConfig.KeyProperty.PropertyType);
|
||||
return Activator.CreateInstance(type, repo, this, provider, provider.GetRequiredService<ISearchExpressionBuilder>()) as ITableManager;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -72,6 +85,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
|
||||
private void SeedTableData(TableConfig table) {
|
||||
if (table.Seeded) return;
|
||||
if (table.ContextConfig is not DbContextConfig) return;
|
||||
var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!;
|
||||
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Repositories;
|
||||
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
public class RepositoryTableManager<TModel, TKey>(IHopFrameRepository<TModel, TKey> repo, IContextExplorer explorer, IServiceProvider provider, ISearchExpressionBuilder searchExpressionBuilder) : ITableManager where TModel : class {
|
||||
public async Task<IEnumerable<object>> LoadPage(int page, int perPage = 20) {
|
||||
return await repo.LoadPage(page, perPage);
|
||||
}
|
||||
public async Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||
var result = await repo.Search(searchTerm, page, perPage);
|
||||
return (result.Items, result.PageCount);
|
||||
}
|
||||
public Task<int> TotalPages(int perPage = 20) {
|
||||
return repo.GetTotalPageCount(perPage);
|
||||
}
|
||||
public Task DeleteItem(object item) {
|
||||
return repo.DeleteItem((TModel)item);
|
||||
}
|
||||
public Task EditItem(object item) {
|
||||
return repo.EditItem((TModel)item);
|
||||
}
|
||||
public Task AddItem(object item) {
|
||||
return repo.CreateItem((TModel)item);
|
||||
}
|
||||
public Task AddAll(IEnumerable<object> items) {
|
||||
var tasks = items
|
||||
.Select(item => repo.CreateItem((TModel)item))
|
||||
.ToList();
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
public async Task<object?> GetOne(object key) {
|
||||
return await repo.GetOne((TKey)key);
|
||||
}
|
||||
public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) {
|
||||
var manager = new TableManager<TModel>(null!, null!, explorer, provider, searchExpressionBuilder);
|
||||
return await manager.DisplayProperty(item, prop, value, enumerableValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using HopFrame.Core.Config;
|
||||
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
internal sealed class SearchExpressionBuilder(IContextExplorer explorer) : ISearchExpressionBuilder {
|
||||
private readonly struct SearchPart {
|
||||
public string? Property { get; init; }
|
||||
public string Term { get; init; }
|
||||
public bool Negated { get; init; }
|
||||
}
|
||||
|
||||
private Expression AddPropertySearchExpression(PropertyInfo property, ParameterExpression parameter, string searchTerm, PropertyConfig config) {
|
||||
Expression propertyAccess = Expression.Property(parameter, property);
|
||||
|
||||
if (config.IsEnumerable) { //Call Count() extension method before checking the search term
|
||||
propertyAccess = Expression.Property(propertyAccess, config.Info.PropertyType.GetProperty(nameof(List<object>.Count))!);
|
||||
}
|
||||
|
||||
var toStringCall = Expression.Call(propertyAccess, nameof(ToString), Type.EmptyTypes);
|
||||
var searchExpression = Expression.Call(
|
||||
toStringCall,
|
||||
typeof(string).GetMethod(config.IsEnumerable ? nameof(string.Equals) : nameof(string.Contains), [typeof(string)])!,
|
||||
Expression.Constant(searchTerm));
|
||||
|
||||
return searchExpression;
|
||||
}
|
||||
|
||||
private Expression AddForeignPropertySearchExpression(PropertyInfo navigationProperty, PropertyInfo displayedProperty, ParameterExpression parameter, string searchTerm) {
|
||||
var navigationAccess = Expression.Property(parameter, navigationProperty);
|
||||
var nullCheck = Expression.NotEqual(navigationAccess, Expression.Constant(null));
|
||||
var displayedPropertyAccess = Expression.Property(navigationAccess, displayedProperty);
|
||||
|
||||
var toStringCall = Expression.Call(displayedPropertyAccess, nameof(ToString), Type.EmptyTypes);
|
||||
var searchExpression = Expression.Call(
|
||||
toStringCall,
|
||||
typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) })!,
|
||||
Expression.Constant(searchTerm));
|
||||
|
||||
return Expression.AndAlso(nullCheck, searchExpression);
|
||||
}
|
||||
|
||||
private IEnumerable<PropertyInfo> GetSuitableProperties(TableConfig table) {
|
||||
Type[] validTypes = [typeof(string), typeof(Guid), typeof(DateTime), typeof(DateOnly), typeof(TimeOnly)];
|
||||
|
||||
return table.Properties
|
||||
.Where(prop => !prop.IsVirtualProperty)
|
||||
.Where(prop => prop.List)
|
||||
.Where(prop => prop.Searchable)
|
||||
.Where(prop => prop.Info.PropertyType.IsEnum || validTypes.Contains(prop.Info.PropertyType) || prop.IsEnumerable)
|
||||
.Select(prop => prop.Info);
|
||||
}
|
||||
|
||||
private IEnumerable<(PropertyInfo navigation, PropertyInfo display)> GetSuitableForeignProperties(TableConfig table) {
|
||||
return table.Properties
|
||||
.Where(prop => prop.List)
|
||||
.Where(prop => prop.IsRelation)
|
||||
.Where(prop => prop.Searchable)
|
||||
.Where(prop => prop.DisplayedProperty != null)
|
||||
.Select(prop => (prop.Info, explorer
|
||||
.GetTable(prop.Info.PropertyType)!.Properties
|
||||
.Find(p => p.Info.Name == prop.DisplayedProperty!.Name)!
|
||||
.Info));
|
||||
}
|
||||
|
||||
private IEnumerable<SearchPart> ExtractSearchParts(string searchTerm) {
|
||||
var rawParts = searchTerm.Split(' ');
|
||||
var parts = new List<SearchPart>();
|
||||
|
||||
foreach (var part in rawParts) {
|
||||
if (string.IsNullOrWhiteSpace(part))
|
||||
continue;
|
||||
|
||||
if (!part.Contains('=')) {
|
||||
var negated = part.StartsWith('!');
|
||||
|
||||
parts.Add(new() {
|
||||
Term = negated ? part[1..] : part,
|
||||
Negated = negated,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var split = part.Split('=');
|
||||
var term = string.Join('=', split[1..]);
|
||||
var termNegated = term.StartsWith('!');
|
||||
|
||||
parts.Add(new() {
|
||||
Property = split[0],
|
||||
Term = termNegated ? term[1..] : term,
|
||||
Negated = termNegated
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
public Expression? BuildSearchExpression(TableConfig table, string searchTerm, ParameterExpression parameter) {
|
||||
var properties = GetSuitableProperties(table).ToArray();
|
||||
var foreignProperties = GetSuitableForeignProperties(table).ToArray();
|
||||
|
||||
var parts = ExtractSearchParts(searchTerm);
|
||||
|
||||
Expression? expression = null;
|
||||
foreach (var part in parts) {
|
||||
Expression? subExp = null;
|
||||
|
||||
if (part.Property is null) {
|
||||
foreach (var property in properties) {
|
||||
var exp = AddPropertySearchExpression(property, parameter, part.Term, table.Properties.First(p => p.Info == property));
|
||||
subExp = subExp is null
|
||||
? exp
|
||||
: Expression.OrElse(subExp, exp);
|
||||
}
|
||||
|
||||
foreach (var property in foreignProperties) {
|
||||
var exp = AddForeignPropertySearchExpression(property.navigation, property.display, parameter, part.Term);
|
||||
subExp = subExp is null
|
||||
? exp
|
||||
: Expression.OrElse(subExp, exp);
|
||||
}
|
||||
|
||||
if (subExp is null)
|
||||
continue;
|
||||
}
|
||||
|
||||
var prop = properties.FirstOrDefault(p => p.Name == part.Property);
|
||||
if (prop is not null) {
|
||||
subExp = AddPropertySearchExpression(prop, parameter, part.Term, table.Properties.First(p => p.Info == prop));
|
||||
}
|
||||
|
||||
var forProp = foreignProperties.FirstOrDefault(p => p.navigation.Name == part.Property);
|
||||
if (forProp.navigation is not null) {
|
||||
subExp = AddForeignPropertySearchExpression(forProp.navigation, forProp.display, parameter, part.Term);
|
||||
}
|
||||
|
||||
if (subExp is null)
|
||||
continue;
|
||||
|
||||
if (part.Negated)
|
||||
subExp = Expression.Not(subExp);
|
||||
|
||||
expression = expression is null
|
||||
? subExp
|
||||
: Expression.AndAlso(expression, subExp);
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,46 @@
|
||||
using System.Collections;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using HopFrame.Core.Config;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
internal sealed class TableManager<TModel>(DbContext context, TableConfig config, IContextExplorer explorer, IServiceProvider provider) : ITableManager where TModel : class {
|
||||
internal sealed class TableManager<TModel>(DbContext context, TableConfig config, IContextExplorer explorer, IServiceProvider provider, ISearchExpressionBuilder searchExpressionBuilder) : ITableManager where TModel : class {
|
||||
|
||||
public IQueryable<object> LoadPage(int page, int perPage = 20) {
|
||||
public async Task<IEnumerable<object>> LoadPage(int page, int perPage = 20) {
|
||||
var table = context.Set<TModel>();
|
||||
var data = IncludeForeignKeys(table);
|
||||
return data
|
||||
return await data
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage);
|
||||
.Take(perPage)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
|
||||
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||
public async Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||
var table = context.Set<TModel>();
|
||||
var all = IncludeForeignKeys(table)
|
||||
.AsEnumerable()
|
||||
.Where(item => ItemSearched(item, searchTerm))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult((
|
||||
(IEnumerable<object>)all.Skip(page * perPage).Take(perPage),
|
||||
(int)Math.Ceiling(all.Count / (double)perPage)));
|
||||
var parameter = Expression.Parameter(typeof(TModel), "x");
|
||||
var exp = searchExpressionBuilder.BuildSearchExpression(config, searchTerm, parameter);
|
||||
|
||||
if (exp is null)
|
||||
return ([], 0);
|
||||
|
||||
var lambda = Expression.Lambda<Func<TModel, bool>>(exp, parameter);
|
||||
var result = await IncludeForeignKeys(table)
|
||||
.Where(lambda)
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage)
|
||||
.ToListAsync();
|
||||
|
||||
var totalEntries = await table
|
||||
.Where(lambda)
|
||||
.CountAsync();
|
||||
|
||||
return (result, (int)Math.Ceiling(totalEntries / (double)perPage));
|
||||
}
|
||||
|
||||
public async Task<int> TotalPages(int perPage = 20) {
|
||||
@@ -60,31 +75,6 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
||||
return await table.FindAsync(key);
|
||||
}
|
||||
|
||||
public async Task RevertChanges(object item) {
|
||||
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) {
|
||||
foreach (var property in config.Properties) {
|
||||
if (!property.Searchable) continue;
|
||||
var value = property.GetValue(item, provider);
|
||||
if (value is null) continue;
|
||||
|
||||
var strValue = value.ToString();
|
||||
if (strValue?.Contains(searchTerm) == true)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) {
|
||||
if (item is null) return string.Empty;
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@
|
||||
|
||||
var relationType = config.Info.PropertyType;
|
||||
if (config.IsEnumerable) {
|
||||
relationType = config.Info.PropertyType.GetGenericArguments().First();
|
||||
relationType = relationType.GetGenericArguments().First();
|
||||
}
|
||||
|
||||
var relationTable = Explorer.GetTable(relationType);
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
@using HopFrame.Web.Models
|
||||
@using HopFrame.Web.Plugins
|
||||
@using HopFrame.Web.Plugins.Events
|
||||
@using HopFrame.Web.Services
|
||||
@using Microsoft.JSInterop
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@if (!DisplaySelection) {
|
||||
<PageTitle>@_config?.DisplayName</PageTitle>
|
||||
@@ -40,7 +40,27 @@
|
||||
}
|
||||
|
||||
<FluentSpacer />
|
||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
||||
<div
|
||||
style="position: relative; height: 32px"
|
||||
class="hopframe-search">
|
||||
|
||||
<FluentSearch
|
||||
@ref="_searchBox"
|
||||
@oninput="OnSearch"
|
||||
@onchange="OnSearch"
|
||||
@onfocusin="() => { SearchFocus(); UpdateSearchSuggestions(); }"
|
||||
@onfocusout="SearchUnfocus"
|
||||
Style="width: 500px"/>
|
||||
|
||||
@if (_isSearchActive && _searchSuggestions.Count > 0) {
|
||||
<FluentListbox
|
||||
TOption="string"
|
||||
Items="_searchSuggestions"
|
||||
SelectedOptionChanged="SearchSuggestionSelected"
|
||||
@onfocusin="SearchFocus"
|
||||
@onfocusout="SearchUnfocus"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (_hasCreatePolicy && DisplayActions && _buttonToggles.ShowAddEntityButton) {
|
||||
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entity</FluentButton>
|
||||
@@ -162,6 +182,7 @@
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
@inject ICallbackEmitter Emitter
|
||||
@inject IPluginOrchestrator PluginOrchestrator
|
||||
@inject ISearchSuggestionProvider SearchSuggestions
|
||||
|
||||
@code {
|
||||
|
||||
@@ -191,6 +212,9 @@
|
||||
private int _totalPages;
|
||||
private string? _searchTerm;
|
||||
private bool _loading;
|
||||
private bool _isSearchActive;
|
||||
private IList<string> _searchSuggestions = [];
|
||||
private FluentSearch? _searchBox;
|
||||
|
||||
private bool _hasUpdatePolicy;
|
||||
private bool _hasDeletePolicy;
|
||||
@@ -231,7 +255,7 @@
|
||||
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
||||
|
||||
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
||||
CurrentlyDisplayedModels = await _manager!.LoadPage(_currentPage, PerPage).ToArrayAsync();
|
||||
CurrentlyDisplayedModels = (await _manager!.LoadPage(_currentPage, PerPage)).ToArray();
|
||||
_totalPages = await _manager.TotalPages(PerPage);
|
||||
}
|
||||
|
||||
@@ -255,6 +279,7 @@
|
||||
_searchTerm = eventArgs.Value?.ToString();
|
||||
if (_searchTerm is null) return;
|
||||
_searchCancel = new();
|
||||
UpdateSearchSuggestions();
|
||||
|
||||
await Task.Delay(500, _searchCancel.Token);
|
||||
|
||||
@@ -275,6 +300,36 @@
|
||||
await Reload();
|
||||
}
|
||||
|
||||
private async Task SearchSuggestionSelected(string? suggestion) {
|
||||
if (string.IsNullOrWhiteSpace(suggestion)) return;
|
||||
_searchTerm = SearchSuggestions.CompleteSearchSuggestion(_config!, _searchTerm ?? string.Empty, suggestion);
|
||||
_searchBox!.Value = _searchTerm;
|
||||
_searchBox.FocusAsync();
|
||||
UpdateSearchSuggestions();
|
||||
|
||||
if (!suggestion.EndsWith('='))
|
||||
await OnSearch(new() {
|
||||
Value = _searchTerm
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateSearchSuggestions() {
|
||||
if (_config is null || !_config.ShowSearchSuggestions) return;
|
||||
_searchSuggestions = SearchSuggestions.GenerateSearchSuggestions(_config, _searchTerm ?? string.Empty).ToList();
|
||||
}
|
||||
|
||||
private CancellationTokenSource _searchFocusCancel = new();
|
||||
private async Task SearchFocus() {
|
||||
_isSearchActive = true;
|
||||
await _searchFocusCancel.CancelAsync();
|
||||
_searchFocusCancel = new();
|
||||
}
|
||||
|
||||
private async Task SearchUnfocus() {
|
||||
await Task.Delay(10, _searchFocusCancel.Token);
|
||||
_isSearchActive = false;
|
||||
}
|
||||
|
||||
public async Task Reload() {
|
||||
_loading = true;
|
||||
|
||||
@@ -421,4 +476,5 @@
|
||||
public void RequestRender() {
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,3 +20,13 @@
|
||||
place-items: center;
|
||||
border-bottom: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest);
|
||||
}
|
||||
|
||||
.hopframe-search ::deep fluent-listbox {
|
||||
width: 500px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: var(--fill-color);
|
||||
z-index: 1;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -27,9 +27,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.11.2" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.11.2" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.13.2" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.13.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -56,6 +56,10 @@ public static class HopFrameConfiguratorExtensions {
|
||||
return configurator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the Exporter Plugin for data import/export functionality.
|
||||
/// </summary>
|
||||
/// <param name="configurator">The configurator for the HopFrame configuration.</param>
|
||||
public static HopFrameConfigurator AddExporters(this HopFrameConfigurator configurator) {
|
||||
configurator.AddPlugin<ExporterPlugin>();
|
||||
return configurator;
|
||||
|
||||
@@ -17,6 +17,8 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
|
||||
[EventHandler]
|
||||
public void OnInit(TableInitializedEvent e) {
|
||||
if (e.Sender.DialogData is not null) return;
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -29,8 +31,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
}
|
||||
|
||||
var data = await manager
|
||||
.LoadPage(0, int.MaxValue)
|
||||
.ToArrayAsync();
|
||||
.LoadPage(0, int.MaxValue);
|
||||
|
||||
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).ToArray();
|
||||
|
||||
@@ -81,6 +82,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
if (property is null) continue;
|
||||
|
||||
object? value = rowValues[i];
|
||||
if (string.IsNullOrWhiteSpace((string)value)) continue;
|
||||
|
||||
if (property.IsEnumerable) {
|
||||
if (!property.Info.PropertyType.IsGenericType) continue;
|
||||
@@ -102,7 +104,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
|
||||
var enumerable = Activator.CreateInstance(property.Info.PropertyType);
|
||||
foreach (var key in values) {
|
||||
var entry = await relationManager.GetOne(ParseString(key, primaryKeyType));
|
||||
var entry = await relationManager.GetOne(ParseString(key, primaryKeyType)!);
|
||||
if (entry is null) continue;
|
||||
|
||||
addMethod.Invoke(enumerable, [entry]);
|
||||
@@ -116,7 +118,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
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));
|
||||
value = await relationManager.GetOne(ParseString((string)value, relationPrimaryKeyType)!);
|
||||
}
|
||||
else if (property.Info.PropertyType == typeof(Guid)) {
|
||||
var success = Guid.TryParse((string)value, out var guid);
|
||||
@@ -151,13 +153,20 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
|
||||
if (property.IsEnumerable) {
|
||||
var enumerable = (IEnumerable)value;
|
||||
return '[' + string.Join(',', enumerable.OfType<object>().Select(o => SelectPrimaryKey(o) ?? o.ToString())) + ']';
|
||||
return '[' + string.Join(',', enumerable.OfType<object>().Select(o => SelectPrimaryKey(o, property) ?? o.ToString())) + ']';
|
||||
}
|
||||
|
||||
return SelectPrimaryKey(value) ?? value.ToString() ?? string.Empty;
|
||||
return SelectPrimaryKey(value, property) ?? value.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private string? SelectPrimaryKey(object entity, PropertyConfig config) {
|
||||
if (config.IsRelation) {
|
||||
var table = explorer.GetTable(entity.GetType());
|
||||
if (table?.ContextConfig is RepositoryGroupConfig repoConfig) {
|
||||
return repoConfig.KeyProperty.GetValue(entity)?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private string? SelectPrimaryKey(object entity) {
|
||||
return entity
|
||||
.GetType()
|
||||
.GetProperties()
|
||||
@@ -169,6 +178,11 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
||||
}
|
||||
|
||||
private Type? GetPrimaryKeyType(Type tableType) {
|
||||
var table = explorer.GetTable(tableType);
|
||||
if (table?.ContextConfig is RepositoryGroupConfig repoConfig) {
|
||||
return repoConfig.KeyProperty.PropertyType;
|
||||
}
|
||||
|
||||
return tableType
|
||||
.GetProperties()
|
||||
.FirstOrDefault(prop => prop
|
||||
|
||||
@@ -10,6 +10,7 @@ using HopFrame.Web.Services.Implementation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
@@ -44,6 +45,7 @@ public static class ServiceCollectionExtensions {
|
||||
|
||||
services.AddScoped<IPluginOrchestrator, PluginOrchestrator>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
services.TryAddScoped<ISearchSuggestionProvider, SearchSuggestionProvider>();
|
||||
|
||||
if (addRazorComponents) {
|
||||
services.AddRazorComponents()
|
||||
|
||||
@@ -2,10 +2,22 @@
|
||||
|
||||
namespace HopFrame.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides file handling capabilities for downloading and uploading files.
|
||||
/// </summary>
|
||||
public interface IFileService {
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a file download with the specified name and data.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the file to be downloaded.</param>
|
||||
/// <param name="data">The byte array representing the file's content.</param>
|
||||
public Task DownloadFile(string name, byte[] data);
|
||||
|
||||
/// <summary>
|
||||
/// Allows the user to upload a file and returns the uploaded file for processing.
|
||||
/// </summary>
|
||||
/// <returns>A task that returns an IBrowserFile representing the uploaded file.</returns>
|
||||
public Task<IBrowserFile> UploadFile();
|
||||
|
||||
}
|
||||
11
src/HopFrame.Web/Services/ISearchSuggestionProvider.cs
Normal file
11
src/HopFrame.Web/Services/ISearchSuggestionProvider.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using HopFrame.Core.Config;
|
||||
|
||||
namespace HopFrame.Web.Services;
|
||||
|
||||
public interface ISearchSuggestionProvider {
|
||||
|
||||
public IEnumerable<string> GenerateSearchSuggestions(TableConfig table, string searchText);
|
||||
|
||||
public string CompleteSearchSuggestion(TableConfig table, string searchText, string selectedSuggestion);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Services;
|
||||
using HopFrame.Web.Helpers;
|
||||
|
||||
namespace HopFrame.Web.Services.Implementation;
|
||||
|
||||
public sealed class SearchSuggestionProvider : ISearchSuggestionProvider {
|
||||
|
||||
public IEnumerable<string> GenerateSearchSuggestions(TableConfig table, string searchText) {
|
||||
var searchParts = searchText.Trim().Split(' ');
|
||||
if (searchParts.Length != 0 && searchParts.Last().EndsWith('=') && !searchText.EndsWith(' ')) {
|
||||
var part = searchParts.Last()[..^1];
|
||||
var property = table.Properties
|
||||
.Where(p => p.List)
|
||||
.Where(p => !p.IsVirtualProperty)
|
||||
.FirstOrDefault(p => p.Name == part);
|
||||
|
||||
if (property is null) return [];
|
||||
|
||||
if (property.Info.PropertyType.IsEnum)
|
||||
return Enum.GetNames(property.Info.PropertyType);
|
||||
|
||||
if (property.Info.PropertyType == typeof(DateOnly))
|
||||
return [DateOnly.FromDateTime(DateTime.Now).ToString()];
|
||||
|
||||
if (property.Info.PropertyType == typeof(TimeOnly))
|
||||
return [TimeOnly.FromDateTime(DateTime.Now).ToString()];
|
||||
}
|
||||
|
||||
if (searchText.Length != 0 && !searchText.EndsWith(' '))
|
||||
return [];
|
||||
|
||||
Type[] validTypes = [typeof(string), typeof(Guid), typeof(bool), typeof(DateOnly), typeof(TimeOnly)];
|
||||
var searchableProperties = table.Properties
|
||||
.Where(p => !p.IsVirtualProperty)
|
||||
.Where(p => p.List)
|
||||
.Where(p => p.Searchable)
|
||||
.Where(p =>
|
||||
p.Info.PropertyType.IsEnum ||
|
||||
p.Info.PropertyType.IsNumeric() ||
|
||||
validTypes.Contains(p.Info.PropertyType) ||
|
||||
p.IsRelation)
|
||||
.ToArray();
|
||||
|
||||
return searchableProperties
|
||||
.Select(p => p.Name + "=");
|
||||
}
|
||||
|
||||
public string CompleteSearchSuggestion(TableConfig table, string searchText, string selectedSuggestion) {
|
||||
return searchText + selectedSuggestion;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.11.2" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.11.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.13.2" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.13.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
59
testing/HopFrame.Testing/Models/Guest.cs
Normal file
59
testing/HopFrame.Testing/Models/Guest.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using HopFrame.Core.Repositories;
|
||||
|
||||
namespace HopFrame.Testing.Models;
|
||||
|
||||
public class Guest {
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<Message> Messages { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GuestRepository : IHopFrameRepository<Guest, int> {
|
||||
|
||||
public List<Guest> Guests { get; } = new();
|
||||
|
||||
public async Task<IEnumerable<Guest>> LoadPage(int page, int perPage) {
|
||||
return Guests
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage);
|
||||
}
|
||||
|
||||
public async Task<SearchResult<Guest>> Search(string searchTerm, int page, int perPage) {
|
||||
var results = Guests
|
||||
.Where(message => message.Name.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
var totalPages = (int)Math.Ceiling(results.Length / (double)perPage);
|
||||
return new SearchResult<Guest>(results
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage), totalPages);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalPageCount(int perPage) {
|
||||
return (int)Math.Ceiling(Guests.Count / (double)perPage);
|
||||
}
|
||||
|
||||
public Task CreateItem(Guest item) {
|
||||
Guests.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task EditItem(Guest item) {
|
||||
var old = Guests.Find(m => m.Id == item.Id);
|
||||
if (old is not null)
|
||||
Guests.Remove(old);
|
||||
|
||||
Guests.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteItem(Guest item) {
|
||||
Guests.Remove(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<Guest?> GetOne(int key) {
|
||||
return Guests.Find(m => m.Id == key);
|
||||
}
|
||||
}
|
||||
59
testing/HopFrame.Testing/Models/Message.cs
Normal file
59
testing/HopFrame.Testing/Models/Message.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using HopFrame.Core.Repositories;
|
||||
|
||||
namespace HopFrame.Testing.Models;
|
||||
|
||||
public class Message {
|
||||
public required int MessageIdentifier { get; set; }
|
||||
public required User Sender { get; set; }
|
||||
public required Guest Receiver { get; set; }
|
||||
public required string Content { get; set; }
|
||||
}
|
||||
|
||||
public class MessageRepository : IHopFrameRepository<Message, int> {
|
||||
|
||||
public List<Message> Messages { get; } = new();
|
||||
|
||||
public async Task<IEnumerable<Message>> LoadPage(int page, int perPage) {
|
||||
return Messages
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage);
|
||||
}
|
||||
|
||||
public async Task<SearchResult<Message>> Search(string searchTerm, int page, int perPage) {
|
||||
var results = Messages
|
||||
.Where(message => message.Content.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
var totalPages = (int)Math.Ceiling(results.Length / (double)perPage);
|
||||
return new SearchResult<Message>(results
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage), totalPages);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalPageCount(int perPage) {
|
||||
return (int)Math.Ceiling(Messages.Count / (double)perPage);
|
||||
}
|
||||
|
||||
public Task CreateItem(Message item) {
|
||||
Messages.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task EditItem(Message item) {
|
||||
var old = Messages.Find(m => m.MessageIdentifier == item.MessageIdentifier);
|
||||
if (old is not null)
|
||||
Messages.Remove(old);
|
||||
|
||||
Messages.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteItem(Message item) {
|
||||
Messages.Remove(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<Message?> GetOne(int key) {
|
||||
return Messages.Find(m => m.MessageIdentifier == key);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,4 @@ public class User {
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public virtual List<Post> Posts { get; set; } = new();
|
||||
|
||||
public override string ToString() {
|
||||
return Username;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using HopFrame.Testing.Components;
|
||||
using HopFrame.Testing.Models;
|
||||
using HopFrame.Web;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Message = HopFrame.Testing.Models.Message;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -50,9 +51,13 @@ builder.Services.AddHopFrame(options => {
|
||||
.FormatEach<Post>((post, _) => post.Caption);
|
||||
});
|
||||
|
||||
context.Table<Post>()
|
||||
.ShowSearchSuggestions(false);
|
||||
|
||||
context.Table<Post>()
|
||||
.Property(p => p.Author)
|
||||
.Format((user, _) => $"{user.FirstName} {user.LastName}")
|
||||
//.Format((user, _) => $"{user.FirstName} {user.LastName}")
|
||||
.SetDisplayedProperty(u => u.Username)
|
||||
.SetValidator((_, _) => []);
|
||||
|
||||
context.Table<Post>()
|
||||
@@ -88,8 +93,31 @@ builder.Services.AddHopFrame(options => {
|
||||
.SetPolicy("counter.view");
|
||||
|
||||
options.AddExporters();
|
||||
|
||||
options.AddCustomRepository<GuestRepository, Guest, int>(g => g.Id, table => {
|
||||
table.SetDisplayName("Guests");
|
||||
|
||||
table.Property(g => g.Messages)
|
||||
.ForceRelation(true)
|
||||
.FormatEach<Message>((m, _) => m.Content);
|
||||
});
|
||||
|
||||
options.AddCustomRepository<MessageRepository, Message, int>(m => m.MessageIdentifier, table => {
|
||||
table.SetDisplayName("Messages");
|
||||
|
||||
table.Property(m => m.Receiver)
|
||||
.ForceRelation()
|
||||
.Format((u, _) => u.Name);
|
||||
|
||||
table.Property(m => m.Sender)
|
||||
.ForceRelation()
|
||||
.Format((u, _) => u.Username ?? string.Empty);
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<MessageRepository>();
|
||||
builder.Services.AddSingleton<GuestRepository>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.2"/>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Services;
|
||||
using HopFrame.Core.Services.Implementations;
|
||||
using HopFrame.Tests.Core.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
@@ -121,6 +123,7 @@ public class ContextExplorerTests {
|
||||
var dbContext = new MockDbContext();
|
||||
var provider = new Mock<IServiceProvider>();
|
||||
provider.Setup(p => p.GetService(typeof(MockDbContext))).Returns(dbContext);
|
||||
provider.Setup(p => p.GetService(typeof(ISearchExpressionBuilder))).Returns(new Mock<ISearchExpressionBuilder>().Object);
|
||||
var contextExplorer = new ContextExplorer(config, provider.Object, new Logger<ContextExplorer>(new LoggerFactory()));
|
||||
|
||||
// Act
|
||||
|
||||
@@ -19,7 +19,7 @@ public class DisplayPropertyTests {
|
||||
_explorerMock = new Mock<IContextExplorer>();
|
||||
_config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||
_tableManager =
|
||||
new TableManager<object>(contextMock.Object, _config, _explorerMock.Object, _providerMock.Object);
|
||||
new TableManager<object>(contextMock.Object, _config, _explorerMock.Object, _providerMock.Object, new SearchExpressionBuilder(_explorerMock.Object));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -10,6 +10,9 @@ using Moq;
|
||||
namespace HopFrame.Tests.Core.Services;
|
||||
|
||||
public class TableManagerTests {
|
||||
|
||||
private Mock<ISearchExpressionBuilder> _searchBuilderMock = new();
|
||||
|
||||
private Mock<DbContext> CreateMockDbContext<TModel>(List<TModel> data) where TModel : class {
|
||||
var dbContext = new Mock<DbContext>();
|
||||
var dbSet = CreateMockDbSet(data);
|
||||
@@ -40,7 +43,7 @@ public class TableManagerTests {
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadPage_ReturnsPagedData() {
|
||||
public async Task LoadPage_ReturnsPagedData() {
|
||||
// Arrange
|
||||
var data = new List<MockModel> {
|
||||
new MockModel { Id = 1, Name = "Item1" },
|
||||
@@ -51,42 +54,16 @@ public class TableManagerTests {
|
||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||
var explorer = new Mock<IContextExplorer>();
|
||||
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, _searchBuilderMock.Object);
|
||||
|
||||
// Act
|
||||
var result = manager.LoadPage(1, 2).ToList();
|
||||
var result = (await manager.LoadPage(1, 2)).ToArray();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Item3", ((MockModel)result[0]).Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_ReturnsMatchingData() {
|
||||
// Arrange
|
||||
var data = new List<MockModel> {
|
||||
new MockModel { Id = 1, Name = "Item1" },
|
||||
new MockModel { Id = 2, Name = "Item2" },
|
||||
new MockModel { Id = 3, Name = "TestItem" }
|
||||
};
|
||||
var dbContext = CreateMockDbContext(data);
|
||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||
config.Properties.Add(new PropertyConfig(typeof(MockModel).GetProperty("Name")!, config, 0)
|
||||
{ Searchable = true });
|
||||
var explorer = new Mock<IContextExplorer>();
|
||||
var provider = new Mock<IServiceProvider>();
|
||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
||||
|
||||
// Act
|
||||
var (result, totalPages) = await manager.Search("Test", 0, 2);
|
||||
|
||||
// Assert
|
||||
var collection = result as object[] ?? result.ToArray();
|
||||
Assert.Single(collection);
|
||||
Assert.Equal("TestItem", ((MockModel)collection.First()).Name);
|
||||
Assert.Equal(1, totalPages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TotalPages_ReturnsCorrectPageCount() {
|
||||
// Arrange
|
||||
@@ -99,7 +76,7 @@ public class TableManagerTests {
|
||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||
var explorer = new Mock<IContextExplorer>();
|
||||
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, _searchBuilderMock.Object);
|
||||
await dbContext.Models.AddRangeAsync(data);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
@@ -121,7 +98,7 @@ public class TableManagerTests {
|
||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||
var explorer = new Mock<IContextExplorer>();
|
||||
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, _searchBuilderMock.Object);
|
||||
var item = data.First();
|
||||
|
||||
// Act
|
||||
@@ -142,7 +119,7 @@ public class TableManagerTests {
|
||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||
var explorer = new Mock<IContextExplorer>();
|
||||
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, _searchBuilderMock.Object);
|
||||
|
||||
// Act
|
||||
await manager.EditItem(data.First());
|
||||
@@ -159,7 +136,7 @@ public class TableManagerTests {
|
||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||
var explorer = new Mock<IContextExplorer>();
|
||||
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, _searchBuilderMock.Object);
|
||||
var newItem = new MockModel { Id = 3, Name = "NewItem" };
|
||||
|
||||
// Act
|
||||
|
||||
@@ -47,7 +47,7 @@ public class HopFrameEditorTests : TestContext {
|
||||
|
||||
var dialogData = new EditorDialogData(tableConfig, new MyTable());
|
||||
var dialog = new FluentDialog() {
|
||||
Instance = new DialogInstance(typeof(HopFrameEditor), new DialogParameters(), dialogData)
|
||||
Instance = new DialogInstance(typeof(HopFrameEditor), new DialogParameters(), dialogData, null)
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -33,7 +33,7 @@ public class HopFrameTablePageTests : TestContext {
|
||||
contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig);
|
||||
contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(managerMock.Object);
|
||||
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true);
|
||||
managerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(Enumerable.Empty<object>().AsAsyncQueryable());
|
||||
managerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync([]);
|
||||
|
||||
Services.AddHopFrame(config, null, false);
|
||||
Services.AddSingleton(contextExplorerMock.Object);
|
||||
@@ -71,7 +71,7 @@ public class HopFrameTablePageTests : TestContext {
|
||||
|
||||
var tableManagerMock = new Mock<ITableManager>();
|
||||
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>())).ReturnsAsync(items);
|
||||
tableManagerMock.Setup(t => t.DisplayProperty(It.IsAny<object>(), It.IsAny<PropertyConfig>(), null, null))
|
||||
.ReturnsAsync(string.Empty);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
Reference in New Issue
Block a user