Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12c86bcb16 | |||
| a826ddbfb1 | |||
| 297cf00891 | |||
| d393ae787d | |||
| 10913b0a21 | |||
| 31b0b3970a | |||
| e6726037b6 | |||
| c5388fc044 | |||
| 66d03513eb | |||
| 68a4479c2d | |||
| 5dec609004 | |||
| 7d3aa6de94 | |||
| 5c6fafcd6f | |||
| 222d4276d2 | |||
| 4407d173a9 |
@@ -1,14 +1,67 @@
|
|||||||
image: mcr.microsoft.com/dotnet/sdk:9.0
|
image: mcr.microsoft.com/dotnet/sdk:9.0
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
- publish
|
||||||
- publish-help
|
- 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:
|
publish-help:
|
||||||
stage: publish-help
|
stage: publish-help
|
||||||
image: docker:latest
|
tags:
|
||||||
services:
|
- docker
|
||||||
- name: docker:dind
|
|
||||||
alias: docker
|
|
||||||
script:
|
script:
|
||||||
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
|
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
|
||||||
- cd docs
|
- cd docs
|
||||||
@@ -18,3 +71,5 @@ publish-help:
|
|||||||
- docker push registry.leon-hoppe.de/leon.hoppe/hopframe:latest
|
- docker push registry.leon-hoppe.de/leon.hoppe/hopframe:latest
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
|
dependencies:
|
||||||
|
- publish
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="RiderProjectSettingsUpdater">
|
<component name="RiderProjectSettingsUpdater">
|
||||||
<option name="singleClickDiffPreview" value="1" />
|
<option name="singleClickDiffPreview" value="1" />
|
||||||
|
<option name="unhandledExceptionsIgnoreList" value="1" />
|
||||||
<option name="vcsConfiguration" value="3" />
|
<option name="vcsConfiguration" value="3" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
463
.idea/.idea.HopFrame/.idea/workspace.xml
generated
463
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -12,9 +12,9 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
|
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/docs/Writerside/topics/Plugins.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/Writerside/topics/Plugins.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Plugins/Internal/ExporterPlugin.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Plugins/Internal/ExporterPlugin.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -34,10 +34,11 @@
|
|||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
<map>
|
<map>
|
||||||
<entry key="$PROJECT_DIR$" value="feature/virtual-properties" />
|
<entry key="$PROJECT_DIR$" value="dev" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
<option name="RESET_MODE" value="HARD" />
|
||||||
</component>
|
</component>
|
||||||
<component name="GitLabMergeRequestFiltersHistory">{
|
<component name="GitLabMergeRequestFiltersHistory">{
|
||||||
"lastFilter": {
|
"lastFilter": {
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
<component name="GitLabMergeRequestsSettings">{
|
<component name="GitLabMergeRequestsSettings">{
|
||||||
"selectedUrlAndAccountId": {
|
"selectedUrlAndAccountId": {
|
||||||
"first": "https://git.leon-hoppe.de/leon.hoppe/hopframe.git",
|
"first": "https://git.leon-hoppe.de/leon.hoppe/hopframe.git",
|
||||||
"second": "2d65fdcb-5f13-45ad-a7ba-91dd4a88d6e4"
|
"second": "f58c9371-9f54-454e-a0db-5b4bc1187bad"
|
||||||
}
|
}
|
||||||
}</component>
|
}</component>
|
||||||
<component name="HighlightingSettingsPerFile">
|
<component name="HighlightingSettingsPerFile">
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
<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/a0/0a968c53/IEnumerable`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/ad/ba9a50e7/ICollection.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/ad/ba9a50e7/ICollection.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/fc/6f7933d2/ICollection`1.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/fc/6f7933d2/ICollection`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/0f73968de5cdfe0aa57817b8dd2a3c5d1db615ba4ae4629a5af59bb6c8922/RemoteNavigationManager.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/10c66a9a1e137111895f7182a2ae246eabe06a261578c3fa495a45f6f177d35/IconVariant.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/10c66a9a1e137111895f7182a2ae246eabe06a261578c3fa495a45f6f177d35/IconVariant.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/1b81cb3be224213a6a73519b6e340a628d9a1fb8629c351a186a26f6376669/List.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/24dd1164ba47541cb1d3eb011e638e16953dbea3ae3f4dc208c3bbf3e96298a/ServiceCollectionServiceExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
@@ -80,6 +82,7 @@
|
|||||||
<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/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/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/7ad7d2d0ae865063993eb8a03427815ea3bdb6a774e0a2f95512e9f669a4f489/MemberEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/876cd892fc66a9dc8f6afd3704c264acebdfc46aed08089463e8117c21a532/String.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/87c584767b46b5fd42769be76547105558e6690f785614efddca134b2d682/Type.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/87c584767b46b5fd42769be76547105558e6690f785614efddca134b2d682/Type.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/8d5d6cbff46ddc7b152381f92ae1ae51d3e7b57b14dd23840a11f5aaaaed396/InternalEntityEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/8d5d6cbff46ddc7b152381f92ae1ae51d3e7b57b14dd23840a11f5aaaaed396/InternalEntityEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a882d183338544fdbcbdfc7b6d3dcb78916630765551644a221b5be9c45a121b/Int32.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a882d183338544fdbcbdfc7b6d3dcb78916630765551644a221b5be9c45a121b/Int32.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
@@ -111,7 +114,7 @@
|
|||||||
"associatedIndex": 3
|
"associatedIndex": 3
|
||||||
}</component>
|
}</component>
|
||||||
<component name="ProjectId" id="2raEUZtlphkj04rfRNtlQw6fPMU" />
|
<component name="ProjectId" id="2raEUZtlphkj04rfRNtlQw6fPMU" />
|
||||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
<component name="ProjectLevelVcsManager">
|
||||||
<OptionsSetting value="false" id="Update" />
|
<OptionsSetting value="false" id="Update" />
|
||||||
<ConfirmationsSetting value="2" id="Add" />
|
<ConfirmationsSetting value="2" id="Add" />
|
||||||
</component>
|
</component>
|
||||||
@@ -128,9 +131,10 @@
|
|||||||
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
|
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
||||||
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
||||||
"git-widget-placeholder": "!33 on feature/exporters",
|
"git-widget-placeholder": "dev",
|
||||||
"list.type.of.created.stylesheet": "CSS",
|
"list.type.of.created.stylesheet": "CSS",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
@@ -146,13 +150,14 @@
|
|||||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
|
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
|
||||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||||
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||||
|
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||||
|
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
@@ -161,13 +166,14 @@
|
|||||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
|
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
|
||||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||||
|
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||||
|
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
@@ -176,13 +182,14 @@
|
|||||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj" />
|
<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_TFM" value="net9.0" />
|
||||||
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||||
|
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||||
|
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
@@ -191,13 +198,14 @@
|
|||||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj" />
|
<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_TFM" value="net9.0" />
|
||||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
||||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||||
|
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||||
|
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
@@ -258,150 +266,21 @@
|
|||||||
<workItem from="1740736919561" duration="191000" />
|
<workItem from="1740736919561" duration="191000" />
|
||||||
<workItem from="1740738257628" duration="3216000" />
|
<workItem from="1740738257628" duration="3216000" />
|
||||||
<workItem from="1740741585276" duration="17000" />
|
<workItem from="1740741585276" duration="17000" />
|
||||||
</task>
|
<workItem from="1740742098571" duration="78000" />
|
||||||
<task id="LOCAL-00001" summary="Added basic configuration">
|
<workItem from="1740742471317" duration="672000" />
|
||||||
<option name="closed" value="true" />
|
<workItem from="1741974241977" duration="10854000" />
|
||||||
<created>1736850899254</created>
|
<workItem from="1742038098473" duration="990000" />
|
||||||
<option name="number" value="00001" />
|
<workItem from="1742059898156" duration="3488000" />
|
||||||
<option name="presentableId" value="LOCAL-00001" />
|
<workItem from="1744725284649" duration="60000" />
|
||||||
<option name="project" value="LOCAL" />
|
<workItem from="1744916016381" duration="66000" />
|
||||||
<updated>1736850899254</updated>
|
<workItem from="1744916106166" duration="49000" />
|
||||||
</task>
|
<workItem from="1744966207145" duration="5231000" />
|
||||||
<task id="LOCAL-00002" summary="Added admin page navigation">
|
<workItem from="1751713720880" duration="8243000" />
|
||||||
<option name="closed" value="true" />
|
<workItem from="1751741813788" duration="4623000" />
|
||||||
<created>1736855209077</created>
|
<workItem from="1768753475773" duration="455000" />
|
||||||
<option name="number" value="00002" />
|
<workItem from="1768753946559" duration="690000" />
|
||||||
<option name="presentableId" value="LOCAL-00002" />
|
<workItem from="1768756619311" duration="94000" />
|
||||||
<option name="project" value="LOCAL" />
|
<workItem from="1768847832546" duration="758000" />
|
||||||
<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>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00017" summary="Created tests for the core module">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1737212497960</created>
|
|
||||||
<option name="number" value="00017" />
|
|
||||||
<option name="presentableId" value="LOCAL-00017" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1737212497960</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00018" summary="Added more tests">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1737285123218</created>
|
|
||||||
<option name="number" value="00018" />
|
|
||||||
<option name="presentableId" value="LOCAL-00018" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1737285123218</updated>
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00019" summary="Added web module tests">
|
<task id="LOCAL-00019" summary="Added web module tests">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -611,7 +490,191 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1740741334420</updated>
|
<updated>1740741334420</updated>
|
||||||
</task>
|
</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="Code cleanup + new pipeline setup">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1768754560236</created>
|
||||||
|
<option name="number" value="00065" />
|
||||||
|
<option name="presentableId" value="LOCAL-00065" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1768754560236</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00066" summary="Fixed #38">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1768847968844</created>
|
||||||
|
<option name="number" value="00066" />
|
||||||
|
<option name="presentableId" value="LOCAL-00066" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1768847968844</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00067" summary="Fixed #37">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1768848267710</created>
|
||||||
|
<option name="number" value="00067" />
|
||||||
|
<option name="presentableId" value="LOCAL-00067" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1768848267710</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="68" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -621,38 +684,17 @@
|
|||||||
<option name="coveragePercentColumnWidth" value="129" />
|
<option name="coveragePercentColumnWidth" value="129" />
|
||||||
<option name="sortOrder" value="DESCENDING" />
|
<option name="sortOrder" value="DESCENDING" />
|
||||||
<option name="sortedColumn" value="1" />
|
<option name="sortedColumn" value="1" />
|
||||||
<option name="symbolColumnWidth" value="451" />
|
<option name="symbolColumnWidth" value="559" />
|
||||||
<coverage-tree-state>
|
<coverage-tree-state>
|
||||||
<expand>
|
<expand>
|
||||||
<path>
|
<path>
|
||||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
<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>
|
||||||
<path>
|
<path>
|
||||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
<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" />
|
||||||
<item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
<item name="Core 40% 862/1439" 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" />
|
|
||||||
</path>
|
</path>
|
||||||
</expand>
|
</expand>
|
||||||
<select />
|
<select />
|
||||||
@@ -660,33 +702,44 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
|
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
|
||||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
<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">
|
<component name="VcsManagerConfiguration">
|
||||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
<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="Added basic export and import feature" />
|
||||||
<MESSAGE value="Finished converter plugin" />
|
<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="Code cleanup + new pipeline setup" />
|
||||||
|
<MESSAGE value="Fixed #38" />
|
||||||
|
<MESSAGE value="Fixed #37" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="Fixed #37" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<toc-element topic="PropertyConfig.md"/>
|
<toc-element topic="PropertyConfig.md"/>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
<toc-element topic="Callbacks.md"/>
|
<toc-element topic="Callbacks.md"/>
|
||||||
|
<toc-element topic="Custom-Repositories.md"/>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
<toc-element toc-title="Web Module">
|
<toc-element toc-title="Web Module">
|
||||||
<toc-element toc-title="Interface">
|
<toc-element toc-title="Interface">
|
||||||
@@ -27,6 +28,10 @@
|
|||||||
<toc-element topic="Plugins.md">
|
<toc-element topic="Plugins.md">
|
||||||
<toc-element topic="Events.md">
|
<toc-element topic="Events.md">
|
||||||
</toc-element>
|
</toc-element>
|
||||||
|
<toc-element topic="Exporter-Plugin.md"/>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
|
<toc-element toc-title="Services">
|
||||||
|
<toc-element topic="IFileService.md"/>
|
||||||
|
</toc-element>
|
||||||
</instance-profile>
|
</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.
|
- **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
|
### DisplayUserInfo
|
||||||
|
|
||||||
Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin UI.
|
Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin UI.
|
||||||
|
|||||||
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;
|
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 Type ContextType { get; }
|
||||||
public List<TableConfig> Tables { get; } = new();
|
public List<TableConfig> Tables { get; } = new();
|
||||||
public HopFrameConfig ParentConfig { get; }
|
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.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace HopFrame.Core.Config;
|
namespace HopFrame.Core.Config;
|
||||||
|
|
||||||
public class HopFrameConfig {
|
public class HopFrameConfig {
|
||||||
public List<DbContextConfig> Contexts { get; } = new();
|
public List<ITableGroupConfig> Contexts { get; } = new();
|
||||||
public bool DisplayUserInfo { get; set; } = true;
|
public bool DisplayUserInfo { get; set; } = true;
|
||||||
public string? BasePolicy { get; set; }
|
public string? BasePolicy { get; set; }
|
||||||
public string? LoginPageRewrite { get; set; }
|
public string? LoginPageRewrite { get; set; }
|
||||||
@@ -48,6 +50,36 @@ public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollecti
|
|||||||
return new DbContextConfigurator<TDbContext>(context);
|
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>
|
/// <summary>
|
||||||
/// Check if a context is already registered in the HopFrame
|
/// Check if a context is already registered in the HopFrame
|
||||||
/// </summary>
|
/// </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>
|
/// <returns>The configurator of the context if it already was defined, null if not</returns>
|
||||||
public DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext : DbContext {
|
public DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext : DbContext {
|
||||||
var config = InnerConfig.Contexts
|
var config = InnerConfig.Contexts
|
||||||
|
.OfType<DbContextConfig>()
|
||||||
.SingleOrDefault(context => context.ContextType == typeof(TDbContext));
|
.SingleOrDefault(context => context.ContextType == typeof(TDbContext));
|
||||||
if (config is null) return null;
|
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 PropertyName { get; }
|
||||||
public string DisplayName { get; set; }
|
public string DisplayName { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public DbContextConfig ContextConfig { get; }
|
public ITableGroupConfig ContextConfig { get; }
|
||||||
public bool Ignored { get; set; }
|
public bool Ignored { get; set; }
|
||||||
public int Order { get; set; }
|
public int Order { get; set; }
|
||||||
internal bool Seeded { get; set; }
|
internal bool Seeded { get; set; }
|
||||||
|
public bool ShowSearchSuggestions { get; set; } = true;
|
||||||
|
|
||||||
public string? ViewPolicy { get; set; }
|
public string? ViewPolicy { get; set; }
|
||||||
public string? CreatePolicy { get; set; }
|
public string? CreatePolicy { get; set; }
|
||||||
@@ -23,7 +24,7 @@ public class TableConfig {
|
|||||||
|
|
||||||
public List<PropertyConfig> Properties { get; } = new();
|
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;
|
TableType = tableType;
|
||||||
PropertyName = propertyName;
|
PropertyName = propertyName;
|
||||||
ContextConfig = config;
|
ContextConfig = config;
|
||||||
@@ -62,10 +63,17 @@ public sealed class TableConfigurator<TModel>(TableConfig config) {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if the table should be ignored in the admin ui
|
/// Determines if the table should be ignored in the admin ui
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TableConfigurator<TModel> Ignore(bool ignore) {
|
public TableConfigurator<TModel> Ignore(bool ignore = true) {
|
||||||
InnerConfig.Ignored = ignore;
|
InnerConfig.Ignored = ignore;
|
||||||
return this;
|
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>
|
/// <summary>
|
||||||
/// Configures the property of the table
|
/// Configures the property of the table
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<PackageId>HopFrame.Core</PackageId>
|
<PackageId>HopFrame.Core</PackageId>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<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.AddScoped<IContextExplorer, ContextExplorer>();
|
||||||
services.TryAddScoped<IHopFrameAuthHandler, DefaultAuthHandler>();
|
services.TryAddScoped<IHopFrameAuthHandler, DefaultAuthHandler>();
|
||||||
services.TryAddScoped<ICallbackEmitter, CallbackEmitter>();
|
services.TryAddScoped<ICallbackEmitter, CallbackEmitter>();
|
||||||
|
services.AddScoped<ISearchExpressionBuilder, SearchExpressionBuilder>();
|
||||||
return services;
|
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;
|
namespace HopFrame.Core.Services;
|
||||||
|
|
||||||
public interface ITableManager {
|
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<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20);
|
||||||
public Task<int> TotalPages(int perPage = 20);
|
public Task<int> TotalPages(int perPage = 20);
|
||||||
public Task DeleteItem(object item);
|
public Task DeleteItem(object item);
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
|
|
||||||
public TableConfig? GetTable(string tableDisplayName) {
|
public TableConfig? GetTable(string tableDisplayName) {
|
||||||
foreach (var context in config.Contexts) {
|
foreach (var context in config.Contexts) {
|
||||||
var table = context.Tables.FirstOrDefault(table => table.DisplayName.Equals(tableDisplayName, StringComparison.CurrentCultureIgnoreCase));
|
var table = context.Tables
|
||||||
|
.Where(t => !t.Ignored)
|
||||||
|
.FirstOrDefault(table => table.DisplayName.Equals(tableDisplayName, StringComparison.CurrentCultureIgnoreCase));
|
||||||
if (table is null) continue;
|
if (table is null) continue;
|
||||||
|
|
||||||
SeedTableData(table);
|
SeedTableData(table);
|
||||||
@@ -30,7 +32,8 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
|
|
||||||
public TableConfig? GetTable(Type tableEntity) {
|
public TableConfig? GetTable(Type tableEntity) {
|
||||||
foreach (var context in config.Contexts) {
|
foreach (var context in config.Contexts) {
|
||||||
var table = context.Tables.FirstOrDefault(table => table.TableType == tableEntity);
|
var table = context.Tables
|
||||||
|
.FirstOrDefault(table => table.TableType == tableEntity);
|
||||||
if (table is null) continue;
|
if (table is null) continue;
|
||||||
|
|
||||||
SeedTableData(table);
|
SeedTableData(table);
|
||||||
@@ -45,11 +48,18 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tablePropertyName);
|
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tablePropertyName);
|
||||||
if (table is null) continue;
|
if (table is null) continue;
|
||||||
|
|
||||||
var dbContext = provider.GetService(context.ContextType) as DbContext;
|
var repo = provider.GetService(context.ContextType);
|
||||||
if (dbContext is null) return null;
|
if (repo is null) return null;
|
||||||
|
|
||||||
|
if (context is DbContextConfig) {
|
||||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
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;
|
return null;
|
||||||
@@ -60,11 +70,18 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
var table = context.Tables.FirstOrDefault(table => table.TableType == tableType);
|
var table = context.Tables.FirstOrDefault(table => table.TableType == tableType);
|
||||||
if (table is null) continue;
|
if (table is null) continue;
|
||||||
|
|
||||||
var dbContext = provider.GetService(context.ContextType) as DbContext;
|
var repo = provider.GetService(context.ContextType);
|
||||||
if (dbContext is null) return null;
|
if (repo is null) return null;
|
||||||
|
|
||||||
|
if (context is DbContextConfig) {
|
||||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
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;
|
return null;
|
||||||
@@ -72,6 +89,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
|
|
||||||
private void SeedTableData(TableConfig table) {
|
private void SeedTableData(TableConfig table) {
|
||||||
if (table.Seeded) return;
|
if (table.Seeded) return;
|
||||||
|
if (table.ContextConfig is not DbContextConfig) return;
|
||||||
var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!;
|
var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!;
|
||||||
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
||||||
|
|
||||||
|
|||||||
@@ -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.Collections;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Metadata;
|
||||||
using HopFrame.Core.Config;
|
using HopFrame.Core.Config;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
|
||||||
namespace HopFrame.Core.Services.Implementations;
|
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 table = context.Set<TModel>();
|
||||||
var data = IncludeForeignKeys(table);
|
var data = IncludeForeignKeys(table);
|
||||||
return data
|
return await data
|
||||||
.Skip(page * perPage)
|
.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 table = context.Set<TModel>();
|
||||||
var all = IncludeForeignKeys(table)
|
|
||||||
.AsEnumerable()
|
|
||||||
.Where(item => ItemSearched(item, searchTerm))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return Task.FromResult((
|
var parameter = Expression.Parameter(typeof(TModel), "x");
|
||||||
(IEnumerable<object>)all.Skip(page * perPage).Take(perPage),
|
var exp = searchExpressionBuilder.BuildSearchExpression(config, searchTerm, parameter);
|
||||||
(int)Math.Ceiling(all.Count / (double)perPage)));
|
|
||||||
|
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) {
|
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);
|
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) {
|
public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) {
|
||||||
if (item is null) return string.Empty;
|
if (item is null) return string.Empty;
|
||||||
|
|
||||||
|
|||||||
@@ -39,11 +39,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 5px; margin-bottom: 4px">
|
<div style="display: flex; gap: 5px; margin-bottom: 4px">
|
||||||
@if (!property.IsRequired) {
|
@if (!property.IsRequired) {
|
||||||
<FluentButton OnClick="async () => await SetPropertyValue(property, null, InputType.Relation)" Disabled="@(_currentlyEditing && !property.Editable)">
|
<FluentButton OnClick="@(async () => await SetPropertyValue(property, null, InputType.Relation))" Disabled="@(_currentlyEditing && !property.Editable)">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
}
|
}
|
||||||
<FluentButton OnClick="async () => await OpenRelationalPicker(property)" Disabled="@(_currentlyEditing && !property.Editable)">
|
<FluentButton OnClick="@(async () => await OpenRelationalPicker(property))" Disabled="@(_currentlyEditing && !property.Editable)">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size20.Open())" Color="Color.Neutral" />
|
<FluentIcon Value="@(new Icons.Regular.Size20.Open())" Color="Color.Neutral" />
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Enum))" />
|
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Enum))" />
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 5px">
|
<div style="display: flex; gap: 5px">
|
||||||
<FluentButton OnClick="async () => await SetPropertyValue(property, null, InputType.Enum)" Disabled="@(_currentlyEditing && !property.Editable)">
|
<FluentButton OnClick="@(async () => await SetPropertyValue(property, null, InputType.Enum))" Disabled="@(_currentlyEditing && !property.Editable)">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -340,7 +340,7 @@
|
|||||||
|
|
||||||
var relationType = config.Info.PropertyType;
|
var relationType = config.Info.PropertyType;
|
||||||
if (config.IsEnumerable) {
|
if (config.IsEnumerable) {
|
||||||
relationType = config.Info.PropertyType.GetGenericArguments().First();
|
relationType = relationType.GetGenericArguments().First();
|
||||||
}
|
}
|
||||||
|
|
||||||
var relationTable = Explorer.GetTable(relationType);
|
var relationTable = Explorer.GetTable(relationType);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
DisplayActions="false"
|
DisplayActions="false"
|
||||||
DisplaySelection="true"
|
DisplaySelection="true"
|
||||||
TableDisplayName="@Content.SourceTable.DisplayName"
|
TableDisplayName="@Content.SourceTable.DisplayName"
|
||||||
|
TableType="@Content.SourceTable.TableType"
|
||||||
PerPage="15"
|
PerPage="15"
|
||||||
DialogData="Content"
|
DialogData="Content"
|
||||||
SelectionMode="@(Content.AllowMultiple ? DataGridSelectMode.Multiple : DataGridSelectMode.Single)"/>
|
SelectionMode="@(Content.AllowMultiple ? DataGridSelectMode.Multiple : DataGridSelectMode.Single)"/>
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
@using HopFrame.Web.Models
|
@using HopFrame.Web.Models
|
||||||
@using HopFrame.Web.Plugins
|
@using HopFrame.Web.Plugins
|
||||||
@using HopFrame.Web.Plugins.Events
|
@using HopFrame.Web.Plugins.Events
|
||||||
|
@using HopFrame.Web.Services
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
|
|
||||||
@if (!DisplaySelection) {
|
@if (!DisplaySelection) {
|
||||||
<PageTitle>@_config?.DisplayName</PageTitle>
|
<PageTitle>@_config?.DisplayName</PageTitle>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
@if (!DisplaySelection && _buttonToggles.ShowRefreshButton) {
|
@if (!DisplaySelection && _buttonToggles.ShowRefreshButton) {
|
||||||
<FluentButton
|
<FluentButton
|
||||||
IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
|
IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
|
||||||
OnClick="Reload"
|
OnClick="@(Reload)"
|
||||||
Loading="_loading"
|
Loading="_loading"
|
||||||
Style="margin-left: 10px">
|
Style="margin-left: 10px">
|
||||||
Refresh
|
Refresh
|
||||||
@@ -34,22 +34,43 @@
|
|||||||
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopLeft)) {
|
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopLeft)) {
|
||||||
<FluentButton
|
<FluentButton
|
||||||
IconStart="@(button.Icon?.GetInstance())"
|
IconStart="@(button.Icon?.GetInstance())"
|
||||||
OnClick="() => button.Handler.Invoke(null!, _config!)">
|
OnClick="@(() => button.Handler.Invoke(null!, _config!))">
|
||||||
@button.Title
|
@button.Title
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
}
|
}
|
||||||
|
|
||||||
<FluentSpacer />
|
<FluentSpacer />
|
||||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
<div
|
||||||
|
style="position: relative; height: 32px"
|
||||||
|
class="hopframe-search">
|
||||||
|
|
||||||
|
@* ReSharper disable once CSharpWarnings::CS4014 *@
|
||||||
|
<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) {
|
@if (_hasCreatePolicy && DisplayActions && _buttonToggles.ShowAddEntityButton) {
|
||||||
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entity</FluentButton>
|
<FluentButton OnClick="@(async () => { await CreateOrEdit(null); })">Add Entity</FluentButton>
|
||||||
}
|
}
|
||||||
|
|
||||||
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopRight)) {
|
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopRight)) {
|
||||||
<FluentButton
|
<FluentButton
|
||||||
IconStart="@(button.Icon?.GetInstance())"
|
IconStart="@(button.Icon?.GetInstance())"
|
||||||
OnClick="() => button.Handler.Invoke(null!, _config!)">
|
OnClick="@(() => button.Handler.Invoke(null!, _config!))">
|
||||||
@button.Title
|
@button.Title
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
}
|
}
|
||||||
@@ -64,7 +85,7 @@
|
|||||||
TGridItem="object"
|
TGridItem="object"
|
||||||
SelectMode="SelectionMode"
|
SelectMode="SelectionMode"
|
||||||
SelectFromEntireRow="true"
|
SelectFromEntireRow="true"
|
||||||
OnSelect="data => SelectItem(data.Item, data.Selected)"
|
OnSelect="@(data => SelectItem(data.Item, data.Selected))"
|
||||||
SelectAllDisabled="true"
|
SelectAllDisabled="true"
|
||||||
Property="o => DialogData!.SelectedObjects.Contains(o)"
|
Property="o => DialogData!.SelectedObjects.Contains(o)"
|
||||||
Style="min-width: max-content; height: 44px; display: grid; align-items: center" />
|
Style="min-width: max-content; height: 44px; display: grid; align-items: center" />
|
||||||
@@ -80,19 +101,19 @@
|
|||||||
@if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy)) {
|
@if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy)) {
|
||||||
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content">
|
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content">
|
||||||
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.OnEntry)) {
|
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.OnEntry)) {
|
||||||
<FluentButton OnClick="() => button.Handler.Invoke(context, _config!)">
|
<FluentButton OnClick="@(() => button.Handler.Invoke(context, _config!))">
|
||||||
<FluentIcon Value="@(button.Icon!.GetInstance())" />
|
<FluentIcon Value="@(button.Icon!.GetInstance())" />
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_hasUpdatePolicy && _buttonToggles.ShowEditButton) {
|
@if (_hasUpdatePolicy && _buttonToggles.ShowEditButton) {
|
||||||
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(context); }">
|
<FluentButton aria-label="Edit entry" OnClick="@(async () => { await CreateOrEdit(context); })">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_hasDeletePolicy && _buttonToggles.ShowDeleteButton) {
|
@if (_hasDeletePolicy && _buttonToggles.ShowDeleteButton) {
|
||||||
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(context); }">
|
<FluentButton aria-label="Delete entry" OnClick="@(async () => { await DeleteEntry(context); })">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
}
|
}
|
||||||
@@ -104,7 +125,7 @@
|
|||||||
|
|
||||||
@if (_totalPages > 1) {
|
@if (_totalPages > 1) {
|
||||||
<div class="hopframe-paginator">
|
<div class="hopframe-paginator">
|
||||||
<FluentButton BackgroundColor="transparent" OnClick="async () => await ChangePage(_currentPage - 1)">
|
<FluentButton BackgroundColor="transparent" OnClick="@(async () => await ChangePage(_currentPage - 1))">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size20.ArrowPrevious())" Color="Color.Neutral" />
|
<FluentIcon Value="@(new Icons.Regular.Size20.ArrowPrevious())" Color="Color.Neutral" />
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
|
|
||||||
@@ -114,12 +135,12 @@
|
|||||||
Items="Enumerable.Range(0, _totalPages)"
|
Items="Enumerable.Range(0, _totalPages)"
|
||||||
OptionValue="@(p => p.ToString())"
|
OptionValue="@(p => p.ToString())"
|
||||||
OptionText="@(p => (p + 1).ToString())"
|
OptionText="@(p => (p + 1).ToString())"
|
||||||
ValueChanged="async s => await ChangePage(Convert.ToInt32(s))"
|
ValueChanged="@(async s => await ChangePage(Convert.ToInt32(s)))"
|
||||||
Width="max-content" SelectedOption="@_currentPage"/>
|
Width="max-content" SelectedOption="@_currentPage"/>
|
||||||
|
|
||||||
<span>of @_totalPages</span>
|
<span>of @_totalPages</span>
|
||||||
|
|
||||||
<FluentButton BackgroundColor="transparent" OnClick="async () => await ChangePage(_currentPage + 1)">
|
<FluentButton BackgroundColor="transparent" OnClick="@(async () => await ChangePage(_currentPage + 1))">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size20.ArrowNext())" Color="Color.Neutral" />
|
<FluentIcon Value="@(new Icons.Regular.Size20.ArrowNext())" Color="Color.Neutral" />
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +174,7 @@
|
|||||||
|
|
||||||
<FluentToastProvider MaxToastCount="10" />
|
<FluentToastProvider MaxToastCount="10" />
|
||||||
|
|
||||||
<InputFile style="display: none" @ref="FileInputElement" OnChange="OnInputFiles"></InputFile>
|
<InputFile style="display: none" @ref="FileInputElement" OnChange="@(OnInputFiles)"></InputFile>
|
||||||
|
|
||||||
@inject IContextExplorer Explorer
|
@inject IContextExplorer Explorer
|
||||||
@inject NavigationManager Navigator
|
@inject NavigationManager Navigator
|
||||||
@@ -162,11 +183,15 @@
|
|||||||
@inject IHopFrameAuthHandler Handler
|
@inject IHopFrameAuthHandler Handler
|
||||||
@inject ICallbackEmitter Emitter
|
@inject ICallbackEmitter Emitter
|
||||||
@inject IPluginOrchestrator PluginOrchestrator
|
@inject IPluginOrchestrator PluginOrchestrator
|
||||||
|
@inject ISearchSuggestionProvider SearchSuggestions
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public required string TableDisplayName { get; set; }
|
public string TableDisplayName { get; set; } = null!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Type? TableType { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool DisplaySelection { get; set; }
|
public bool DisplaySelection { get; set; }
|
||||||
@@ -191,13 +216,14 @@
|
|||||||
private int _totalPages;
|
private int _totalPages;
|
||||||
private string? _searchTerm;
|
private string? _searchTerm;
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
|
private bool _isSearchActive;
|
||||||
|
private IList<string> _searchSuggestions = [];
|
||||||
|
private FluentSearch? _searchBox;
|
||||||
|
|
||||||
private bool _hasUpdatePolicy;
|
private bool _hasUpdatePolicy;
|
||||||
private bool _hasDeletePolicy;
|
private bool _hasDeletePolicy;
|
||||||
private bool _hasCreatePolicy;
|
private bool _hasCreatePolicy;
|
||||||
|
|
||||||
private bool _allSelected;
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource _tokenSource = new();
|
private readonly CancellationTokenSource _tokenSource = new();
|
||||||
private List<PluginButton> _pluginButtons = new();
|
private List<PluginButton> _pluginButtons = new();
|
||||||
private DefaultButtonToggles _buttonToggles = new();
|
private DefaultButtonToggles _buttonToggles = new();
|
||||||
@@ -206,6 +232,11 @@
|
|||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override void OnInitialized() {
|
||||||
CurrentInstance = this;
|
CurrentInstance = this;
|
||||||
|
|
||||||
|
if (TableType is not null) {
|
||||||
|
_config ??= Explorer.GetTable(TableType);
|
||||||
|
}
|
||||||
|
|
||||||
_config ??= Explorer.GetTable(TableDisplayName);
|
_config ??= Explorer.GetTable(TableDisplayName);
|
||||||
|
|
||||||
if (_config is null || (_config.Ignored && DialogData is null)) {
|
if (_config is null || (_config.Ignored && DialogData is null)) {
|
||||||
@@ -231,7 +262,7 @@
|
|||||||
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
||||||
|
|
||||||
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
||||||
CurrentlyDisplayedModels = await _manager!.LoadPage(_currentPage, PerPage).ToArrayAsync();
|
CurrentlyDisplayedModels = (await _manager!.LoadPage(_currentPage, PerPage)).ToArray();
|
||||||
_totalPages = await _manager.TotalPages(PerPage);
|
_totalPages = await _manager.TotalPages(PerPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +286,7 @@
|
|||||||
_searchTerm = eventArgs.Value?.ToString();
|
_searchTerm = eventArgs.Value?.ToString();
|
||||||
if (_searchTerm is null) return;
|
if (_searchTerm is null) return;
|
||||||
_searchCancel = new();
|
_searchCancel = new();
|
||||||
|
UpdateSearchSuggestions();
|
||||||
|
|
||||||
await Task.Delay(500, _searchCancel.Token);
|
await Task.Delay(500, _searchCancel.Token);
|
||||||
|
|
||||||
@@ -275,6 +307,36 @@
|
|||||||
await Reload();
|
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() {
|
public async Task Reload() {
|
||||||
_loading = true;
|
_loading = true;
|
||||||
|
|
||||||
@@ -388,14 +450,6 @@
|
|||||||
else DialogData!.SelectedObjects.Add(item);
|
else DialogData!.SelectedObjects.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectAll() {
|
|
||||||
var selected = CurrentlyDisplayedModels.All(DialogData!.SelectedObjects.Contains);
|
|
||||||
foreach (var displayedModel in CurrentlyDisplayedModels) {
|
|
||||||
SelectItem(displayedModel, !selected);
|
|
||||||
}
|
|
||||||
_allSelected = selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> DisplayProperty(PropertyConfig config, object entry) {
|
private async Task<string> DisplayProperty(PropertyConfig config, object entry) {
|
||||||
var display = await _manager!.DisplayProperty(entry, config);
|
var display = await _manager!.DisplayProperty(entry, config);
|
||||||
|
|
||||||
@@ -421,4 +475,5 @@
|
|||||||
public void RequestRender() {
|
public void RequestRender() {
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -20,3 +20,13 @@
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
border-bottom: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<PackageId>HopFrame.Web</PackageId>
|
<PackageId>HopFrame.Web</PackageId>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ public static class HopFrameConfiguratorExtensions {
|
|||||||
return configurator;
|
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) {
|
public static HopFrameConfigurator AddExporters(this HopFrameConfigurator configurator) {
|
||||||
configurator.AddPlugin<ExporterPlugin>();
|
configurator.AddPlugin<ExporterPlugin>();
|
||||||
return configurator;
|
return configurator;
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
|
|
||||||
[EventHandler]
|
[EventHandler]
|
||||||
public void OnInit(TableInitializedEvent e) {
|
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("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());
|
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
|
var data = await manager
|
||||||
.LoadPage(0, int.MaxValue)
|
.LoadPage(0, int.MaxValue);
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).ToArray();
|
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;
|
if (property is null) continue;
|
||||||
|
|
||||||
object? value = rowValues[i];
|
object? value = rowValues[i];
|
||||||
|
if (string.IsNullOrWhiteSpace((string)value)) continue;
|
||||||
|
|
||||||
if (property.IsEnumerable) {
|
if (property.IsEnumerable) {
|
||||||
if (!property.Info.PropertyType.IsGenericType) continue;
|
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);
|
var enumerable = Activator.CreateInstance(property.Info.PropertyType);
|
||||||
foreach (var key in values) {
|
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;
|
if (entry is null) continue;
|
||||||
|
|
||||||
addMethod.Invoke(enumerable, [entry]);
|
addMethod.Invoke(enumerable, [entry]);
|
||||||
@@ -116,7 +118,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
var relationManager = explorer.GetTableManager(property.Info.PropertyType);
|
var relationManager = explorer.GetTableManager(property.Info.PropertyType);
|
||||||
var relationPrimaryKeyType = GetPrimaryKeyType(property.Info.PropertyType);
|
var relationPrimaryKeyType = GetPrimaryKeyType(property.Info.PropertyType);
|
||||||
if (relationManager is null || relationPrimaryKeyType is null) continue;
|
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)) {
|
else if (property.Info.PropertyType == typeof(Guid)) {
|
||||||
var success = Guid.TryParse((string)value, out var guid);
|
var success = Guid.TryParse((string)value, out var guid);
|
||||||
@@ -151,13 +153,20 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
|
|
||||||
if (property.IsEnumerable) {
|
if (property.IsEnumerable) {
|
||||||
var enumerable = (IEnumerable)value;
|
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
|
return entity
|
||||||
.GetType()
|
.GetType()
|
||||||
.GetProperties()
|
.GetProperties()
|
||||||
@@ -169,6 +178,11 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Type? GetPrimaryKeyType(Type tableType) {
|
private Type? GetPrimaryKeyType(Type tableType) {
|
||||||
|
var table = explorer.GetTable(tableType);
|
||||||
|
if (table?.ContextConfig is RepositoryGroupConfig repoConfig) {
|
||||||
|
return repoConfig.KeyProperty.PropertyType;
|
||||||
|
}
|
||||||
|
|
||||||
return tableType
|
return tableType
|
||||||
.GetProperties()
|
.GetProperties()
|
||||||
.FirstOrDefault(prop => prop
|
.FirstOrDefault(prop => prop
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using HopFrame.Web.Services.Implementation;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.FluentUI.AspNetCore.Components;
|
using Microsoft.FluentUI.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
namespace HopFrame.Web;
|
namespace HopFrame.Web;
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ public static class ServiceCollectionExtensions {
|
|||||||
|
|
||||||
services.AddScoped<IPluginOrchestrator, PluginOrchestrator>();
|
services.AddScoped<IPluginOrchestrator, PluginOrchestrator>();
|
||||||
services.AddScoped<IFileService, FileService>();
|
services.AddScoped<IFileService, FileService>();
|
||||||
|
services.TryAddScoped<ISearchSuggestionProvider, SearchSuggestionProvider>();
|
||||||
|
|
||||||
if (addRazorComponents) {
|
if (addRazorComponents) {
|
||||||
services.AddRazorComponents()
|
services.AddRazorComponents()
|
||||||
|
|||||||
@@ -2,10 +2,22 @@
|
|||||||
|
|
||||||
namespace HopFrame.Web.Services;
|
namespace HopFrame.Web.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides file handling capabilities for downloading and uploading files.
|
||||||
|
/// </summary>
|
||||||
public interface IFileService {
|
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);
|
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();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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 string? LastName { get; set; }
|
||||||
|
|
||||||
public virtual List<Post> Posts { get; set; } = new();
|
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.Testing.Models;
|
||||||
using HopFrame.Web;
|
using HopFrame.Web;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Message = HopFrame.Testing.Models.Message;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -50,9 +51,13 @@ builder.Services.AddHopFrame(options => {
|
|||||||
.FormatEach<Post>((post, _) => post.Caption);
|
.FormatEach<Post>((post, _) => post.Caption);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context.Table<Post>()
|
||||||
|
.ShowSearchSuggestions(false);
|
||||||
|
|
||||||
context.Table<Post>()
|
context.Table<Post>()
|
||||||
.Property(p => p.Author)
|
.Property(p => p.Author)
|
||||||
.Format((user, _) => $"{user.FirstName} {user.LastName}")
|
//.Format((user, _) => $"{user.FirstName} {user.LastName}")
|
||||||
|
.SetDisplayedProperty(u => u.Username)
|
||||||
.SetValidator((_, _) => []);
|
.SetValidator((_, _) => []);
|
||||||
|
|
||||||
context.Table<Post>()
|
context.Table<Post>()
|
||||||
@@ -88,8 +93,31 @@ builder.Services.AddHopFrame(options => {
|
|||||||
.SetPolicy("counter.view");
|
.SetPolicy("counter.view");
|
||||||
|
|
||||||
options.AddExporters();
|
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using HopFrame.Core.Config;
|
using HopFrame.Core.Config;
|
||||||
|
using HopFrame.Core.Services;
|
||||||
using HopFrame.Core.Services.Implementations;
|
using HopFrame.Core.Services.Implementations;
|
||||||
using HopFrame.Tests.Core.Models;
|
using HopFrame.Tests.Core.Models;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
||||||
@@ -121,6 +123,7 @@ public class ContextExplorerTests {
|
|||||||
var dbContext = new MockDbContext();
|
var dbContext = new MockDbContext();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
provider.Setup(p => p.GetService(typeof(MockDbContext))).Returns(dbContext);
|
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()));
|
var contextExplorer = new ContextExplorer(config, provider.Object, new Logger<ContextExplorer>(new LoggerFactory()));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class DisplayPropertyTests {
|
|||||||
_explorerMock = new Mock<IContextExplorer>();
|
_explorerMock = new Mock<IContextExplorer>();
|
||||||
_config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
_config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
_tableManager =
|
_tableManager =
|
||||||
new TableManager<object>(contextMock.Object, _config, _explorerMock.Object, _providerMock.Object);
|
new TableManager<object>(contextMock.Object, _config, _explorerMock.Object, _providerMock.Object, new SearchExpressionBuilder(_explorerMock.Object));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ using Moq;
|
|||||||
namespace HopFrame.Tests.Core.Services;
|
namespace HopFrame.Tests.Core.Services;
|
||||||
|
|
||||||
public class TableManagerTests {
|
public class TableManagerTests {
|
||||||
|
|
||||||
|
private Mock<ISearchExpressionBuilder> _searchBuilderMock = new();
|
||||||
|
|
||||||
private Mock<DbContext> CreateMockDbContext<TModel>(List<TModel> data) where TModel : class {
|
private Mock<DbContext> CreateMockDbContext<TModel>(List<TModel> data) where TModel : class {
|
||||||
var dbContext = new Mock<DbContext>();
|
var dbContext = new Mock<DbContext>();
|
||||||
var dbSet = CreateMockDbSet(data);
|
var dbSet = CreateMockDbSet(data);
|
||||||
@@ -40,7 +43,7 @@ public class TableManagerTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LoadPage_ReturnsPagedData() {
|
public async Task LoadPage_ReturnsPagedData() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var data = new List<MockModel> {
|
var data = new List<MockModel> {
|
||||||
new MockModel { Id = 1, Name = "Item1" },
|
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 config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object, _searchBuilderMock.Object);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = manager.LoadPage(1, 2).ToList();
|
var result = (await manager.LoadPage(1, 2)).ToArray();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Single(result);
|
Assert.Single(result);
|
||||||
Assert.Equal("Item3", ((MockModel)result[0]).Name);
|
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]
|
[Fact]
|
||||||
public async Task TotalPages_ReturnsCorrectPageCount() {
|
public async Task TotalPages_ReturnsCorrectPageCount() {
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -99,7 +76,7 @@ public class TableManagerTests {
|
|||||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext, config, explorer.Object, provider.Object, _searchBuilderMock.Object);
|
||||||
await dbContext.Models.AddRangeAsync(data);
|
await dbContext.Models.AddRangeAsync(data);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
@@ -121,7 +98,7 @@ public class TableManagerTests {
|
|||||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object, _searchBuilderMock.Object);
|
||||||
var item = data.First();
|
var item = data.First();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -142,7 +119,7 @@ public class TableManagerTests {
|
|||||||
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object, _searchBuilderMock.Object);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await manager.EditItem(data.First());
|
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 config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||||
var explorer = new Mock<IContextExplorer>();
|
var explorer = new Mock<IContextExplorer>();
|
||||||
var provider = new Mock<IServiceProvider>();
|
var provider = new Mock<IServiceProvider>();
|
||||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object, _searchBuilderMock.Object);
|
||||||
var newItem = new MockModel { Id = 3, Name = "NewItem" };
|
var newItem = new MockModel { Id = 3, Name = "NewItem" };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public class HopFrameTablePageTests : TestContext {
|
|||||||
contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig);
|
contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig);
|
||||||
contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(managerMock.Object);
|
contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(managerMock.Object);
|
||||||
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true);
|
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.AddHopFrame(config, null, false);
|
||||||
Services.AddSingleton(contextExplorerMock.Object);
|
Services.AddSingleton(contextExplorerMock.Object);
|
||||||
@@ -71,7 +71,7 @@ public class HopFrameTablePageTests : TestContext {
|
|||||||
|
|
||||||
var tableManagerMock = new Mock<ITableManager>();
|
var tableManagerMock = new Mock<ITableManager>();
|
||||||
var items = new List<object> { new MyTable(), new MyTable() };
|
var items = new List<object> { new MyTable(), new MyTable() };
|
||||||
tableManagerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(items.AsAsyncQueryable());
|
tableManagerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync(items);
|
||||||
tableManagerMock.Setup(t => t.DisplayProperty(It.IsAny<object>(), It.IsAny<PropertyConfig>(), null, null))
|
tableManagerMock.Setup(t => t.DisplayProperty(It.IsAny<object>(), It.IsAny<PropertyConfig>(), null, null))
|
||||||
.ReturnsAsync(string.Empty);
|
.ReturnsAsync(string.Empty);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user