Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d393ae787d | |||
| 10913b0a21 | |||
| 31b0b3970a | |||
| e6726037b6 | |||
| c5388fc044 | |||
| 66d03513eb | |||
| 68a4479c2d | |||
| 5dec609004 | |||
| 7d3aa6de94 | |||
| 5c6fafcd6f | |||
| 222d4276d2 | |||
| 4407d173a9 | |||
| 7e5d50b1c9 | |||
| 18937f9275 | |||
| 2fdd305cd7 | |||
| 6bc2479984 | |||
| b9c34a85df | |||
| e773871dc0 | |||
| 86ace64618 | |||
| 6c42008a28 | |||
| 0262b3b97b | |||
| 84c37012ec | |||
| 56d45575f8 | |||
| 9a66f88f3c | |||
| 93d41ad6d3 | |||
| 08d4ddb2c6 | |||
| 47f30bf33f | |||
| 8db0f84a80 | |||
| 46f14d3ddb | |||
| 43fda30a01 | |||
| 23c5115c99 | |||
| fb761c74d2 | |||
| 13e9af892c | |||
| 4cfeaab652 | |||
| 2256a59f9a | |||
| bfea4e9cff | |||
| 7ce066df7b | |||
| 39641f18a8 | |||
| 966ced57d6 | |||
| ec3ab67cb9 | |||
| d802fde7d8 | |||
| 88d843c1cb | |||
| fecbc0717b | |||
| 5a342e2c53 | |||
| e553d47841 | |||
| d09264d700 | |||
| 9e931c77e0 | |||
| c8a342986b | |||
| 62e4daf60d | |||
| ac320d7445 | |||
| 193f334708 | |||
| b288d58c5d | |||
| b6a7c508db | |||
| d42f024175 | |||
| 2f15986dbf | |||
| 6842e48a70 | |||
| fd71767271 |
@@ -4,13 +4,12 @@ stages:
|
||||
- build
|
||||
- test
|
||||
- publish
|
||||
|
||||
before_script:
|
||||
- echo "Setting up environment"
|
||||
- 'dotnet --version'
|
||||
- publish-help
|
||||
|
||||
build:
|
||||
stage: build
|
||||
only:
|
||||
- pushes
|
||||
script:
|
||||
- dotnet restore
|
||||
- dotnet build --configuration Release --no-restore
|
||||
@@ -21,10 +20,31 @@ build:
|
||||
|
||||
test:
|
||||
stage: test
|
||||
only:
|
||||
- pushes
|
||||
script:
|
||||
- dotnet test --verbosity normal
|
||||
dependencies:
|
||||
- build
|
||||
- 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
|
||||
@@ -37,3 +57,19 @@ publish:
|
||||
dependencies:
|
||||
- build
|
||||
- test
|
||||
|
||||
publish-help:
|
||||
stage: publish-help
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
|
||||
- cd docs
|
||||
- docker login -u leon.hoppe -p ${CI_REGISTRY_PASSWORD} registry.leon-hoppe.de
|
||||
- docker build -t registry.leon-hoppe.de/leon.hoppe/hopframe:$VERSION -t registry.leon-hoppe.de/leon.hoppe/hopframe:latest .
|
||||
- docker push registry.leon-hoppe.de/leon.hoppe/hopframe:$VERSION
|
||||
- docker push registry.leon-hoppe.de/leon.hoppe/hopframe:latest
|
||||
only:
|
||||
- tags
|
||||
dependencies:
|
||||
- publish
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="RiderProjectSettingsUpdater">
|
||||
<option name="singleClickDiffPreview" value="1" />
|
||||
<option name="unhandledExceptionsIgnoreList" value="1" />
|
||||
<option name="vcsConfiguration" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
730
.idea/.idea.HopFrame/.idea/workspace.xml
generated
730
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -3,17 +3,19 @@
|
||||
<component name="AutoGeneratedRunConfigurationManager">
|
||||
<projectFile profileName="http">HopFrame.Testing/HopFrame.Testing.csproj</projectFile>
|
||||
<projectFile profileName="https">HopFrame.Testing/HopFrame.Testing.csproj</projectFile>
|
||||
<projectFile profileName="http">testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj</projectFile>
|
||||
<projectFile profileName="https">testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj</projectFile>
|
||||
<projectFile profileName="http">testing/HopFrame.Testing/HopFrame.Testing.csproj</projectFile>
|
||||
</component>
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="prepared project for release">
|
||||
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.gitlab-ci.yml" beforeDir="false" afterPath="$PROJECT_DIR$/.gitlab-ci.yml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/HopFrame.Core.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/HopFrame.Core.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/HopFrame.Web.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/HopFrame.Web.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -37,6 +39,7 @@
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
<option name="RESET_MODE" value="HARD" />
|
||||
</component>
|
||||
<component name="GitLabMergeRequestFiltersHistory">{
|
||||
"lastFilter": {
|
||||
@@ -51,67 +54,111 @@
|
||||
<component name="GitLabMergeRequestsSettings">{
|
||||
"selectedUrlAndAccountId": {
|
||||
"first": "https://git.leon-hoppe.de/leon.hoppe/hopframe.git",
|
||||
"second": "2d65fdcb-5f13-45ad-a7ba-91dd4a88d6e4"
|
||||
"second": "f58c9371-9f54-454e-a0db-5b4bc1187bad"
|
||||
}
|
||||
}</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/02/6ae7626a/IList.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/5b/a350be00/IEnumerable.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/62/1fb63ed0/IDisposable.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/8b/db8582a3/IList`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/a0/0a968c53/IEnumerable`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/ad/ba9a50e7/ICollection.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/fc/6f7933d2/ICollection`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/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/1b81cb3be224213a6a73519b6e340a628d9a1fb8629c351a186a26f6376669/List.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/24dd1164ba47541cb1d3eb011e638e16953dbea3ae3f4dc208c3bbf3e96298a/ServiceCollectionServiceExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/26c9a2fb5243863babc926e4be763daf4128d4f97c4a769cdce1e2e3e5c532/FluentButton.razor.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/2751d5afefca5424bfc4b21347f581372f7a739c0ae4df661ea557fcb97ef20/EnumExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/2a4d2ce4c06ab596b3676c5cf06066b4391ec7dd93cdf8f0334b69dc1a9de/TextReader.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/439c4ee753b23e743cc14119593bc889751f9eb0b38997577d8e4c47c4fed/ToCollection.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/4c41a7d338749915157d56585365d1693fbad6be8231d3d583b1cf10d16896d9/FluentIcon.razor.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/4ee221fd7e91e9a4c14ff82aae2ee938edecde35a934133e991aba56aa9499/Icon.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/558c1d46e1e21d2e78ee2ab67a674f6927bf95355b2f245f35d74bb5ec0f92/CancellationTokenSource.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/5a69b82eed595b731b82667db08722b69b82482e275cf32dfb219190e3dc49/CollectionEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/60e7b22380df80ef6fefe43138047f49ec6eff4b25c12b42ce3d6ed5aac/MethodInvokerCommon.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6354a7b35d7821629924d3676acd7e67a6f7f94343e0e66ec439aa2bd6ed5/ThrowHelper.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/642391624bd5c30b3411a11434588aba4906207335166b784bf3a4325f6c7/NavigationEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6d1d64f05e7045295fa180276a8c2aef0302c9e96eb53b3431ab13db4579/FluentAppBarItem.razor.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6fe785cceb29ca2d1da78e157315815a7c4372b582a20a71c28b210f9d56e/IconsExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/7ad7d2d0ae865063993eb8a03427815ea3bdb6a774e0a2f95512e9f669a4f489/MemberEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/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/8d5d6cbff46ddc7b152381f92ae1ae51d3e7b57b14dd23840a11f5aaaaed396/InternalEntityEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a882d183338544fdbcbdfc7b6d3dcb78916630765551644a221b5be9c45a121b/Int32.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/aa3ea54f92373c58ec1149fbd41215869a98bd385c30584bc6db2fa3c6e88443/Filled24.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/adcd2c45092dd8e4fc412325c8adb75d6e7d8b3e90a9523f167583fb9c60/ServiceCollectionExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/b3ccb66df3646cb51df73ad51716136ebd2eefb4edb1308dd52a7e999582d59e/IBindableColumn.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/bfff78ecaa39c818519fc918bb2d4bbdca6ad93d7170f5cf325f67ccd0b97d43/BooleanAsserts.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d0165cb640e16fb3b8fe6932c042fc2917cd7f2770ff123cf7b9d11b5bfc6/Task.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d04a416cac8afac0341a8be0e859b230f2eae64924298eef48c317ba35916/RenderTreeBuilder.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d1287462d4ec4078c61b8e92a0952fb7de3e7e877d279e390a4c136a6365126/Stream.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d39923abb31e6a6e7a9e8173e217da584c54925ce63e568126a2b89b9ab/DefaultRazorComponentsServiceOptionsConfiguration.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d858ddb35a8e36df5573b7612542f9ad50f426b8ab43818587d1ac65fab14829/DatabaseGeneratedAttribute.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/dac3553c90d47a746e7e7f02faecb1a5e581090/Components_AppBar_FluentAppBarItem_razor.g.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/e26a4f2df232f16e374b9719f883c1b2419f6341838d94b7581db9c7d2de17/IconInfo.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/eab2d6b892f743a27cb49a139ba782855897baf1233febd2dfd2092f3/EntityEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ece8533187fe96ce67b3ef1c9cc3502ef8da5510aadb132a9b21c5605d7c2119/PropertyColumn.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ee4d234452e240d83e3de396c2e85cbf9ac9fb9add618b955eea196c81aaf8/IDialogContentComponent.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/fc2027f7e776fc105cddb56b1a25eeb3895b3ae6f3aac854d786e63bd01f75e2/CallSiteFactory.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/fd57398b7dc3a8ce7da2786f2c67289c3d974658a9e90d0c1e84db3d965fbf1/Console.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ff37d54b3bf4d2756237fb789635831532603376e940f63d634b869d26d74c/Regular16.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
</component>
|
||||
<component name="KubernetesApiPersistence">{}</component>
|
||||
<component name="KubernetesApiProvider">{
|
||||
"isMigrated": true
|
||||
}</component>
|
||||
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 3
|
||||
}</component>
|
||||
<component name="ProjectId" id="2raEUZtlphkj04rfRNtlQw6fPMU" />
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
||||
<component name="ProjectLevelVcsManager">
|
||||
<OptionsSetting value="false" id="Update" />
|
||||
<ConfirmationsSetting value="2" id="Add" />
|
||||
</component>
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
|
||||
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
|
||||
".NET Project.HopFrame.Testing.executor": "Run",
|
||||
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
||||
"git-widget-placeholder": "release/v3.0.0",
|
||||
"list.type.of.created.stylesheet": "CSS",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "preferences.environmentSetup",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run",
|
||||
".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
|
||||
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
|
||||
".NET Project.HopFrame.Testing.executor": "Run",
|
||||
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
||||
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
||||
"git-widget-placeholder": "dev",
|
||||
"list.type.of.created.stylesheet": "CSS",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.HopFrame.Testing: https">
|
||||
<configuration name="HopFrame.Testing: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
@@ -120,18 +167,53 @@
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="HopFrame.Testing.Api: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="HopFrame.Testing.Api: https" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<option name="MIXED_MODE_DEBUG" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<list>
|
||||
<item itemvalue=".NET Launch Settings Profile.HopFrame.Testing.Api: http" />
|
||||
<item itemvalue=".NET Launch Settings Profile.HopFrame.Testing.Api: https" />
|
||||
<item itemvalue=".NET Launch Settings Profile.HopFrame.Testing: https" />
|
||||
<item itemvalue=".NET Launch Settings Profile.HopFrame.Testing: http" />
|
||||
</list>
|
||||
@@ -159,127 +241,45 @@
|
||||
<workItem from="1737199714142" duration="8344000" />
|
||||
<workItem from="1737208313207" duration="4612000" />
|
||||
<workItem from="1737281957060" duration="3232000" />
|
||||
<workItem from="1737293153907" duration="7750000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Added basic configuration">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736850899254</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736850899254</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="Added admin page navigation">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736855209077</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736855209077</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00003" summary="Added database loading logic">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736859917232</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736859917232</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00004" summary="Started working on listing page">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736885531216</created>
|
||||
<option name="number" value="00004" />
|
||||
<option name="presentableId" value="LOCAL-00004" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736885531216</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00005" summary="Added entry saving support">
|
||||
<option name="closed" value="true" />
|
||||
<created>1736970238802</created>
|
||||
<option name="number" value="00005" />
|
||||
<option name="presentableId" value="LOCAL-00005" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1736970238802</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00006" summary="Added reload button and animation">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737023058093</created>
|
||||
<option name="number" value="00006" />
|
||||
<option name="presentableId" value="LOCAL-00006" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737023058093</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00007" summary="Added relation picker dialog">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737035288104</created>
|
||||
<option name="number" value="00007" />
|
||||
<option name="presentableId" value="LOCAL-00007" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737035288104</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00008" summary="Added automatic relation mapping">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737037853482</created>
|
||||
<option name="number" value="00008" />
|
||||
<option name="presentableId" value="LOCAL-00008" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737037853482</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00009" summary="Added property validation">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737040612038</created>
|
||||
<option name="number" value="00009" />
|
||||
<option name="presentableId" value="LOCAL-00009" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737040612038</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00010" summary="Added creation/modification confirmation">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737040946489</created>
|
||||
<option name="number" value="00010" />
|
||||
<option name="presentableId" value="LOCAL-00010" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737040946489</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00011" summary="Removed Template">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737042229086</created>
|
||||
<option name="number" value="00011" />
|
||||
<option name="presentableId" value="LOCAL-00011" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737042229086</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00012" summary="Added policy validation, ordering and virtual listing properties">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737055409534</created>
|
||||
<option name="number" value="00012" />
|
||||
<option name="presentableId" value="LOCAL-00012" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737055409535</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00013" summary="Added n -> m relation support">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737129518866</created>
|
||||
<option name="number" value="00013" />
|
||||
<option name="presentableId" value="LOCAL-00013" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737129518866</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00014" summary="Added text area support and DI support for modifier functions">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737202192471</created>
|
||||
<option name="number" value="00014" />
|
||||
<option name="presentableId" value="LOCAL-00014" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737202192471</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00015" summary="Addressed all build warnings">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737203441319</created>
|
||||
<option name="number" value="00015" />
|
||||
<option name="presentableId" value="LOCAL-00015" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737203441319</updated>
|
||||
<workItem from="1737293153907" duration="8953000" />
|
||||
<workItem from="1737390240714" duration="60000" />
|
||||
<workItem from="1737390360987" duration="601000" />
|
||||
<workItem from="1737993570961" duration="4163000" />
|
||||
<workItem from="1738054766160" duration="7449000" />
|
||||
<workItem from="1738075629332" duration="8862000" />
|
||||
<workItem from="1738335286481" duration="2039000" />
|
||||
<workItem from="1738403493974" duration="4231000" />
|
||||
<workItem from="1738418482606" duration="2795000" />
|
||||
<workItem from="1738421294144" duration="1651000" />
|
||||
<workItem from="1738422949337" duration="141000" />
|
||||
<workItem from="1738512801911" duration="6776000" />
|
||||
<workItem from="1738769458367" duration="5256000" />
|
||||
<workItem from="1738774834563" duration="728000" />
|
||||
<workItem from="1739301922710" duration="33000" />
|
||||
<workItem from="1739352479748" duration="3047000" />
|
||||
<workItem from="1739369355001" duration="1751000" />
|
||||
<workItem from="1739461452173" duration="5533000" />
|
||||
<workItem from="1739550750776" duration="3613000" />
|
||||
<workItem from="1739617785048" duration="5992000" />
|
||||
<workItem from="1739975843065" duration="1921000" />
|
||||
<workItem from="1740168829540" duration="1382000" />
|
||||
<workItem from="1740595969750" duration="34000" />
|
||||
<workItem from="1740736919561" duration="191000" />
|
||||
<workItem from="1740738257628" duration="3216000" />
|
||||
<workItem from="1740741585276" duration="17000" />
|
||||
<workItem from="1740742098571" duration="78000" />
|
||||
<workItem from="1740742471317" duration="672000" />
|
||||
<workItem from="1741974241977" duration="10854000" />
|
||||
<workItem from="1742038098473" duration="990000" />
|
||||
<workItem from="1742059898156" duration="3488000" />
|
||||
<workItem from="1744725284649" duration="60000" />
|
||||
<workItem from="1744916016381" duration="66000" />
|
||||
<workItem from="1744916106166" duration="49000" />
|
||||
<workItem from="1744966207145" duration="5231000" />
|
||||
<workItem from="1751713720880" duration="8243000" />
|
||||
<workItem from="1751741813788" duration="4623000" />
|
||||
<workItem from="1768753475773" duration="455000" />
|
||||
<workItem from="1768753946559" duration="344000" />
|
||||
</task>
|
||||
<task id="LOCAL-00016" summary="Added documentation for the configurators and service extensions methods">
|
||||
<option name="closed" value="true" />
|
||||
@@ -329,7 +329,351 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737300408069</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="22" />
|
||||
<task id="LOCAL-00022" summary="Included readme file in projects">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737301230493</created>
|
||||
<option name="number" value="00022" />
|
||||
<option name="presentableId" value="LOCAL-00022" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737301230493</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00023" summary="Added missing files">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737994074137</created>
|
||||
<option name="number" value="00023" />
|
||||
<option name="presentableId" value="LOCAL-00023" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737994074137</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00024" summary="Added a simple web api abstraction method">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737995782424</created>
|
||||
<option name="number" value="00024" />
|
||||
<option name="presentableId" value="LOCAL-00024" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737995782424</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00025" summary="Implemented async delegates">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737997122807</created>
|
||||
<option name="number" value="00025" />
|
||||
<option name="presentableId" value="LOCAL-00025" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737997122807</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00026" summary="Added maximum display length">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738055497527</created>
|
||||
<option name="number" value="00026" />
|
||||
<option name="presentableId" value="LOCAL-00026" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738055497527</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00027" summary="Fixed test for table view">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738055822074</created>
|
||||
<option name="number" value="00027" />
|
||||
<option name="presentableId" value="LOCAL-00027" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738055822074</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00028" summary="Added n-m relation mapping">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738062559567</created>
|
||||
<option name="number" value="00028" />
|
||||
<option name="presentableId" value="LOCAL-00028" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738062559567</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00029" summary="Fixed wrong element selection for action buttons">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738063028173</created>
|
||||
<option name="number" value="00029" />
|
||||
<option name="presentableId" value="LOCAL-00029" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738063028173</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00030" summary="Implemented primitive change reversion">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738079122848</created>
|
||||
<option name="number" value="00030" />
|
||||
<option name="presentableId" value="LOCAL-00030" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738079122848</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00031" summary="Implemented deferred entry manipulation">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738084259089</created>
|
||||
<option name="number" value="00031" />
|
||||
<option name="presentableId" value="LOCAL-00031" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738084259089</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00032" summary="Removed select all button">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738337068205</created>
|
||||
<option name="number" value="00032" />
|
||||
<option name="presentableId" value="LOCAL-00032" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738337068205</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00033" summary="Added missing installation instructions">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738337314351</created>
|
||||
<option name="number" value="00033" />
|
||||
<option name="presentableId" value="LOCAL-00033" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738337314351</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00034" summary="Added modular event system">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738407061976</created>
|
||||
<option name="number" value="00034" />
|
||||
<option name="presentableId" value="LOCAL-00034" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738407061976</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00035" summary="Fixed event emitter service scope">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738407710507</created>
|
||||
<option name="number" value="00035" />
|
||||
<option name="presentableId" value="LOCAL-00035" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738407710507</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00036" summary="Added custom views">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738422931038</created>
|
||||
<option name="number" value="00036" />
|
||||
<option name="presentableId" value="LOCAL-00036" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738422931038</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00037" summary="Added plugin events">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738519603597</created>
|
||||
<option name="number" value="00037" />
|
||||
<option name="presentableId" value="LOCAL-00037" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738519603597</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00038" summary="Passed cancellation tokens to event handlers if needed">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738770468949</created>
|
||||
<option name="number" value="00038" />
|
||||
<option name="presentableId" value="LOCAL-00038" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738770468949</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00039" summary="Added plugin buttons">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738773315593</created>
|
||||
<option name="number" value="00039" />
|
||||
<option name="presentableId" value="LOCAL-00039" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738773315593</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00040" summary="Added default button removal feature">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738774569657</created>
|
||||
<option name="number" value="00040" />
|
||||
<option name="presentableId" value="LOCAL-00040" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738774569657</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00041" summary="Added custom search functionality">
|
||||
<option name="closed" value="true" />
|
||||
<created>1738775556256</created>
|
||||
<option name="number" value="00041" />
|
||||
<option name="presentableId" value="LOCAL-00041" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1738775556256</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00042" summary="Added fully virtual properties">
|
||||
<option name="closed" value="true" />
|
||||
<created>1739554261551</created>
|
||||
<option name="number" value="00042" />
|
||||
<option name="presentableId" value="LOCAL-00042" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1739554261551</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00043" summary="Added basic export and import feature">
|
||||
<option name="closed" value="true" />
|
||||
<created>1739623781007</created>
|
||||
<option name="number" value="00043" />
|
||||
<option name="presentableId" value="LOCAL-00043" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1739623781007</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00044" summary="Finished converter plugin">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740741334420</created>
|
||||
<option name="number" value="00044" />
|
||||
<option name="presentableId" value="LOCAL-00044" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740741334420</updated>
|
||||
</task>
|
||||
<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>
|
||||
<option name="localTasksCounter" value="65" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -339,38 +683,17 @@
|
||||
<option name="coveragePercentColumnWidth" value="129" />
|
||||
<option name="sortOrder" value="DESCENDING" />
|
||||
<option name="sortedColumn" value="1" />
|
||||
<option name="symbolColumnWidth" value="451" />
|
||||
<option name="symbolColumnWidth" value="559" />
|
||||
<coverage-tree-state>
|
||||
<expand>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 32% 1123/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Total 53% 1150/2443" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 32% 1123/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 46% 398/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 32% 1123/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 46% 398/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 32% 1123/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 46% 398/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 32% 1123/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 46% 398/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Config 93% 17/228" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Total 53% 1150/2443" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 40% 862/1439" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
</expand>
|
||||
<select />
|
||||
@@ -378,29 +701,44 @@
|
||||
</component>
|
||||
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||
<MESSAGE value="Added basic configuration" />
|
||||
<MESSAGE value="Added admin page navigation" />
|
||||
<MESSAGE value="Added database loading logic" />
|
||||
<MESSAGE value="Started working on listing page" />
|
||||
<MESSAGE value="Added entry saving support" />
|
||||
<MESSAGE value="Added reload button and animation" />
|
||||
<MESSAGE value="Added relation picker dialog" />
|
||||
<MESSAGE value="Added automatic relation mapping" />
|
||||
<MESSAGE value="Added property validation" />
|
||||
<MESSAGE value="Added creation/modification confirmation" />
|
||||
<MESSAGE value="Removed Template" />
|
||||
<MESSAGE value="Added policy validation, ordering and virtual listing properties" />
|
||||
<MESSAGE value="Added n -> m relation support" />
|
||||
<MESSAGE value="Added text area support and DI support for modifier functions" />
|
||||
<MESSAGE value="Addressed all build warnings" />
|
||||
<MESSAGE value="Added documentation for the configurators and service extensions methods" />
|
||||
<MESSAGE value="Created tests for the core module" />
|
||||
<MESSAGE value="Added more tests" />
|
||||
<MESSAGE value="Added web module tests" />
|
||||
<MESSAGE value="Tested login functionality" />
|
||||
<MESSAGE value="prepared project for release" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="prepared project for release" />
|
||||
<MESSAGE value="Added default button removal feature" />
|
||||
<MESSAGE value="Added custom search functionality" />
|
||||
<MESSAGE value="Added fully virtual properties" />
|
||||
<MESSAGE value="Added basic export and import feature" />
|
||||
<MESSAGE value="Finished converter plugin" />
|
||||
<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" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="fixed echo cmd" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Core", "test
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Web", "tests\HopFrame.Tests.Web\HopFrame.Tests.Web.csproj", "{7AB4F4FF-E938-4A40-A7EB-7B2063262896}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Api", "testing\HopFrame.Testing.Api\HopFrame.Testing.Api.csproj", "{B13D2C4E-3993-47CD-A525-FD0B83980F0A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -27,6 +29,7 @@ Global
|
||||
{58490069-51DF-454C-8B54-7FB7D4BDFF81} = {9EB7FDBD-49C2-4872-9666-6F7AEBA541B2}
|
||||
{2E2D29E0-53FA-462D-B4D2-4678CD106E29} = {141928CB-5977-4285-A986-5BD785F2883C}
|
||||
{7AB4F4FF-E938-4A40-A7EB-7B2063262896} = {141928CB-5977-4285-A986-5BD785F2883C}
|
||||
{B13D2C4E-3993-47CD-A525-FD0B83980F0A} = {9EB7FDBD-49C2-4872-9666-6F7AEBA541B2}
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4BFE21C2-EAAC-4662-8B97-500836651B2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
@@ -49,5 +52,9 @@ Global
|
||||
{7AB4F4FF-E938-4A40-A7EB-7B2063262896}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7AB4F4FF-E938-4A40-A7EB-7B2063262896}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7AB4F4FF-E938-4A40-A7EB-7B2063262896}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B13D2C4E-3993-47CD-A525-FD0B83980F0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B13D2C4E-3993-47CD-A525-FD0B83980F0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B13D2C4E-3993-47CD-A525-FD0B83980F0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B13D2C4E-3993-47CD-A525-FD0B83980F0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
16
README.md
16
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
Welcome to the **HopFrame**! This project aims to provide a comprehensive and modular framework for easy management of your database.
|
||||
The framework is designed to be highly configurable, ensuring that developers either quickly add the framework for simple data editing or
|
||||
configure it to their needs to implement it fully in their data management pipeline.
|
||||
configure it to their needs to implement it fully in their data management pipeline. Read more in the project [docs](https://hopframe.leon-hoppe.de).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -16,6 +16,14 @@ configure it to their needs to implement it fully in their data management pipel
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
Install the nuget package using the CLI or the UI of your IDE:
|
||||
|
||||
```bash
|
||||
dotnet add package HopFrame.Web
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Configuring HopFrame is straightforward and flexible. You can easily define your contexts, tables, and their properties using the provided configurators.
|
||||
@@ -72,6 +80,12 @@ builder.Services.AddHopFrame(options => {
|
||||
});
|
||||
```
|
||||
|
||||
Then you need to map the frontend pages in your application:
|
||||
|
||||
```csharp
|
||||
app.MapHopFrame();
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
- Navigate to `/admin` to access the admin dashboard and start managing your tables.
|
||||
|
||||
3
docs/.idea/.gitignore
generated
vendored
Normal file
3
docs/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
8
docs/.idea/docs.iml
generated
Normal file
8
docs/.idea/docs.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
docs/.idea/modules.xml
generated
Normal file
8
docs/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/docs.iml" filepath="$PROJECT_DIR$/.idea/docs.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
docs/.idea/vcs.xml
generated
Normal file
6
docs/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
19
docs/Dockerfile
Normal file
19
docs/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM jetbrains/writerside-builder:243.22562 AS build
|
||||
|
||||
ARG INSTANCE=Writerside/hopframe
|
||||
|
||||
RUN mkdir /opt/sources
|
||||
|
||||
WORKDIR /opt/sources
|
||||
|
||||
ADD Writerside ./Writerside
|
||||
|
||||
RUN export DISPLAY=:99 && Xvfb :99 & /opt/builder/bin/idea.sh helpbuilderinspect --source-dir /opt/sources --product $INSTANCE --runner other --output-dir /opt/wrs-output/
|
||||
|
||||
WORKDIR /opt/wrs-output
|
||||
|
||||
RUN unzip -O UTF-8 webHelpHOPFRAME2-all.zip -d /opt/wrs-output/unzipped-artifact
|
||||
|
||||
FROM httpd:2.4 AS http-server
|
||||
|
||||
COPY --from=build /opt/wrs-output/unzipped-artifact/ /usr/local/apache2/htdocs/
|
||||
6
docs/Writerside/c.list
Normal file
6
docs/Writerside/c.list
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE categories
|
||||
SYSTEM "https://resources.jetbrains.com/writerside/1.0/categories.dtd">
|
||||
<categories>
|
||||
<category id="wrs" name="Writerside documentation" order="1"/>
|
||||
</categories>
|
||||
13
docs/Writerside/cfg/buildprofiles.xml
Normal file
13
docs/Writerside/cfg/buildprofiles.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE buildprofiles SYSTEM "https://resources.jetbrains.com/writerside/1.0/build-profiles.dtd">
|
||||
<buildprofiles xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/build-profiles.xsd"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<build-profile instance="hopframe">
|
||||
<sitemap priority="0.35" change-frequency="monthly"/>
|
||||
<variables>
|
||||
<noindex-content>false</noindex-content>
|
||||
</variables>
|
||||
</build-profile>
|
||||
|
||||
</buildprofiles>
|
||||
7
docs/Writerside/cfg/glossary.xml
Normal file
7
docs/Writerside/cfg/glossary.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE terms SYSTEM "https://resources.jetbrains.com/writerside/1.0/glossary.dtd">
|
||||
<terms>
|
||||
<term name="foo">
|
||||
Description of what "foo" is.
|
||||
</term>
|
||||
</terms>
|
||||
37
docs/Writerside/hopframe.tree
Normal file
37
docs/Writerside/hopframe.tree
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE instance-profile
|
||||
SYSTEM "https://resources.jetbrains.com/writerside/1.0/product-profile.dtd">
|
||||
|
||||
<instance-profile id="hopframe"
|
||||
name="HopFrame"
|
||||
start-page="Overview.md">
|
||||
|
||||
<toc-element topic="Overview.md"/>
|
||||
<toc-element topic="Installation.md"/>
|
||||
<toc-element topic="Authentication.md"/>
|
||||
<toc-element toc-title="Core Module">
|
||||
<toc-element toc-title="Configurations">
|
||||
<toc-element topic="HopFrameConfig.md"/>
|
||||
<toc-element topic="DbContextConfig.md"/>
|
||||
<toc-element topic="TableConfig.md"/>
|
||||
<toc-element topic="PropertyConfig.md"/>
|
||||
</toc-element>
|
||||
<toc-element topic="Callbacks.md"/>
|
||||
<toc-element topic="Custom-Repositories.md"/>
|
||||
</toc-element>
|
||||
<toc-element toc-title="Web Module">
|
||||
<toc-element toc-title="Interface">
|
||||
<toc-element topic="Custom-Views.md"/>
|
||||
<toc-element topic="Table.md"/>
|
||||
<toc-element topic="Dashboard.md"/>
|
||||
</toc-element>
|
||||
<toc-element topic="Plugins.md">
|
||||
<toc-element topic="Events.md">
|
||||
</toc-element>
|
||||
<toc-element topic="Exporter-Plugin.md"/>
|
||||
</toc-element>
|
||||
</toc-element>
|
||||
<toc-element toc-title="Services">
|
||||
<toc-element topic="IFileService.md"/>
|
||||
</toc-element>
|
||||
</instance-profile>
|
||||
BIN
docs/Writerside/images/dashboard.png
Normal file
BIN
docs/Writerside/images/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/Writerside/images/editor.png
Normal file
BIN
docs/Writerside/images/editor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
BIN
docs/Writerside/images/table.png
Normal file
BIN
docs/Writerside/images/table.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
9
docs/Writerside/redirection-rules.xml
Normal file
9
docs/Writerside/redirection-rules.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE rules SYSTEM "https://resources.jetbrains.com/writerside/1.0/redirection-rules.dtd">
|
||||
<rules>
|
||||
<!-- format is as follows
|
||||
<rule id="<unique id>">
|
||||
<accepts>page.html</accepts>
|
||||
</rule>
|
||||
-->
|
||||
</rules>
|
||||
35
docs/Writerside/topics/Authentication.md
Normal file
35
docs/Writerside/topics/Authentication.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Authentication
|
||||
|
||||
The HopFrame is a powerful tool to manage your backend data. So you probably don't want anybody to access the pages.
|
||||
Luckily the HopFrame supports policy based authentication. By default, everybody is allowed to access the whole
|
||||
HopFrame, but you can restrict that by registering a scoped service implementing the `IHopFrameAuthHandler`.
|
||||
If no service is registered, the default handler gets registered, but it lets any traffic pass.
|
||||
|
||||
## Example
|
||||
|
||||
Create a service that handles authentication:
|
||||
|
||||
```C#
|
||||
public class AuthService(IAuthStore store) : IHopFrameAuthHandler {
|
||||
|
||||
public async Task<bool> IsAuthenticatedAsync(string? policy) {
|
||||
var currentUser = await store.GetCurrentUser();
|
||||
return await store.IsPermitted(currentUser, policy);
|
||||
}
|
||||
|
||||
public async Task<string> GetCurrentUserDisplayNameAsync() {
|
||||
var currentUser = await store.GetCurrentUser();
|
||||
return currentUser.FullName;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Now register it in the DI container:
|
||||
|
||||
```C#
|
||||
builder.Services.AddScoped<IHopFrameAuthHandler, AuthService>();
|
||||
```
|
||||
|
||||
**Hint:** You can display the current users name in the ui by enabling the feature in
|
||||
the [HopFrameConfig](HopFrameConfig.md#displayuserinfo).
|
||||
31
docs/Writerside/topics/Callbacks.md
Normal file
31
docs/Writerside/topics/Callbacks.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Callbacks
|
||||
|
||||
Callbacks are a way of executing actions on curtain events in the web ui.
|
||||
|
||||
## Registering a callback handler
|
||||
|
||||
You can register a callback handler using the method provided in the [](TableConfig.md):
|
||||
|
||||
```c#
|
||||
table.AddCallbackHandler(CallbackType.DeleteEntry, (user, services) => {
|
||||
var logger = services.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogInformation("User {user} deleted!", user.Username);
|
||||
});
|
||||
```
|
||||
|
||||
The callback handler takes the entity that's modified and a `IServiceProvider` as arguments
|
||||
and can either be synchronous or asynchronous.
|
||||
|
||||
## Callback types
|
||||
|
||||
```C#
|
||||
public enum CallbackType {
|
||||
CreateEntry = 0,
|
||||
UpdateEntry = 1,
|
||||
DeleteEntry = 2
|
||||
}
|
||||
```
|
||||
|
||||
- `CallbackType.CreateEntry`: The handler gets executed, when an entity is created.
|
||||
- `CallbackType.UpdateEntry`: The handler gets executed, when an entity is updated.
|
||||
- `CallbackType.DeleteEntry`: The handler gets executed, when an entity is deleted.
|
||||
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!
|
||||
101
docs/Writerside/topics/Custom-Views.md
Normal file
101
docs/Writerside/topics/Custom-Views.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Custom Views
|
||||
|
||||
You can also add your own pages to the HopFrame UI by defining the routes as custom views.
|
||||
You can do that by using the following extension methods for the [](HopFrameConfig.md):
|
||||
|
||||
## Configuration methods
|
||||
|
||||
### AddCustomView (With configurator delegate)
|
||||
|
||||
Creates an entry to the side menu and dashboard with a custom URL.
|
||||
|
||||
```c#
|
||||
HopFrameConfigurator AddCustomView(string name, string url, Action<CustomViewConfigurator> configuratorDelegate)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `configurator`: The configurator for the HopFrame config that is being created.
|
||||
- `name`: The name of the navigation entry.
|
||||
- `url`: The target URL of the navigation entry.
|
||||
- `configuratorDelegate`: The delegate for configuring the view.
|
||||
|
||||
- **Returns:** `HopFrameConfigurator`
|
||||
|
||||
### AddCustomView (Without configurator delegate)
|
||||
|
||||
Creates an entry to the side menu and dashboard with a custom URL.
|
||||
|
||||
```c#
|
||||
CustomViewConfigurator AddCustomView(string name, string url)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `configurator`: The configurator for the HopFrame config that is being created.
|
||||
- `name`: The name of the navigation entry.
|
||||
- `url`: The target URL of the navigation entry.
|
||||
|
||||
- **Returns:** `CustomViewConfigurator`
|
||||
|
||||
## CustomViewConfigurator
|
||||
|
||||
### SetDescription
|
||||
|
||||
Sets the description displayed in the dashboard.
|
||||
|
||||
```c#
|
||||
CustomViewConfigurator SetDescription(string description)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `description`: The desired description.
|
||||
|
||||
- **Returns:** `CustomViewConfigurator`
|
||||
|
||||
### SetPolicy
|
||||
|
||||
Sets the policy needed in order to access the view.
|
||||
|
||||
```c#
|
||||
CustomViewConfigurator SetPolicy(string policy)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `policy`: The desired policy.
|
||||
|
||||
- **Returns:** `CustomViewConfigurator`
|
||||
|
||||
### SetIcon
|
||||
|
||||
Sets the icon displayed in the sidebar.
|
||||
|
||||
```c#
|
||||
CustomViewConfigurator SetIcon(string icon)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `icon`: The desired [fluent-icon](https://www.fluentui-blazor.net/Icon#explorer).
|
||||
|
||||
- **Returns:** `CustomViewConfigurator`
|
||||
|
||||
### SetLinkMatch
|
||||
|
||||
Sets the rule for the sidebar to determine if the link is active.
|
||||
|
||||
```c#
|
||||
CustomViewConfigurator SetLinkMatch(NavLinkMatch match)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `match`: The desired match rule.
|
||||
|
||||
- **Returns:** `CustomViewConfigurator`
|
||||
|
||||
## Example
|
||||
|
||||
```C#
|
||||
builder.Services.AddHopFrame(options => {
|
||||
options.AddCustomView("Counter", "/counter")
|
||||
.SetDescription("A custom view")
|
||||
.SetPolicy("counter.view");
|
||||
});
|
||||
```
|
||||
9
docs/Writerside/topics/Dashboard.md
Normal file
9
docs/Writerside/topics/Dashboard.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Dashboard
|
||||
|
||||
The dashboard gives you an overview of all pages accessible through the HopFrame interface.
|
||||
|
||||
An example configuration could lead to something like this:
|
||||
|
||||

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

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

|
||||
|
||||
You can modify the data of the selected entity based on your [PropertyConfigs](PropertyConfig.md).
|
||||
The HopFrame can handle various property types like strings, numbers, enums, relations and many more.
|
||||
|
||||
### Validation
|
||||
|
||||
The HopFrame also supports input validation. By default, a required validation is automatically applied
|
||||
to every property that's not nullable. You can change the validation behavior in the
|
||||
[PropertyConfig](PropertyConfig.md#setvalidator-synchronous).
|
||||
191
docs/Writerside/topics/TableConfig.md
Normal file
191
docs/Writerside/topics/TableConfig.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# TableConfig
|
||||
|
||||
This configuration contains all configurations for the given table type.
|
||||
|
||||
## Configuration methods
|
||||
|
||||
### Ignore
|
||||
|
||||
Determines if the table should be ignored in the admin UI.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> Ignore(bool ignore)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `ignore`: A boolean value to set if the table should be ignored.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
|
||||
### Property (With configurator)
|
||||
|
||||
Configures the property of the table using the provided configurator.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression, Action<PropertyConfigurator<TProp>> configurator)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `propertyExpression`: Used for determining the property.
|
||||
- `configurator`: Used for configuring the property.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
|
||||
- **See Also:** [](PropertyConfig.md)
|
||||
|
||||
### Property (Without configurator)
|
||||
|
||||
Configures the property of the table.
|
||||
|
||||
```c#
|
||||
PropertyConfigurator<TProp> Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `propertyExpression`: Used for determining the property.
|
||||
|
||||
- **Returns:** `PropertyConfigurator<TProp>`
|
||||
|
||||
- **See Also:** [](PropertyConfig.md)
|
||||
|
||||
### AddVirtualProperty (With configurator)
|
||||
|
||||
Adds a virtual property to the table view and configures it using the provided configurator (this property will not appear in the editor).
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template, Action<PropertyConfigurator<string>> configurator)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `name`: The name of the virtual property.
|
||||
- `template`: The template used for generating the property value.
|
||||
- `configurator`: Used for configuring the virtual property.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
|
||||
- **See Also:** [](PropertyConfig.md)
|
||||
|
||||
### AddVirtualProperty (Synchronous)
|
||||
|
||||
Adds a virtual property to the table view (this property will not appear in the editor).
|
||||
|
||||
```c#
|
||||
PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `name`: The name of the virtual property.
|
||||
- `template`: The template used for generating the property value.
|
||||
|
||||
- **Returns:** `PropertyConfigurator<string>`
|
||||
|
||||
- **See Also:** [](PropertyConfig.md)
|
||||
|
||||
### AddVirtualProperty (Asynchronous)
|
||||
|
||||
Adds a virtual property to the table view (this property will not appear in the editor).
|
||||
|
||||
```c#
|
||||
PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, Task<string>> template)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `name`: The name of the virtual property.
|
||||
- `template`: The template used for generating the property value.
|
||||
|
||||
- **Returns:** `PropertyConfigurator<string>`
|
||||
|
||||
- **See Also:** [](PropertyConfig.md)
|
||||
|
||||
### SetDisplayName
|
||||
|
||||
Determines the name for the table used in the admin UI and URL for the table page.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> SetDisplayName(string name)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `name`: The display name for the table.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
|
||||
### SetDescription
|
||||
|
||||
Determines the description displayed in the admin UI.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> SetDescription(string description)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `description`: The description for the table.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
|
||||
### SetOrderIndex
|
||||
|
||||
Determines the order index for the table in the admin UI.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> SetOrderIndex(int index)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `index`: The order index for the table.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
|
||||
- **See Also:** [](PropertyConfig.md#setorderindex)
|
||||
|
||||
### SetViewPolicy
|
||||
|
||||
Determines the policy needed by a user in order to view the table.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> SetViewPolicy(string policy)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `policy`: The view policy string.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
|
||||
### SetUpdatePolicy
|
||||
|
||||
Determines the policy needed by a user in order to edit the entries.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> SetUpdatePolicy(string policy)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `policy`: The update policy string.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
|
||||
### SetCreatePolicy
|
||||
|
||||
Determines the policy needed by a user in order to create entries.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> SetCreatePolicy(string policy)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `policy`: The create policy string.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
|
||||
### SetDeletePolicy
|
||||
|
||||
Determines the policy needed by a user in order to delete entries.
|
||||
|
||||
```c#
|
||||
TableConfigurator<TModel> SetDeletePolicy(string policy)
|
||||
```
|
||||
|
||||
- **Parameters:**
|
||||
- `policy`: The delete policy string.
|
||||
|
||||
- **Returns:** `TableConfigurator<TModel>`
|
||||
5
docs/Writerside/v.list
Normal file
5
docs/Writerside/v.list
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE vars SYSTEM "https://resources.jetbrains.com/writerside/1.0/vars.dtd">
|
||||
<vars>
|
||||
<var name="product" value="Writerside"/>
|
||||
</vars>
|
||||
11
docs/Writerside/writerside.cfg
Normal file
11
docs/Writerside/writerside.cfg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE ihp SYSTEM "https://resources.jetbrains.com/writerside/1.0/ihp.dtd">
|
||||
|
||||
<ihp version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/writerside-cfg.xsd">
|
||||
<topics dir="topics"/>
|
||||
<images dir="images" web-path="images"/>
|
||||
<categories src="c.list"/>
|
||||
<vars src="v.list"/>
|
||||
<instance src="hopframe.tree"/>
|
||||
</ihp>
|
||||
31
src/HopFrame.Core/Callbacks/CallbackTypes.cs
Normal file
31
src/HopFrame.Core/Callbacks/CallbackTypes.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using HopFrame.Core.Config;
|
||||
|
||||
namespace HopFrame.Core.Callbacks;
|
||||
|
||||
public static class CallbackTypes {
|
||||
|
||||
private const string Prefix = "HopFrame.";
|
||||
private const string CreateEntryPrefix = Prefix + "Entry.Create.";
|
||||
private const string UpdateEntryPrefix = Prefix + "Entry.Update.";
|
||||
private const string DeleteEntryPrefix = Prefix + "Entry.Delete.";
|
||||
|
||||
public static string CreateEntry(TableConfig config) => CreateEntryPrefix + config.PropertyName;
|
||||
public static string UpdateEntry(TableConfig config) => UpdateEntryPrefix + config.PropertyName;
|
||||
public static string DeleteEntry(TableConfig config) => DeleteEntryPrefix + config.PropertyName;
|
||||
|
||||
public static string ConstructCallbackName(CallbackType type, TableConfig config) {
|
||||
return type switch {
|
||||
CallbackType.CreateEntry => CreateEntry(config),
|
||||
CallbackType.UpdateEntry => UpdateEntry(config),
|
||||
CallbackType.DeleteEntry => DeleteEntry(config),
|
||||
_ => Prefix
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum CallbackType {
|
||||
CreateEntry = 0,
|
||||
UpdateEntry = 1,
|
||||
DeleteEntry = 2
|
||||
}
|
||||
7
src/HopFrame.Core/Callbacks/HopCallbackHandler.cs
Normal file
7
src/HopFrame.Core/Callbacks/HopCallbackHandler.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace HopFrame.Core.Callbacks;
|
||||
|
||||
public readonly struct HopCallbackHandler(string eventType, Func<object, IServiceProvider, Task> handler) {
|
||||
public Guid Id { get; } = Guid.CreateVersion7();
|
||||
public Func<object, IServiceProvider, Task> Handler { get; } = handler;
|
||||
public string EventType { get; } = eventType;
|
||||
}
|
||||
14
src/HopFrame.Core/Callbacks/ICallbackEmitter.cs
Normal file
14
src/HopFrame.Core/Callbacks/ICallbackEmitter.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace HopFrame.Core.Callbacks;
|
||||
|
||||
public interface ICallbackEmitter {
|
||||
|
||||
Guid RegisterCallbackHandler(string @event, Func<object, IServiceProvider, Task> handler);
|
||||
|
||||
bool RemoveCallbackHandler(Guid id);
|
||||
|
||||
Task DispatchCallback(string @event, object argument = null!);
|
||||
|
||||
void RemoveAllCallbackHandlers(string @event);
|
||||
void RemoveAllCallbackHandlers();
|
||||
|
||||
}
|
||||
@@ -2,12 +2,20 @@
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
public class DbContextConfig {
|
||||
public interface ITableGroupConfig {
|
||||
public Type ContextType { get; }
|
||||
public List<TableConfig> Tables { get; }
|
||||
public HopFrameConfig ParentConfig { get; }
|
||||
}
|
||||
|
||||
public class DbContextConfig : ITableGroupConfig {
|
||||
public Type ContextType { get; }
|
||||
public List<TableConfig> Tables { get; } = new();
|
||||
public HopFrameConfig ParentConfig { get; }
|
||||
|
||||
public DbContextConfig(Type context) {
|
||||
public DbContextConfig(Type context, HopFrameConfig parentConfig) {
|
||||
ContextType = context;
|
||||
ParentConfig = parentConfig;
|
||||
|
||||
foreach (var property in ContextType.GetProperties()) {
|
||||
if (!property.PropertyType.IsGenericType) continue;
|
||||
@@ -24,7 +32,7 @@ public class DbContextConfig {
|
||||
/// <summary>
|
||||
/// A helper class for editing the <see cref="DbContextConfig"/>
|
||||
/// </summary>
|
||||
public class DbContextConfigurator<TDbContext>(DbContextConfig config) {
|
||||
public sealed class DbContextConfigurator<TDbContext>(DbContextConfig config) {
|
||||
|
||||
/// <summary>
|
||||
/// The Internal DbContext configuration that's modified by the helper functions
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
using HopFrame.Core.Callbacks;
|
||||
using HopFrame.Core.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
public class HopFrameConfig {
|
||||
public List<DbContextConfig> Contexts { get; } = new();
|
||||
public List<ITableGroupConfig> Contexts { get; } = new();
|
||||
public bool DisplayUserInfo { get; set; } = true;
|
||||
public string? BasePolicy { get; set; }
|
||||
public string? LoginPageRewrite { get; set; }
|
||||
public List<HopCallbackHandler> Handlers { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A helper class for editing the <see cref="HopFrameConfig"/>
|
||||
/// </summary>
|
||||
public class HopFrameConfigurator(HopFrameConfig config) {
|
||||
public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollection collection = null!) {
|
||||
|
||||
/// <summary>
|
||||
/// The Internal HopFrame configuration that's modified by the helper functions
|
||||
/// </summary>
|
||||
public HopFrameConfig InnerConfig { get; } = config;
|
||||
|
||||
public IServiceCollection ServiceCollection { get; } = collection;
|
||||
|
||||
/// <summary>
|
||||
/// Adds all tables defined in the DbContext to the HopFrame ui and configures it using the provided configurator
|
||||
@@ -38,11 +45,64 @@ public class HopFrameConfigurator(HopFrameConfig config) {
|
||||
/// <returns>The configurator used for the DbContext</returns>
|
||||
/// <seealso cref="DbContextConfigurator{TDbContext}"/>
|
||||
public DbContextConfigurator<TDbContext> AddDbContext<TDbContext>() where TDbContext : DbContext {
|
||||
var context = new DbContextConfig(typeof(TDbContext));
|
||||
var context = new DbContextConfig(typeof(TDbContext), InnerConfig);
|
||||
InnerConfig.Contexts.Add(context);
|
||||
return new DbContextConfigurator<TDbContext>(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a table of the desired type and configures it to use a custom repository
|
||||
/// </summary>
|
||||
/// <param name="keyExpression">The key of the model</param>
|
||||
/// <param name="configurator">The configurator used for configuring the table page</param>
|
||||
/// <typeparam name="TRepository">The repository class that inherits from the <see cref="IHopFrameRepository{TModel,TKey}"/> (needs to be registered as a service)</typeparam>
|
||||
/// <typeparam name="TModel">The model of the table</typeparam>
|
||||
/// <typeparam name="TKey">The type of the primary key</typeparam>
|
||||
public HopFrameConfigurator AddCustomRepository<TRepository, TModel, TKey>(Expression<Func<TModel, TKey>> keyExpression, Action<TableConfigurator<TModel>> configurator) {
|
||||
var context = AddCustomRepository<TRepository, TModel, TKey>(keyExpression);
|
||||
configurator.Invoke(context);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a table of the desired type and configures it to use a custom repository
|
||||
/// </summary>
|
||||
/// <param name="keyExpression">The key of the model</param>
|
||||
/// <typeparam name="TRepository">The repository class that inherits from the <see cref="IHopFrameRepository{TModel,TKey}"/> (needs to be registered as a service)</typeparam>
|
||||
/// <typeparam name="TModel">The model of the table</typeparam>
|
||||
/// <typeparam name="TKey">The type of the primary key</typeparam>
|
||||
/// <returns>The configurator used for configuring the table page</returns>
|
||||
public TableConfigurator<TModel> AddCustomRepository<TRepository, TModel, TKey>(Expression<Func<TModel, TKey>> keyExpression) {
|
||||
var keyProperty = TableConfigurator<TModel>.GetPropertyInfo(keyExpression);
|
||||
var context = new RepositoryGroupConfig(typeof(TRepository), keyProperty, InnerConfig);
|
||||
context.Tables.Add(new TableConfig(context, typeof(TModel), typeof(TRepository).Name, 0));
|
||||
InnerConfig.Contexts.Add(context);
|
||||
return new TableConfigurator<TModel>(context.Tables[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a context is already registered in the HopFrame
|
||||
/// </summary>
|
||||
/// <typeparam name="TDbContext">The context that should be checked</typeparam>
|
||||
/// <returns>true if the context is already registered, false if not</returns>
|
||||
public bool HasDbContext<TDbContext>() where TDbContext : DbContext {
|
||||
return InnerConfig.Contexts.Any(context => context.ContextType == typeof(TDbContext));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a configurator for the context if it was already defined
|
||||
/// </summary>
|
||||
/// <typeparam name="TDbContext"></typeparam>
|
||||
/// <returns>The configurator of the context if it already was defined, null if not</returns>
|
||||
public DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext : DbContext {
|
||||
var config = InnerConfig.Contexts
|
||||
.OfType<DbContextConfig>()
|
||||
.SingleOrDefault(context => context.ContextType == typeof(TDbContext));
|
||||
if (config is null) return null;
|
||||
|
||||
return new DbContextConfigurator<TDbContext>(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin ui
|
||||
/// </summary>
|
||||
|
||||
@@ -11,9 +11,9 @@ public class PropertyConfig(PropertyInfo info, TableConfig table, int nthPropert
|
||||
public bool Sortable { get; set; } = true;
|
||||
public bool Searchable { get; set; } = true;
|
||||
public PropertyInfo? DisplayedProperty { get; set; }
|
||||
public Func<object, IServiceProvider, string>? Formatter { get; set; }
|
||||
public Func<object, IServiceProvider, string>? EnumerableFormatter { get; set; }
|
||||
public Func<string, IServiceProvider, object>? Parser { get; set; }
|
||||
public Func<object, IServiceProvider, Task<string>>? Formatter { get; set; }
|
||||
public Func<object, IServiceProvider, Task<string>>? EnumerableFormatter { get; set; }
|
||||
public Func<string, IServiceProvider, Task<object>>? Parser { get; set; }
|
||||
public Func<object?, IServiceProvider, Task<IEnumerable<string>>>? Validator { get; set; }
|
||||
public bool Editable { get; set; } = true;
|
||||
public bool Creatable { get; set; } = true;
|
||||
@@ -23,8 +23,37 @@ public class PropertyConfig(PropertyInfo info, TableConfig table, int nthPropert
|
||||
public bool IsRelation { get; internal set; }
|
||||
public bool IsRequired { get; internal set; }
|
||||
public bool IsEnumerable { get; internal set; }
|
||||
public bool IsListingProperty { get; set; }
|
||||
public bool IsVirtualProperty { get; set; }
|
||||
public int Order { get; set; } = nthProperty;
|
||||
public int DisplayLength { get; set; } = 32;
|
||||
|
||||
public virtual object? GetValue(object? source, IServiceProvider provider) {
|
||||
return Info.GetValue(source);
|
||||
}
|
||||
|
||||
public virtual void SetValue(object? source, object? value, IServiceProvider provider) {
|
||||
Info.SetValue(source, value);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class VirtualPropertyConfig(TableConfig table, int nthProperty) : PropertyConfig(GetDummyProperty(), table, nthProperty) {
|
||||
public string? DummyProperty { get; set; } = null;
|
||||
|
||||
public Func<object, string, IServiceProvider, Task>? VirtualParser { get; set; }
|
||||
|
||||
public override object? GetValue(object? source, IServiceProvider provider) {
|
||||
return Formatter!.Invoke(source!, provider).Result;
|
||||
}
|
||||
|
||||
public override void SetValue(object? source, object? value, IServiceProvider provider) {
|
||||
VirtualParser?.Invoke(source!, (string)value!, provider).Wait();
|
||||
}
|
||||
|
||||
private static PropertyInfo GetDummyProperty() {
|
||||
return typeof(VirtualPropertyConfig)
|
||||
.GetProperties()
|
||||
.First(prop => prop.Name == nameof(DummyProperty));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,7 +103,6 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
|
||||
/// <summary>
|
||||
/// Determines if the value that should be displayed instead of the string representation of the type
|
||||
/// </summary>
|
||||
/// <seealso cref="Format"/>
|
||||
public PropertyConfigurator<TProp> SetDisplayedProperty<TInnerProp>(Expression<Func<TProp, TInnerProp>> propertyExpression) {
|
||||
InnerConfig.DisplayedProperty = TableConfigurator<TProp>.GetPropertyInfo(propertyExpression);
|
||||
return this;
|
||||
@@ -83,9 +111,14 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
|
||||
/// <summary>
|
||||
/// Determines the value that's displayed in the admin ui
|
||||
/// </summary>
|
||||
/// <seealso cref="FormatEach{TInnerProp}"/>
|
||||
/// <seealso cref="SetDisplayedProperty{TInnerProp}"/>
|
||||
public PropertyConfigurator<TProp> Format(Func<TProp, IServiceProvider, string> formatter) {
|
||||
InnerConfig.Formatter = (obj, provider) => Task.FromResult(formatter.Invoke((TProp)obj, provider));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Format(System.Func{TProp,System.IServiceProvider,string})"/>
|
||||
public PropertyConfigurator<TProp> Format(Func<TProp, IServiceProvider, Task<string>> formatter) {
|
||||
InnerConfig.Formatter = (obj, provider) => formatter.Invoke((TProp)obj, provider);
|
||||
return this;
|
||||
}
|
||||
@@ -94,6 +127,12 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
|
||||
/// Determines the value that's displayed for each entry in the list
|
||||
/// </summary>
|
||||
public PropertyConfigurator<TProp> FormatEach<TInnerProp>(Func<TInnerProp, IServiceProvider, string> formatter) {
|
||||
InnerConfig.EnumerableFormatter = (obj, provider) => Task.FromResult(formatter.Invoke((TInnerProp)obj, provider));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="FormatEach{TInnerProp}(System.Func{TInnerProp,System.IServiceProvider,string})"/>
|
||||
public PropertyConfigurator<TProp> FormatEach<TInnerProp>(Func<TInnerProp, IServiceProvider, Task<string>> formatter) {
|
||||
InnerConfig.EnumerableFormatter = (obj, provider) => formatter.Invoke((TInnerProp)obj, provider);
|
||||
return this;
|
||||
}
|
||||
@@ -102,7 +141,13 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
|
||||
/// Determines the function used for parsing the value provided in the editor dialog to the actual property value
|
||||
/// </summary>
|
||||
public PropertyConfigurator<TProp> SetParser(Func<string, IServiceProvider, TProp> parser) {
|
||||
InnerConfig.Parser = (str, provider) => parser.Invoke(str, provider)!;
|
||||
InnerConfig.Parser = (str, provider) => Task.FromResult<object>(parser.Invoke(str, provider)!);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SetParser(System.Func{string,System.IServiceProvider,TProp})"/>
|
||||
public PropertyConfigurator<TProp> SetParser(Func<string, IServiceProvider, Task<TProp>> parser) {
|
||||
InnerConfig.Parser = async (str, provider) => (await parser.Invoke(str, provider))!;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -174,4 +219,50 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
|
||||
InnerConfig.Order = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum character length displayed in the admin ui (not in the editor dialog)
|
||||
/// </summary>
|
||||
/// <param name="maxLength">The maximum length of characters to be displayed</param>
|
||||
public PropertyConfigurator<TProp> SetDisplayLength(int maxLength) {
|
||||
InnerConfig.DisplayLength = maxLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces a property to be treated as a relation
|
||||
/// </summary>
|
||||
/// <param name="isEnumerable">Determines if it is possible to assign multiple objects to the property</param>
|
||||
/// <param name="isRequired">Determines if the property is nullable</param>
|
||||
public PropertyConfigurator<TProp> ForceRelation(bool isEnumerable = false, bool isRequired = true) {
|
||||
InnerConfig.IsRelation = true;
|
||||
InnerConfig.IsEnumerable = isEnumerable;
|
||||
InnerConfig.IsRequired = isRequired;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class VirtualPropertyConfigurator<TModel>(VirtualPropertyConfig config) : PropertyConfigurator<string>(config) {
|
||||
/// <summary>
|
||||
/// Determines the function used for parsing the value provided in the editor dialog to the actual model value
|
||||
/// </summary>
|
||||
public VirtualPropertyConfigurator<TModel> SetVirtualParser(Action<TModel, string, IServiceProvider> parser) {
|
||||
var cfg = InnerConfig as VirtualPropertyConfig;
|
||||
|
||||
cfg!.VirtualParser = (model, input, services) => {
|
||||
parser.Invoke((TModel)model, input, services);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SetVirtualParser{TModel}(System.Action{TModel,string,System.IServiceProvider})"/>
|
||||
public VirtualPropertyConfigurator<TModel> SetVirtualParser(Func<TModel, string, IServiceProvider, Task> parser) {
|
||||
var cfg = InnerConfig as VirtualPropertyConfig;
|
||||
|
||||
cfg!.VirtualParser = (model, input, services) => parser.Invoke((TModel)model, input, services);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using HopFrame.Core.Callbacks;
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
@@ -10,10 +11,11 @@ public class TableConfig {
|
||||
public string PropertyName { get; }
|
||||
public string DisplayName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DbContextConfig ContextConfig { get; }
|
||||
public ITableGroupConfig ContextConfig { get; }
|
||||
public bool Ignored { get; set; }
|
||||
public int Order { get; set; }
|
||||
internal bool Seeded { get; set; }
|
||||
public bool ShowSearchSuggestions { get; set; } = true;
|
||||
|
||||
public string? ViewPolicy { get; set; }
|
||||
public string? CreatePolicy { get; set; }
|
||||
@@ -22,7 +24,7 @@ public class TableConfig {
|
||||
|
||||
public List<PropertyConfig> Properties { get; } = new();
|
||||
|
||||
public TableConfig(DbContextConfig config, Type tableType, string propertyName, int nthTable) {
|
||||
public TableConfig(ITableGroupConfig config, Type tableType, string propertyName, int nthTable) {
|
||||
TableType = tableType;
|
||||
PropertyName = propertyName;
|
||||
ContextConfig = config;
|
||||
@@ -51,7 +53,7 @@ public class TableConfig {
|
||||
/// <summary>
|
||||
/// A helper class for editing the <see cref="TableConfig"/>
|
||||
/// </summary>
|
||||
public class TableConfigurator<TModel>(TableConfig config) {
|
||||
public sealed class TableConfigurator<TModel>(TableConfig config) {
|
||||
|
||||
/// <summary>
|
||||
/// The Internal property configuration that's modified by the helper functions
|
||||
@@ -65,6 +67,13 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
||||
InnerConfig.Ignored = ignore;
|
||||
return this;
|
||||
}
|
||||
/// <summary>
|
||||
/// Determines if search suggestions should be displayed in the ui (Advanced Search)
|
||||
/// </summary>
|
||||
public TableConfigurator<TModel> ShowSearchSuggestions(bool show = true) {
|
||||
InnerConfig.ShowSearchSuggestions = show;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the property of the table
|
||||
@@ -75,7 +84,7 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
||||
public PropertyConfigurator<TProp> Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression) {
|
||||
var info = GetPropertyInfo(propertyExpression);
|
||||
var prop = InnerConfig.Properties
|
||||
.Single(prop => prop.Info.Name == info.Name);
|
||||
.Single(prop => prop.Info == info);
|
||||
return new PropertyConfigurator<TProp>(prop);
|
||||
}
|
||||
|
||||
@@ -98,14 +107,25 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
||||
/// <param name="template">The template used for generating the property value</param>
|
||||
/// <returns>The configurator for the virtual property</returns>
|
||||
/// <seealso cref="PropertyConfigurator{TProp}"/>
|
||||
public PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template) {
|
||||
var prop = new PropertyConfig(InnerConfig.Properties.First().Info, InnerConfig, InnerConfig.Properties.Count) {
|
||||
public VirtualPropertyConfigurator<TModel> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template) {
|
||||
var prop = new VirtualPropertyConfig(InnerConfig, InnerConfig.Properties.Count) {
|
||||
Name = name,
|
||||
IsListingProperty = true,
|
||||
IsVirtualProperty = true,
|
||||
Formatter = (obj, provider) => Task.FromResult(template.Invoke((TModel)obj, provider))
|
||||
};
|
||||
InnerConfig.Properties.Add(prop);
|
||||
return new VirtualPropertyConfigurator<TModel>(prop);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AddVirtualProperty(string,System.Func{TModel,System.IServiceProvider,string})"/>
|
||||
public VirtualPropertyConfigurator<TModel> AddVirtualProperty(string name, Func<TModel, IServiceProvider, Task<string>> template) {
|
||||
var prop = new VirtualPropertyConfig(InnerConfig, InnerConfig.Properties.Count) {
|
||||
Name = name,
|
||||
IsVirtualProperty = true,
|
||||
Formatter = (obj, provider) => template.Invoke((TModel)obj, provider)
|
||||
};
|
||||
InnerConfig.Properties.Add(prop);
|
||||
return new PropertyConfigurator<string>(prop);
|
||||
return new VirtualPropertyConfigurator<TModel>(prop);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -177,6 +197,36 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
||||
InnerConfig.DeletePolicy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a callback handler of the provided type
|
||||
/// </summary>
|
||||
/// <param name="type">The type of callback that triggers the handler</param>
|
||||
/// <param name="handler">The handler delegate</param>
|
||||
public TableConfigurator<TModel> AddCallbackHandler(CallbackType type, Func<TModel, IServiceProvider, Task> handler) {
|
||||
var eventName = CallbackTypes.ConstructCallbackName(type, InnerConfig);
|
||||
var handlerStore = new HopCallbackHandler(eventName, (o, provider) => handler.Invoke((TModel)o, provider));
|
||||
InnerConfig.ContextConfig.ParentConfig.Handlers.Add(handlerStore);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a callback handler of the provided type
|
||||
/// </summary>
|
||||
/// <param name="type">The type of callback that triggers the handler</param>
|
||||
/// <param name="handler">The handler delegate</param>
|
||||
public TableConfigurator<TModel> AddCallbackHandler(CallbackType type, Action<TModel, IServiceProvider> handler) {
|
||||
var eventName = CallbackTypes.ConstructCallbackName(type, InnerConfig);
|
||||
var handlerStore = new HopCallbackHandler(eventName, (o, provider) => {
|
||||
handler.Invoke((TModel)o, provider);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
InnerConfig.ContextConfig.ParentConfig.Handlers.Add(handlerStore);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
internal static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) {
|
||||
if (propertyLambda.Body is not MemberExpression member) {
|
||||
@@ -187,7 +237,7 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
|
||||
}
|
||||
|
||||
Type type = typeof(TSource);
|
||||
var type = typeof(TSource);
|
||||
if (propInfo.ReflectedType != null && type != propInfo.ReflectedType &&
|
||||
!type.IsSubclassOf(propInfo.ReflectedType)) {
|
||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a property that is not from type {type}.");
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using HopFrame.Core.Services;
|
||||
using HopFrame.Core.Callbacks;
|
||||
using HopFrame.Core.Services;
|
||||
using HopFrame.Core.Services.Implementations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
@@ -15,6 +16,8 @@ public static class ServiceCollectionExtensions {
|
||||
public static IServiceCollection AddHopFrameServices(this IServiceCollection services) {
|
||||
services.AddScoped<IContextExplorer, ContextExplorer>();
|
||||
services.TryAddScoped<IHopFrameAuthHandler, DefaultAuthHandler>();
|
||||
services.TryAddScoped<ICallbackEmitter, CallbackEmitter>();
|
||||
services.AddScoped<ISearchExpressionBuilder, SearchExpressionBuilder>();
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ public interface IContextExplorer {
|
||||
public TableConfig? GetTable(string tableDisplayName);
|
||||
public TableConfig? GetTable(Type tableEntity);
|
||||
public ITableManager? GetTableManager(string tablePropertyName);
|
||||
public ITableManager? GetTableManager(Type tableType);
|
||||
}
|
||||
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,13 +4,14 @@ using HopFrame.Core.Config;
|
||||
namespace HopFrame.Core.Services;
|
||||
|
||||
public interface ITableManager {
|
||||
public IQueryable<object> LoadPage(int page, int perPage = 20);
|
||||
public Task<IEnumerable<object>> LoadPage(int page, int perPage = 20);
|
||||
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20);
|
||||
public Task<int> TotalPages(int perPage = 20);
|
||||
public Task DeleteItem(object item);
|
||||
public Task EditItem(object item);
|
||||
public Task AddItem(object item);
|
||||
public Task RevertChanges(object item);
|
||||
public Task AddAll(IEnumerable<object> items);
|
||||
public Task<object?> GetOne(object key);
|
||||
|
||||
public string DisplayProperty(object? item, PropertyConfig prop, object? value = null);
|
||||
public Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Callbacks;
|
||||
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
internal sealed class CallbackEmitter(IServiceProvider provider, HopFrameConfig config) : ICallbackEmitter {
|
||||
|
||||
public Guid RegisterCallbackHandler(string @event, Func<object, IServiceProvider, Task> handler) {
|
||||
var handlerStore = new HopCallbackHandler(@event, handler);
|
||||
config.Handlers.Add(handlerStore);
|
||||
return handlerStore.Id;
|
||||
}
|
||||
|
||||
public bool RemoveCallbackHandler(Guid id) {
|
||||
var count = config.Handlers.RemoveAll(handler => handler.Id == id);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public async Task DispatchCallback(string @event, object argument = null!) {
|
||||
var handlers = config.Handlers.Where(handler => handler.EventType == @event);
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var handler in handlers) {
|
||||
var task = handler.Handler.Invoke(argument, provider);
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public void RemoveAllCallbackHandlers(string @event) {
|
||||
config.Handlers.RemoveAll(handler => handler.EventType == @event);
|
||||
}
|
||||
|
||||
public void RemoveAllCallbackHandlers() {
|
||||
config.Handlers.Clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using HopFrame.Core.Config;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
@@ -44,11 +45,40 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tablePropertyName);
|
||||
if (table is null) continue;
|
||||
|
||||
var dbContext = provider.GetService(context.ContextType) as DbContext;
|
||||
if (dbContext is null) return null;
|
||||
var repo = provider.GetService(context.ContextType);
|
||||
if (repo is null) return null;
|
||||
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
return Activator.CreateInstance(type, dbContext, table, this, provider) as ITableManager;
|
||||
if (context is DbContextConfig) {
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
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;
|
||||
}
|
||||
|
||||
public ITableManager? GetTableManager(Type tableType) {
|
||||
foreach (var context in config.Contexts) {
|
||||
var table = context.Tables.FirstOrDefault(table => table.TableType == tableType);
|
||||
if (table is null) continue;
|
||||
|
||||
var repo = provider.GetService(context.ContextType);
|
||||
if (repo is null) return null;
|
||||
|
||||
if (context is DbContextConfig) {
|
||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||
return Activator.CreateInstance(type, (DbContext)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;
|
||||
@@ -56,15 +86,36 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
|
||||
private void SeedTableData(TableConfig table) {
|
||||
if (table.Seeded) return;
|
||||
var dbContext = (provider.GetService(table.ContextConfig.ContextType) as DbContext)!;
|
||||
if (table.ContextConfig is not DbContextConfig) return;
|
||||
var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!;
|
||||
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
||||
|
||||
foreach (var propertyConfig in table.Properties) {
|
||||
if (propertyConfig.IsListingProperty) continue;
|
||||
if (propertyConfig.IsVirtualProperty) continue;
|
||||
if (propertyConfig.IsRelation) continue;
|
||||
|
||||
var prop = entity.FindProperty(propertyConfig.Info.Name);
|
||||
if (prop is not null) continue;
|
||||
|
||||
var nav = entity.FindNavigation(propertyConfig.Info.Name);
|
||||
if (nav is null) continue;
|
||||
if (nav is null) {
|
||||
if (!propertyConfig.Info.PropertyType.IsGenericType) continue;
|
||||
var relationType = propertyConfig.Info.PropertyType.GenericTypeArguments[0];
|
||||
|
||||
var isRelation = entity.Model.GetEntityTypes()
|
||||
.Select(e => e.GetForeignKeys())
|
||||
.Any(keys => keys
|
||||
.Select(k => k.PrincipalEntityType.ClrType)
|
||||
.Any(t => t == relationType || t == table.TableType));
|
||||
if (!isRelation) continue;
|
||||
|
||||
propertyConfig.IsRelation = true;
|
||||
propertyConfig.IsRequired = false;
|
||||
propertyConfig.IsEnumerable = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
propertyConfig.IsRelation = true;
|
||||
propertyConfig.IsRequired = nav.ForeignKey.IsRequired;
|
||||
propertyConfig.IsEnumerable = nav.IsCollection;
|
||||
@@ -72,9 +123,9 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
|
||||
foreach (var property in entity.GetProperties()) {
|
||||
var propConfig = table.Properties
|
||||
.Where(prop => !prop.IsListingProperty)
|
||||
.Where(prop => !prop.IsVirtualProperty)
|
||||
.SingleOrDefault(prop => prop.Info == property.PropertyInfo);
|
||||
if (propConfig is null) continue;
|
||||
if (propConfig is null || propConfig.IsRequired) continue;
|
||||
propConfig.IsRequired = !property.IsNullable;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,32 +1,48 @@
|
||||
using System.Collections;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using HopFrame.Core.Config;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
internal sealed class TableManager<TModel>(DbContext context, TableConfig config, IContextExplorer explorer, IServiceProvider provider) : ITableManager where TModel : class {
|
||||
internal sealed class TableManager<TModel>(DbContext context, TableConfig config, IContextExplorer explorer, IServiceProvider provider, ISearchExpressionBuilder searchExpressionBuilder) : ITableManager where TModel : class {
|
||||
|
||||
public IQueryable<object> LoadPage(int page, int perPage = 20) {
|
||||
public async Task<IEnumerable<object>> LoadPage(int page, int perPage = 20) {
|
||||
var table = context.Set<TModel>();
|
||||
var data = IncludeForeignKeys(table);
|
||||
return data
|
||||
return await data
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage);
|
||||
.Take(perPage)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
|
||||
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||
public async Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||
var table = context.Set<TModel>();
|
||||
var all = IncludeForeignKeys(table)
|
||||
.AsEnumerable()
|
||||
.Where(item => ItemSearched(item, searchTerm))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult((
|
||||
(IEnumerable<object>)all.Skip(page * perPage).Take(perPage),
|
||||
(int)Math.Ceiling(all.Count / (double)perPage)));
|
||||
var parameter = Expression.Parameter(typeof(TModel), "x");
|
||||
var exp = searchExpressionBuilder.BuildSearchExpression(config, searchTerm, parameter);
|
||||
|
||||
if (exp is null)
|
||||
return ([], 0);
|
||||
|
||||
var lambda = Expression.Lambda<Func<TModel, bool>>(exp, parameter);
|
||||
var result = await IncludeForeignKeys(table)
|
||||
.Where(lambda)
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage)
|
||||
.ToListAsync();
|
||||
|
||||
var totalEntries = await table
|
||||
.Where(lambda)
|
||||
.CountAsync();
|
||||
|
||||
return (result, (int)Math.Ceiling(totalEntries / (double)perPage));
|
||||
}
|
||||
|
||||
|
||||
public async Task<int> TotalPages(int perPage = 20) {
|
||||
var table = context.Set<TModel>();
|
||||
return (int)Math.Ceiling(await table.CountAsync() / (double)perPage);
|
||||
@@ -48,45 +64,38 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task RevertChanges(object item) {
|
||||
await context.Entry((TModel)item).ReloadAsync();
|
||||
public async Task AddAll(IEnumerable<object> items) {
|
||||
var table = context.Set<TModel>();
|
||||
await table.AddRangeAsync(items.Cast<TModel>());
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private bool ItemSearched(TModel item, string searchTerm) {
|
||||
foreach (var property in config.Properties) {
|
||||
if (!property.Searchable) continue;
|
||||
var value = property.Info.GetValue(item);
|
||||
if (value is null) continue;
|
||||
|
||||
var strValue = value.ToString();
|
||||
if (strValue?.Contains(searchTerm) == true)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
public async Task<object?> GetOne(object key) {
|
||||
var table = context.Set<TModel>();
|
||||
return await table.FindAsync(key);
|
||||
}
|
||||
|
||||
public string DisplayProperty(object? item, PropertyConfig prop, object? value = null) {
|
||||
public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) {
|
||||
if (item is null) return string.Empty;
|
||||
|
||||
if (prop.IsListingProperty)
|
||||
return prop.Formatter!.Invoke(item, provider);
|
||||
if (prop.IsVirtualProperty)
|
||||
return await prop.Formatter!.Invoke(item, provider);
|
||||
|
||||
var propValue = value ?? prop.Info.GetValue(item);
|
||||
var propValue = value ?? prop.GetValue(item, provider);
|
||||
if (propValue is null)
|
||||
return string.Empty;
|
||||
|
||||
if (prop.Formatter is not null) {
|
||||
return prop.Formatter.Invoke(propValue, provider);
|
||||
return await prop.Formatter.Invoke(propValue, provider);
|
||||
}
|
||||
|
||||
if (prop.IsEnumerable) {
|
||||
if (value is not null) {
|
||||
if (enumerableValue is not null) {
|
||||
if (prop.EnumerableFormatter is not null) {
|
||||
return prop.EnumerableFormatter.Invoke(value, provider);
|
||||
return await prop.EnumerableFormatter.Invoke(enumerableValue, provider);
|
||||
}
|
||||
|
||||
return value.ToString() ?? string.Empty;
|
||||
return enumerableValue.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
return (propValue as IEnumerable)!.OfType<object>().Count().ToString();
|
||||
@@ -103,11 +112,11 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
||||
var innerConfig = explorer.GetTable(propValue.GetType());
|
||||
if (innerConfig is null) return propValue.ToString()!;
|
||||
|
||||
var innerProp = innerConfig!.Properties
|
||||
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsListingProperty);
|
||||
var innerProp = innerConfig.Properties
|
||||
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsVirtualProperty);
|
||||
|
||||
if (innerProp is null) return propValue.ToString() ?? string.Empty;
|
||||
return DisplayProperty(propValue, innerProp);
|
||||
return await DisplayProperty(propValue, innerProp);
|
||||
}
|
||||
|
||||
private IQueryable<TModel> IncludeForeignKeys(IQueryable<TModel> query) {
|
||||
|
||||
23
src/HopFrame.Web/Components/App.razor
Normal file
23
src/HopFrame.Web/Components/App.razor
Normal file
@@ -0,0 +1,23 @@
|
||||
@using HopFrame.Web.Components.Pages
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<base href="/"/>
|
||||
<ImportMap/>
|
||||
<HeadOutlet/>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<Router AppAssembly="typeof(HopFrameHome).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData"/>
|
||||
</Found>
|
||||
</Router>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,14 +1,17 @@
|
||||
@implements IDialogContentComponent<EditorDialogData>
|
||||
@rendermode InteractiveServer
|
||||
@implements IDialogContentComponent<EditorDialogData>
|
||||
@implements IDisposable
|
||||
|
||||
@using System.Collections
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
@using HopFrame.Web.Models
|
||||
@using HopFrame.Web.Helpers
|
||||
@using HopFrame.Web.Plugins
|
||||
@using HopFrame.Web.Plugins.Events
|
||||
|
||||
<FluentDialogBody>
|
||||
@foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) {
|
||||
@foreach (var property in GetEditorProperties()) {
|
||||
if (!_currentlyEditing && !property.Creatable) continue;
|
||||
|
||||
<div style="margin-bottom: 20px">
|
||||
@@ -36,11 +39,11 @@
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px; margin-bottom: 4px">
|
||||
@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" />
|
||||
</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" />
|
||||
</FluentButton>
|
||||
</div>
|
||||
@@ -128,7 +131,7 @@
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Enum))" />
|
||||
</div>
|
||||
<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" />
|
||||
</FluentButton>
|
||||
</div>
|
||||
@@ -155,7 +158,7 @@
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
|
||||
}
|
||||
|
||||
@foreach (var error in _validationErrors[property.Info.Name]) {
|
||||
@foreach (var error in _validationErrors[property.Name]) {
|
||||
<FluentLabel Color="@Color.Error">@error</FluentLabel>
|
||||
}
|
||||
</div>
|
||||
@@ -169,6 +172,7 @@
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
@inject IToastService Toasts
|
||||
@inject IServiceProvider Provider
|
||||
@inject IPluginOrchestrator PluginOrchestrator
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -180,6 +184,8 @@
|
||||
private bool _currentlyEditing;
|
||||
private ITableManager? _manager;
|
||||
private readonly Dictionary<string, List<string>> _validationErrors = new();
|
||||
private readonly List<PropertyChange> _changes = new();
|
||||
private readonly CancellationTokenSource _tokenSource = new();
|
||||
|
||||
protected override void OnInitialized() {
|
||||
_currentlyEditing = Content.CurrentObject is not null;
|
||||
@@ -191,20 +197,25 @@
|
||||
Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType);
|
||||
|
||||
foreach (var property in Content.Config.Properties) {
|
||||
if (property.IsListingProperty) continue;
|
||||
_validationErrors.Add(property.Info.Name, []);
|
||||
_validationErrors.Add(property.Name, []);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<PropertyConfig> GetEditorProperties() {
|
||||
return Content.Config.Properties
|
||||
.Where(prop => prop is not VirtualPropertyConfig { VirtualParser: null })
|
||||
.OrderBy(prop => prop.Order);
|
||||
}
|
||||
|
||||
private TValue? GetPropertyValue<TValue>(PropertyConfig config, object? listItem = null) {
|
||||
if (!config.DisplayValue) return default;
|
||||
if (Content.CurrentObject is null) return default;
|
||||
|
||||
if (listItem is not null) {
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, listItem);
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, null, listItem).Result;
|
||||
}
|
||||
|
||||
var value = config.Info.GetValue(Content.CurrentObject);
|
||||
|
||||
var value = GetNewestValue(config);
|
||||
|
||||
if (value is null)
|
||||
return default;
|
||||
@@ -213,7 +224,7 @@
|
||||
return (TValue)value;
|
||||
|
||||
if (typeof(TValue) == typeof(string))
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config);
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, value).Result;
|
||||
|
||||
return (TValue)Convert.ChangeType(value, typeof(TValue));
|
||||
}
|
||||
@@ -277,15 +288,19 @@
|
||||
else {
|
||||
needsOverride = false;
|
||||
|
||||
if (!typeof(IList).IsAssignableFrom(config.Info.PropertyType)) {
|
||||
throw new ArgumentException($"Invalid type of '{config.Name}' property in '{config.Table.DisplayName}' table, only list types are supported on enumerable relations.");
|
||||
var newItems = ((IEnumerable)value).OfType<object>();
|
||||
|
||||
var collection = Activator.CreateInstance(config.Info.PropertyType);
|
||||
var addMethod = config.Info.PropertyType.GetMethod(nameof(ICollection<object>.Add));
|
||||
|
||||
if (addMethod is null)
|
||||
throw new ArgumentException($"Cannot modify property '{config.Name}' on table '{config.Table}' because no 'Add' method is implemented");
|
||||
|
||||
foreach (var item in newItems) {
|
||||
addMethod.Invoke(collection, [item]);
|
||||
}
|
||||
|
||||
var asList = (IList)config.Info.GetValue(Content.CurrentObject)!;
|
||||
asList.Clear();
|
||||
foreach (var element in (IEnumerable)value) {
|
||||
asList.Add(element);
|
||||
}
|
||||
_changes.Add(new PropertyChange(config.Info, collection));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -295,11 +310,28 @@
|
||||
}
|
||||
|
||||
if (config.Parser is not null && result is not null) {
|
||||
result = config.Parser(result.ToString()!, Provider);
|
||||
result = await config.Parser(result.ToString()!, Provider);
|
||||
}
|
||||
|
||||
if (needsOverride)
|
||||
config.Info.SetValue(Content.CurrentObject, result);
|
||||
_changes.Add(new PropertyChange(config.Info, result));
|
||||
}
|
||||
|
||||
private void ApplyChanges(object entry) {
|
||||
foreach (var prop in Content.Config.Properties) {
|
||||
var newValue = GetNewestValue(prop);
|
||||
prop.SetValue(entry, newValue, Provider);
|
||||
}
|
||||
}
|
||||
|
||||
private object? GetNewestValue(PropertyConfig config) {
|
||||
var value = config.GetValue(Content.CurrentObject, Provider);
|
||||
|
||||
var change = _changes.LastOrDefault(c => c.Property == config.Info);
|
||||
if (change is not null)
|
||||
value = change.Value;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private async Task OpenRelationalPicker(PropertyConfig config) {
|
||||
@@ -308,7 +340,7 @@
|
||||
|
||||
var relationType = config.Info.PropertyType;
|
||||
if (config.IsEnumerable) {
|
||||
relationType = config.Info.PropertyType.GetGenericArguments().First();
|
||||
relationType = relationType.GetGenericArguments().First();
|
||||
}
|
||||
|
||||
var relationTable = Explorer.GetTable(relationType);
|
||||
@@ -321,7 +353,7 @@
|
||||
}
|
||||
}
|
||||
else {
|
||||
var raw = config.Info.GetValue(Content.CurrentObject);
|
||||
var raw = GetNewestValue(config);
|
||||
if (raw is not null)
|
||||
currentValues.Add(raw);
|
||||
}
|
||||
@@ -339,11 +371,11 @@
|
||||
return false;
|
||||
|
||||
foreach (var property in Content.Config.Properties) {
|
||||
if (property.IsListingProperty) continue;
|
||||
if (property.IsVirtualProperty) continue;
|
||||
|
||||
var errorList = _validationErrors[property.Info.Name];
|
||||
var errorList = _validationErrors[property.Name];
|
||||
errorList.Clear();
|
||||
var value = property.Info.GetValue(Content.CurrentObject);
|
||||
var value = GetNewestValue(property);
|
||||
|
||||
if (property.Validator is not null) {
|
||||
errorList.AddRange(await property.Validator.Invoke(value, Provider));
|
||||
@@ -352,6 +384,14 @@
|
||||
|
||||
if (value is null && property.IsRequired)
|
||||
errorList.Add($"{property.Name} is required");
|
||||
|
||||
var eventResult = await PluginOrchestrator.DispatchEvent(new ValidationEvent(this) {
|
||||
Errors = errorList,
|
||||
Property = property,
|
||||
Table = Content.Config
|
||||
}, _tokenSource.Token);
|
||||
|
||||
if (eventResult.IsCanceled) return false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
@@ -362,10 +402,17 @@
|
||||
if (!valid) return false;
|
||||
var dialog = await Dialogs.ShowConfirmationAsync($"Do you really want to {(_currentlyEditing ? "edit" : "create")} this entry?");
|
||||
var result = await dialog.Result;
|
||||
return !result.Cancelled;
|
||||
if (result.Cancelled) return false;
|
||||
|
||||
ApplyChanges(Content.CurrentObject!);
|
||||
return true;
|
||||
}
|
||||
|
||||
private enum InputType {
|
||||
|
||||
public void Dispose() {
|
||||
_tokenSource.Dispose();
|
||||
}
|
||||
|
||||
public enum InputType {
|
||||
Number,
|
||||
Switch,
|
||||
Date,
|
||||
|
||||
35
src/HopFrame.Web/Components/HopFrameCard.razor
Normal file
35
src/HopFrame.Web/Components/HopFrameCard.razor
Normal file
@@ -0,0 +1,35 @@
|
||||
<FluentCard Width="350px" Height="200px" Style="display: flex; flex-direction: column; background-color: var(--neutral-layer-1)">
|
||||
<h3 style="margin-bottom: 0; display: flex; align-items: center; gap: 5px">
|
||||
@if (Icon is not null) {
|
||||
<FluentIcon Value="Icon" Color="Color.Neutral" />
|
||||
}
|
||||
@Title
|
||||
</h3>
|
||||
<FluentLabel Typo="Typography.Body" Color="Color.Info" Style="margin-bottom: 0.5rem">@Subtitle</FluentLabel>
|
||||
<span>@Description</span>
|
||||
<FluentSpacer />
|
||||
<div style="display: flex">
|
||||
<FluentSpacer/>
|
||||
|
||||
<a href="@Href" style="display: inline-block">
|
||||
<FluentButton>Open</FluentButton>
|
||||
</a>
|
||||
</div>
|
||||
</FluentCard>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public required string Title { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Subtitle { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public required string Description { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public required string Href { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Icon? Icon { get; set; }
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
@using HopFrame.Web.Models
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<link rel="stylesheet" type='text/css' href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css" />
|
||||
<link rel="stylesheet" href="/_content/HopFrame.Web/hopframe.css"/>
|
||||
<link rel="stylesheet" href="@Assets["_content/HopFrame.Web/hopframe.css"]"/>
|
||||
<link rel="stylesheet" href="@Assets["_content/HopFrame.Web/HopFrame.Web.bundle.scp.css"]"/>
|
||||
<link rel="stylesheet" href="@Assets["_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css"]"/>
|
||||
<link rel="stylesheet" href="@Assets["_content/Microsoft.FluentUI.AspNetCore.Components/Microsoft.FluentUI.AspNetCore.Components.bundle.scp.css"]">
|
||||
|
||||
<FluentDesignTheme Mode="DesignThemeModes.Dark" />
|
||||
|
||||
@@ -36,10 +40,28 @@
|
||||
|
||||
@code {
|
||||
|
||||
internal static readonly List<CustomView> CustomViews = new();
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
var authorized = await Handler.IsAuthenticatedAsync(Config.BasePolicy);
|
||||
|
||||
var currentUri = "/" + Navigator.ToBaseRelativePath(Navigator.Uri);
|
||||
|
||||
if (authorized) {
|
||||
foreach (var view in CustomViews.Where(view => !string.IsNullOrWhiteSpace(view.Policy))) {
|
||||
switch (view.LinkMatch) {
|
||||
case NavLinkMatch.All when currentUri != view.Url:
|
||||
case NavLinkMatch.Prefix when !currentUri.StartsWith(view.Url):
|
||||
continue;
|
||||
}
|
||||
|
||||
authorized = await Handler.IsAuthenticatedAsync(view.Policy);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!authorized) {
|
||||
Navigator.NavigateTo((Config.LoginPageRewrite ?? "/login") + "?redirect=/" + Navigator.ToBaseRelativePath(Navigator.Uri), true);
|
||||
Navigator.NavigateTo((Config.LoginPageRewrite ?? "/login") + "?redirect=" + currentUri, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
@using HopFrame.Web.Models
|
||||
|
||||
<FluentAppBar Orientation="Orientation.Vertical" PopoverShowSearch="false" Style="background-color: var(--neutral-layer-2); height: auto">
|
||||
<FluentAppBarItem Href="/admin"
|
||||
@@ -11,6 +12,15 @@
|
||||
|
||||
<br>
|
||||
|
||||
@foreach (var view in _views) {
|
||||
<FluentAppBarItem Href="@view.Url"
|
||||
Match="@view.LinkMatch"
|
||||
IconActive="GetLinkIcon(view, IconVariant.Filled)"
|
||||
IconRest="GetLinkIcon(view, IconVariant.Regular)"
|
||||
Text="@view.Name"
|
||||
Style="margin-top: 0.25rem"/>
|
||||
}
|
||||
|
||||
@foreach (var table in _tables.OrderBy(t => t.Order).Select(t => t.DisplayName)) {
|
||||
<FluentAppBarItem Href="@("/admin/" + table.ToLower())"
|
||||
Match="NavLinkMatch.All"
|
||||
@@ -27,6 +37,7 @@
|
||||
@code {
|
||||
|
||||
private readonly List<TableConfig> _tables = [];
|
||||
private readonly List<CustomView> _views = [];
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
foreach (var table in Explorer.GetTables()) {
|
||||
@@ -34,6 +45,21 @@
|
||||
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
||||
_tables.Add(table);
|
||||
}
|
||||
|
||||
foreach (var view in HopFrameLayout.CustomViews) {
|
||||
if (!await Handler.IsAuthenticatedAsync(view.Policy)) continue;
|
||||
_views.Add(view);
|
||||
}
|
||||
}
|
||||
|
||||
internal static Icon GetLinkIcon(CustomView view, IconVariant variant) {
|
||||
var info = new IconInfo {
|
||||
Name = view.Icon,
|
||||
Variant = variant,
|
||||
Size = IconSize.Size24
|
||||
};
|
||||
|
||||
return info.GetInstance();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
@page "/admin"
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
@using HopFrame.Web.Models
|
||||
@layout HopFrameLayout
|
||||
|
||||
<PageTitle>HopFrame</PageTitle>
|
||||
|
||||
<div style="padding: 1.5rem 1.5rem;">
|
||||
<h2>Tables</h2>
|
||||
<h2>Pages</h2>
|
||||
|
||||
<FluentStack Orientation="Orientation.Horizontal" Wrap="true" Style="margin-top: 1.5rem">
|
||||
@foreach (var view in _views) {
|
||||
<HopFrameCard
|
||||
Title="@view.Name"
|
||||
Subtitle="@view.Policy"
|
||||
Description="@view.Description"
|
||||
Href="@view.Url"
|
||||
Icon="HopFrameSideMenu.GetLinkIcon(view, IconVariant.Regular)"/>
|
||||
}
|
||||
|
||||
@foreach (var table in _tables.OrderBy(t => t.Order)) {
|
||||
<FluentCard Width="350px" Height="200px" Style="display: flex; flex-direction: column; background-color: var(--neutral-layer-1)">
|
||||
<h3 style="margin-bottom: 0;">@table.DisplayName</h3>
|
||||
<FluentLabel Typo="Typography.Body" Color="Color.Info" Style="margin-bottom: 0.5rem">@table.ViewPolicy</FluentLabel>
|
||||
<span>@table.Description</span>
|
||||
<FluentSpacer />
|
||||
<div style="display: flex">
|
||||
<FluentSpacer/>
|
||||
|
||||
<a href="@("/admin/" + table.DisplayName.ToLower())" style="display: inline-block">
|
||||
<FluentButton>Open</FluentButton>
|
||||
</a>
|
||||
</div>
|
||||
</FluentCard>
|
||||
<HopFrameCard
|
||||
Title="@table.DisplayName"
|
||||
Subtitle="@table.ViewPolicy"
|
||||
Description="@table.Description"
|
||||
Href="@("/admin/" + table.DisplayName.ToLower())"
|
||||
Icon="new Icons.Regular.Size24.Database()"/>
|
||||
}
|
||||
</FluentStack>
|
||||
</div>
|
||||
@@ -33,6 +36,7 @@
|
||||
@code {
|
||||
|
||||
private readonly List<TableConfig> _tables = [];
|
||||
private readonly List<CustomView> _views = [];
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
foreach (var table in Explorer.GetTables()) {
|
||||
@@ -40,6 +44,11 @@
|
||||
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
||||
_tables.Add(table);
|
||||
}
|
||||
|
||||
foreach (var view in HopFrameLayout.CustomViews) {
|
||||
if (!await Handler.IsAuthenticatedAsync(view.Policy)) continue;
|
||||
_views.Add(view);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,10 +4,13 @@
|
||||
@implements IDisposable
|
||||
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Callbacks
|
||||
@using HopFrame.Core.Services
|
||||
@using HopFrame.Web.Models
|
||||
@using HopFrame.Web.Plugins
|
||||
@using HopFrame.Web.Plugins.Events
|
||||
@using HopFrame.Web.Services
|
||||
@using Microsoft.JSInterop
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@if (!DisplaySelection) {
|
||||
<PageTitle>@_config?.DisplayName</PageTitle>
|
||||
@@ -18,69 +21,102 @@
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
<FluentToolbar Class="hopframe-toolbar">
|
||||
<h3>@_config?.DisplayName</h3>
|
||||
@if (!DisplaySelection) {
|
||||
@if (!DisplaySelection && _buttonToggles.ShowRefreshButton) {
|
||||
<FluentButton
|
||||
IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
|
||||
OnClick="Reload"
|
||||
OnClick="@(Reload)"
|
||||
Loading="_loading"
|
||||
Style="margin-left: 10px">
|
||||
Refresh
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
<FluentSpacer />
|
||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
||||
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopLeft)) {
|
||||
<FluentButton
|
||||
IconStart="@(button.Icon?.GetInstance())"
|
||||
OnClick="@(() => button.Handler.Invoke(null!, _config!))">
|
||||
@button.Title
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
@if (_hasCreatePolicy && DisplayActions) {
|
||||
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entry</FluentButton>
|
||||
<FluentSpacer />
|
||||
<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) {
|
||||
<FluentButton OnClick="@(async () => { await CreateOrEdit(null); })">Add Entity</FluentButton>
|
||||
}
|
||||
|
||||
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopRight)) {
|
||||
<FluentButton
|
||||
IconStart="@(button.Icon?.GetInstance())"
|
||||
OnClick="@(() => button.Handler.Invoke(null!, _config!))">
|
||||
@button.Title
|
||||
</FluentButton>
|
||||
}
|
||||
</FluentToolbar>
|
||||
<FluentProgress Visible="_loading" Width="100%" />
|
||||
|
||||
<div style="display: flex; overflow-y: auto; flex-grow: 1">
|
||||
<div style="flex-grow: 1">
|
||||
<FluentDataGrid Items="_currentlyDisplayedModels.AsQueryable()">
|
||||
<FluentDataGrid Items="CurrentlyDisplayedModels.AsQueryable()">
|
||||
@if (DisplaySelection) {
|
||||
<SelectColumn
|
||||
TGridItem="object"
|
||||
SelectMode="SelectionMode"
|
||||
SelectFromEntireRow="true"
|
||||
SelectedItems="DialogData?.SelectedObjects.ToArray()"
|
||||
OnSelect="data => SelectItem(data.Item, data.Selected)"
|
||||
SelectAllChanged="SelectAll"
|
||||
SelectAll="_allSelected"
|
||||
Style="min-width: max-content; height: 44px; display: grid; align-items: center" @ref="_selectColumn" />
|
||||
OnSelect="@(data => SelectItem(data.Item, data.Selected))"
|
||||
SelectAllDisabled="true"
|
||||
Property="o => DialogData!.SelectedObjects.Contains(o)"
|
||||
Style="min-width: max-content; height: 44px; display: grid; align-items: center" />
|
||||
}
|
||||
|
||||
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
|
||||
<PropertyColumn
|
||||
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property, null)"
|
||||
Title="@property.Name" Property="o => DisplayProperty(property, o).Result"
|
||||
Style="min-width: max-content; height: 44px;"
|
||||
Sortable="@property.Sortable"/>
|
||||
}
|
||||
|
||||
@if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy)) {
|
||||
var dataIndex = 0;
|
||||
|
||||
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content">
|
||||
@{ var currentElement = _currentlyDisplayedModels.ElementAtOrDefault(dataIndex); }
|
||||
|
||||
@if (_hasUpdatePolicy) {
|
||||
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(currentElement); }">
|
||||
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.OnEntry)) {
|
||||
<FluentButton OnClick="@(() => button.Handler.Invoke(context, _config!))">
|
||||
<FluentIcon Value="@(button.Icon!.GetInstance())" />
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
@if (_hasUpdatePolicy && _buttonToggles.ShowEditButton) {
|
||||
<FluentButton aria-label="Edit entry" OnClick="@(async () => { await CreateOrEdit(context); })">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
@if (_hasDeletePolicy) {
|
||||
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(currentElement!); }">
|
||||
@if (_hasDeletePolicy && _buttonToggles.ShowDeleteButton) {
|
||||
<FluentButton aria-label="Delete entry" OnClick="@(async () => { await DeleteEntry(context); })">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
@{
|
||||
dataIndex++;
|
||||
dataIndex %= 20;
|
||||
}
|
||||
</TemplateColumn>
|
||||
}
|
||||
</FluentDataGrid>
|
||||
@@ -89,7 +125,7 @@
|
||||
|
||||
@if (_totalPages > 1) {
|
||||
<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" />
|
||||
</FluentButton>
|
||||
|
||||
@@ -104,7 +140,7 @@
|
||||
|
||||
<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" />
|
||||
</FluentButton>
|
||||
</div>
|
||||
@@ -120,13 +156,34 @@
|
||||
}
|
||||
|
||||
removeBg();
|
||||
|
||||
window.downloadFileFromStream = async (fileName, contentStreamReference) => {
|
||||
const arrayBuffer = await contentStreamReference.arrayBuffer();
|
||||
const blob = new Blob([arrayBuffer]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchorElement = document.createElement('a');
|
||||
anchorElement.href = url;
|
||||
anchorElement.download = fileName ?? '';
|
||||
anchorElement.click();
|
||||
anchorElement.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
window.triggerClick = (elt) => elt.click();
|
||||
</script>
|
||||
|
||||
<FluentToastProvider MaxToastCount="10" />
|
||||
|
||||
<InputFile style="display: none" @ref="FileInputElement" OnChange="@(OnInputFiles)"></InputFile>
|
||||
|
||||
@inject IContextExplorer Explorer
|
||||
@inject NavigationManager Navigator
|
||||
@inject IJSRuntime Js
|
||||
@inject IDialogService Dialogs
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
@inject ICallbackEmitter Emitter
|
||||
@inject IPluginOrchestrator PluginOrchestrator
|
||||
@inject ISearchSuggestionProvider SearchSuggestions
|
||||
|
||||
@code {
|
||||
|
||||
@@ -151,20 +208,27 @@
|
||||
private TableConfig? _config;
|
||||
private ITableManager? _manager;
|
||||
|
||||
private object[] _currentlyDisplayedModels = [];
|
||||
public object[] CurrentlyDisplayedModels = [];
|
||||
private int _currentPage;
|
||||
private int _totalPages;
|
||||
private string? _searchTerm;
|
||||
private bool _loading;
|
||||
private bool _isSearchActive;
|
||||
private IList<string> _searchSuggestions = [];
|
||||
private FluentSearch? _searchBox;
|
||||
|
||||
private bool _hasUpdatePolicy;
|
||||
private bool _hasDeletePolicy;
|
||||
private bool _hasCreatePolicy;
|
||||
|
||||
private SelectColumn<object>? _selectColumn;
|
||||
private bool _allSelected;
|
||||
private readonly CancellationTokenSource _tokenSource = new();
|
||||
private List<PluginButton> _pluginButtons = new();
|
||||
private DefaultButtonToggles _buttonToggles = new();
|
||||
|
||||
internal static HopFrameTablePage? CurrentInstance { get; private set; }
|
||||
|
||||
protected override void OnInitialized() {
|
||||
CurrentInstance = this;
|
||||
_config ??= Explorer.GetTable(TableDisplayName);
|
||||
|
||||
if (_config is null || (_config.Ignored && DialogData is null)) {
|
||||
@@ -178,12 +242,19 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var eventResult = await PluginOrchestrator.DispatchEvent(new TableInitializedEvent(this) {
|
||||
Table = _config!
|
||||
});
|
||||
if (eventResult.IsCanceled) return;
|
||||
_pluginButtons = eventResult.PluginButtons;
|
||||
_buttonToggles = eventResult.DefaultButtons;
|
||||
|
||||
_hasUpdatePolicy = await Handler.IsAuthenticatedAsync(_config?.UpdatePolicy);
|
||||
_hasDeletePolicy = await Handler.IsAuthenticatedAsync(_config?.DeletePolicy);
|
||||
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
||||
|
||||
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
||||
_currentlyDisplayedModels = await _manager!.LoadPage(_currentPage, PerPage).ToArrayAsync();
|
||||
CurrentlyDisplayedModels = (await _manager!.LoadPage(_currentPage, PerPage)).ToArray();
|
||||
_totalPages = await _manager.TotalPages(PerPage);
|
||||
}
|
||||
|
||||
@@ -198,6 +269,7 @@
|
||||
|
||||
public void Dispose() {
|
||||
_searchCancel.Dispose();
|
||||
_tokenSource.Dispose();
|
||||
}
|
||||
|
||||
private CancellationTokenSource _searchCancel = new();
|
||||
@@ -206,16 +278,71 @@
|
||||
_searchTerm = eventArgs.Value?.ToString();
|
||||
if (_searchTerm is null) return;
|
||||
_searchCancel = new();
|
||||
UpdateSearchSuggestions();
|
||||
|
||||
await Task.Delay(500, _searchCancel.Token);
|
||||
|
||||
var eventResult = await PluginOrchestrator.DispatchEvent(new SearchEvent(this) {
|
||||
SearchTerm = _searchTerm,
|
||||
Table = _config!,
|
||||
CurrentPage = _currentPage
|
||||
}, _tokenSource.Token);
|
||||
if (eventResult.IsCanceled) {
|
||||
if (eventResult.SearchResult is null) return;
|
||||
|
||||
CurrentlyDisplayedModels = eventResult.SearchResult.ToArray();
|
||||
_totalPages = eventResult.TotalPages;
|
||||
return;
|
||||
}
|
||||
_searchTerm = eventResult.SearchTerm;
|
||||
|
||||
await Reload();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
private async Task Reload() {
|
||||
if (!suggestion.EndsWith('='))
|
||||
await OnSearch(new() {
|
||||
Value = _searchTerm
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateSearchSuggestions() {
|
||||
if (_config is null || !_config.ShowSearchSuggestions) return;
|
||||
_searchSuggestions = SearchSuggestions.GenerateSearchSuggestions(_config, _searchTerm ?? string.Empty).ToList();
|
||||
}
|
||||
|
||||
private CancellationTokenSource _searchFocusCancel = new();
|
||||
private async Task SearchFocus() {
|
||||
_isSearchActive = true;
|
||||
await _searchFocusCancel.CancelAsync();
|
||||
_searchFocusCancel = new();
|
||||
}
|
||||
|
||||
private async Task SearchUnfocus() {
|
||||
await Task.Delay(10, _searchFocusCancel.Token);
|
||||
_isSearchActive = false;
|
||||
}
|
||||
|
||||
public async Task Reload() {
|
||||
_loading = true;
|
||||
|
||||
var eventResult = await PluginOrchestrator.DispatchEvent(new ReloadEvent(this) {
|
||||
Table = _config!
|
||||
}, _tokenSource.Token);
|
||||
if (eventResult.IsCanceled) {
|
||||
_loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_searchTerm)) {
|
||||
(var query, _totalPages) = await _manager!.Search(_searchTerm, 0, PerPage);
|
||||
_currentlyDisplayedModels = query.ToArray();
|
||||
(var query, _totalPages) = await _manager!.Search(_searchTerm, _currentPage, PerPage);
|
||||
CurrentlyDisplayedModels = query.ToArray();
|
||||
}
|
||||
else {
|
||||
await OnInitializedAsync();
|
||||
@@ -223,7 +350,16 @@
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task ChangePage(int page) {
|
||||
public async Task ChangePage(int page) {
|
||||
var eventResult = await PluginOrchestrator.DispatchEvent(new PageChangeEvent(this) {
|
||||
CurrentPage = _currentPage,
|
||||
NewPage = page,
|
||||
TotalPages = _totalPages,
|
||||
Table = _config!
|
||||
}, _tokenSource.Token);
|
||||
if (eventResult.IsCanceled) return;
|
||||
page = eventResult.NewPage;
|
||||
|
||||
if (page < 0 || page > _totalPages - 1) return;
|
||||
_currentPage = page;
|
||||
await Reload();
|
||||
@@ -234,12 +370,19 @@
|
||||
Navigator.NavigateTo("/admin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
var eventResult = await PluginOrchestrator.DispatchEvent(new DeleteEntryEvent(this) {
|
||||
Entity = element,
|
||||
Table = _config!
|
||||
}, _tokenSource.Token);
|
||||
if (eventResult.IsCanceled) return;
|
||||
|
||||
var dialog = await Dialogs.ShowConfirmationAsync("Do you really want to delete this entry?");
|
||||
var result = await dialog.Result;
|
||||
if (result.Cancelled) return;
|
||||
|
||||
await _manager!.DeleteItem(element);
|
||||
await Emitter.DispatchCallback(CallbackTypes.DeleteEntry(_config!), element);
|
||||
await Reload();
|
||||
}
|
||||
|
||||
@@ -248,6 +391,22 @@
|
||||
Navigator.NavigateTo("/admin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
HopFrameTablePageEventArgs eventArgs;
|
||||
if (element is null) {
|
||||
eventArgs = new CreateEntryEvent(this) {
|
||||
Table = _config!
|
||||
};
|
||||
}
|
||||
else {
|
||||
eventArgs = new UpdateEntryEvent(this) {
|
||||
Table = _config!,
|
||||
Entity = element
|
||||
};
|
||||
}
|
||||
|
||||
var eventResult = await PluginOrchestrator.DispatchEvent(eventArgs, _tokenSource.Token);
|
||||
if (eventResult.IsCanceled) return;
|
||||
|
||||
var panel = await Dialogs.ShowPanelAsync<HopFrameEditor>(new EditorDialogData(_config!, element), new DialogParameters {
|
||||
TrapFocus = false
|
||||
@@ -255,32 +414,58 @@
|
||||
var result = await panel.Result;
|
||||
var data = result.Data as EditorDialogData;
|
||||
|
||||
if (result.Cancelled) {
|
||||
if (data?.CurrentObject is not null)
|
||||
await _manager!.RevertChanges(data.CurrentObject);
|
||||
return;
|
||||
}
|
||||
if (result.Cancelled) return;
|
||||
|
||||
if (element is null)
|
||||
if (element is null) {
|
||||
await _manager!.AddItem(data!.CurrentObject!);
|
||||
else
|
||||
await Emitter.DispatchCallback(CallbackTypes.CreateEntry(_config!), data.CurrentObject!);
|
||||
}
|
||||
else {
|
||||
await _manager!.EditItem(data!.CurrentObject!);
|
||||
await Emitter.DispatchCallback(CallbackTypes.UpdateEntry(_config!), data.CurrentObject!);
|
||||
}
|
||||
|
||||
await Reload();
|
||||
}
|
||||
|
||||
private void SelectItem(object item, bool selected) {
|
||||
var eventResult = PluginOrchestrator.DispatchEvent(new SelectEntryEvent(this) {
|
||||
Entity = item,
|
||||
Selected = selected,
|
||||
Table = _config!
|
||||
}, _tokenSource.Token).Result;
|
||||
if (eventResult.IsCanceled) return;
|
||||
selected = eventResult.Selected;
|
||||
|
||||
if (!selected)
|
||||
DialogData?.SelectedObjects.Remove(item);
|
||||
else DialogData?.SelectedObjects.Add(item);
|
||||
DialogData!.SelectedObjects.Remove(item);
|
||||
else DialogData!.SelectedObjects.Add(item);
|
||||
}
|
||||
|
||||
private void SelectAll() {
|
||||
var selected = _currentlyDisplayedModels.Any(obj => DialogData?.SelectedObjects.Contains(obj) != true);
|
||||
foreach (var displayedModel in _currentlyDisplayedModels) {
|
||||
SelectItem(displayedModel, selected);
|
||||
private async Task<string> DisplayProperty(PropertyConfig config, object entry) {
|
||||
var display = await _manager!.DisplayProperty(entry, config);
|
||||
|
||||
if (display.Length > config.DisplayLength)
|
||||
display = display[..config.DisplayLength] + "...";
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
public InputFile? FileInputElement;
|
||||
public Func<IEnumerable<IBrowserFile>, Task>? OnFileUpload;
|
||||
private async Task OnInputFiles(InputFileChangeEventArgs e) {
|
||||
if (OnFileUpload is null) return;
|
||||
|
||||
if (e.FileCount == 1) {
|
||||
await OnFileUpload.Invoke([e.File]);
|
||||
}
|
||||
else {
|
||||
await OnFileUpload.Invoke(e.GetMultipleFiles());
|
||||
}
|
||||
|
||||
_allSelected = selected;
|
||||
}
|
||||
|
||||
public void RequestRender() {
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,3 +20,13 @@
|
||||
place-items: center;
|
||||
border-bottom: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest);
|
||||
}
|
||||
|
||||
.hopframe-search ::deep fluent-listbox {
|
||||
width: 500px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: var(--fill-color);
|
||||
z-index: 1;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
68
src/HopFrame.Web/HopFrameConfiguratorExtensions.cs
Normal file
68
src/HopFrame.Web/HopFrameConfiguratorExtensions.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Reflection;
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Web.Components.Layout;
|
||||
using HopFrame.Web.Models;
|
||||
using HopFrame.Web.Plugins;
|
||||
using HopFrame.Web.Plugins.Annotations;
|
||||
using HopFrame.Web.Plugins.Internal;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
public static class HopFrameConfiguratorExtensions {
|
||||
|
||||
/// <summary>
|
||||
/// Creates an entry to the side menu and dashboard with a custom url
|
||||
/// </summary>
|
||||
/// <param name="configurator">The configurator for the HopFrame config that is being created</param>
|
||||
/// <param name="name">The name of the navigation entry</param>
|
||||
/// <param name="url">The target url of the navigation entry</param>
|
||||
public static CustomViewConfigurator AddCustomView(this HopFrameConfigurator configurator, string name, string url) {
|
||||
var view = new CustomView {
|
||||
Name = name,
|
||||
Url = url
|
||||
};
|
||||
HopFrameLayout.CustomViews.Add(view);
|
||||
return new CustomViewConfigurator(view);
|
||||
}
|
||||
|
||||
/// <param name="configuratorDelegate">The delegate for configuring the view</param>
|
||||
/// <inheritdoc cref="AddCustomView(HopFrame.Core.Config.HopFrameConfigurator,string,string)"/>
|
||||
public static HopFrameConfigurator AddCustomView(this HopFrameConfigurator configurator, string name, string url, Action<CustomViewConfigurator> configuratorDelegate) {
|
||||
var viewConfigurator = AddCustomView(configurator, name, url);
|
||||
configuratorDelegate.Invoke(viewConfigurator);
|
||||
return configurator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a plugin
|
||||
/// </summary>
|
||||
/// <param name="configurator">The configurator for the HopFrame config that is being created</param>
|
||||
/// <typeparam name="TPlugin">The plugin that should be registered</typeparam>
|
||||
public static HopFrameConfigurator AddPlugin<TPlugin>(this HopFrameConfigurator configurator) where TPlugin : class {
|
||||
PluginOrchestrator.RegisterPlugin(configurator.ServiceCollection, typeof(TPlugin));
|
||||
|
||||
var methods = typeof(TPlugin).GetMethods()
|
||||
.Where(method => method.IsStatic)
|
||||
.Where(method => method.GetCustomAttributes(true)
|
||||
.Any(attr => attr is PluginConfiguratorAttribute))
|
||||
.Where(method => method.GetParameters().Length < 2);
|
||||
|
||||
foreach (var method in methods) {
|
||||
if (method.GetParameters().Length > 0)
|
||||
method.Invoke(null, [configurator]);
|
||||
else method.Invoke(null, []);
|
||||
}
|
||||
|
||||
return configurator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the Exporter Plugin for data import/export functionality.
|
||||
/// </summary>
|
||||
/// <param name="configurator">The configurator for the HopFrame configuration.</param>
|
||||
public static HopFrameConfigurator AddExporters(this HopFrameConfigurator configurator) {
|
||||
configurator.AddPlugin<ExporterPlugin>();
|
||||
return configurator;
|
||||
}
|
||||
|
||||
}
|
||||
54
src/HopFrame.Web/Models/CustomView.cs
Normal file
54
src/HopFrame.Web/Models/CustomView.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
|
||||
namespace HopFrame.Web.Models;
|
||||
|
||||
public sealed class CustomView {
|
||||
public required string Name { get; init; }
|
||||
public string? Description { get; set; }
|
||||
public string? Policy { get; set; }
|
||||
public required string Url { get; init; }
|
||||
public string Icon { get; set; } = "Window";
|
||||
public NavLinkMatch LinkMatch { get; set; } = NavLinkMatch.All;
|
||||
}
|
||||
|
||||
public sealed class CustomViewConfigurator(CustomView view) {
|
||||
public CustomView InnerConfig { get; } = view;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the description displayed in the dashboard
|
||||
/// </summary>
|
||||
/// <param name="description">The desired description</param>
|
||||
public CustomViewConfigurator SetDescription(string description) {
|
||||
InnerConfig.Description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the policy needed in order to access the view
|
||||
/// </summary>
|
||||
/// <param name="policy">The desired policy</param>
|
||||
public CustomViewConfigurator SetPolicy(string policy) {
|
||||
InnerConfig.Policy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the icon displayed in the sidebar
|
||||
/// </summary>
|
||||
/// <param name="icon">The desired <see href="https://www.fluentui-blazor.net/Icon#explorer">fluent-icon</see></param>
|
||||
public CustomViewConfigurator SetIcon(string icon) {
|
||||
InnerConfig.Icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the rule for the sidebar to determine if the link is active
|
||||
/// </summary>
|
||||
/// <param name="match">The desired match rule</param>
|
||||
public CustomViewConfigurator SetLinkMatch(NavLinkMatch match) {
|
||||
InnerConfig.LinkMatch = match;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
8
src/HopFrame.Web/Models/PropertyChange.cs
Normal file
8
src/HopFrame.Web/Models/PropertyChange.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace HopFrame.Web.Models;
|
||||
|
||||
public class PropertyChange(PropertyInfo info, object? value) {
|
||||
public object? Value { get; set; } = value;
|
||||
public PropertyInfo Property { get; set; } = info;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Plugins.Annotations;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class EventHandlerAttribute : Attribute;
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace HopFrame.Web.Plugins.Annotations;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the method as a plugin configurator, so the method gets called, when the plugin is registered.
|
||||
/// Only works on static methods
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PluginConfiguratorAttribute : Attribute;
|
||||
18
src/HopFrame.Web/Plugins/Events/EntryEvent.cs
Normal file
18
src/HopFrame.Web/Plugins/Events/EntryEvent.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using HopFrame.Web.Components.Pages;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Events;
|
||||
|
||||
public sealed class DeleteEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||
public required object Entity { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CreateEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender);
|
||||
|
||||
public sealed class UpdateEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||
public required object Entity { get; init; }
|
||||
}
|
||||
|
||||
public sealed class SelectEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||
public required object Entity { get; init; }
|
||||
public required bool Selected { get; set; }
|
||||
}
|
||||
27
src/HopFrame.Web/Plugins/Events/HopFrameEventArgs.cs
Normal file
27
src/HopFrame.Web/Plugins/Events/HopFrameEventArgs.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Web.Components.Dialogs;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Events;
|
||||
|
||||
public abstract class HopFrameEventArgs(object internalSender) {
|
||||
internal object InternalSender { get; } = internalSender;
|
||||
public bool IsCanceled { get; protected set; }
|
||||
|
||||
|
||||
public void SetCancelled(bool canceled) => IsCanceled = canceled;
|
||||
}
|
||||
|
||||
public abstract class HopFrameEventArgs<TSender>(TSender sender) : HopFrameEventArgs(sender) where TSender : class {
|
||||
public TSender Sender => (TSender)InternalSender;
|
||||
}
|
||||
|
||||
public abstract class HopFrameTablePageEventArgs(HopFrameTablePage sender)
|
||||
: HopFrameEventArgs<HopFrameTablePage>(sender) {
|
||||
public required TableConfig Table { get; init; }
|
||||
}
|
||||
|
||||
public abstract class HopFrameEditorEventArgs(HopFrameEditor sender)
|
||||
: HopFrameEventArgs<HopFrameEditor>(sender) {
|
||||
public required TableConfig Table { get; init; }
|
||||
}
|
||||
9
src/HopFrame.Web/Plugins/Events/PageChangeEvent.cs
Normal file
9
src/HopFrame.Web/Plugins/Events/PageChangeEvent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using HopFrame.Web.Components.Pages;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Events;
|
||||
|
||||
public sealed class PageChangeEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||
public required int CurrentPage { get; init; }
|
||||
public required int TotalPages { get; init; }
|
||||
public required int NewPage { get; set; }
|
||||
}
|
||||
9
src/HopFrame.Web/Plugins/Events/PluginEventContainer.cs
Normal file
9
src/HopFrame.Web/Plugins/Events/PluginEventContainer.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Events;
|
||||
|
||||
internal sealed class PluginEventContainer {
|
||||
public required MethodInfo Handler { get; init; }
|
||||
public required Type EventType { get; init; }
|
||||
public required bool IsAwaitable { get; init; }
|
||||
}
|
||||
7
src/HopFrame.Web/Plugins/Events/ReloadEvent.cs
Normal file
7
src/HopFrame.Web/Plugins/Events/ReloadEvent.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using HopFrame.Web.Components.Pages;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Events;
|
||||
|
||||
public sealed class ReloadEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||
|
||||
}
|
||||
22
src/HopFrame.Web/Plugins/Events/SearchEvent.cs
Normal file
22
src/HopFrame.Web/Plugins/Events/SearchEvent.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Collections;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Events;
|
||||
|
||||
public sealed class SearchEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||
public required string SearchTerm { get; set; }
|
||||
public required int CurrentPage { get; init; }
|
||||
internal IEnumerable<object>? SearchResult { get; set; }
|
||||
internal int TotalPages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the new search result that is being displayed<br />
|
||||
/// The event needs to be canceled in order for the custom search results to appear
|
||||
/// </summary>
|
||||
/// <param name="result">The current page of search results</param>
|
||||
/// <param name="totalPages">The total pages of search results</param>
|
||||
public void SetSearchResult(IEnumerable result, int totalPages) {
|
||||
SearchResult = result.OfType<object>();
|
||||
TotalPages = totalPages;
|
||||
}
|
||||
}
|
||||
101
src/HopFrame.Web/Plugins/Events/TableInitializedEvent.cs
Normal file
101
src/HopFrame.Web/Plugins/Events/TableInitializedEvent.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Events;
|
||||
|
||||
public class TableInitializedEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) {
|
||||
public List<PluginButton> PluginButtons { get; } = new();
|
||||
public DefaultButtonToggles DefaultButtons { get; set; } = new();
|
||||
|
||||
public void AddPageButton(string title, Func<Task> callback, bool pushRight = false, IconInfo? icon = null) {
|
||||
PluginButtons.Add(new() {
|
||||
Title = title,
|
||||
Icon = icon,
|
||||
Position = pushRight ? PluginButtonPosition.TopRight : PluginButtonPosition.TopLeft,
|
||||
Handler = (_, _) => callback.Invoke()
|
||||
});
|
||||
}
|
||||
|
||||
public void AddPageButton(string title, Action callback, bool pushRight = false, IconInfo? icon = null) {
|
||||
AddPageButton(title, () => {
|
||||
callback.Invoke();
|
||||
return Task.CompletedTask;
|
||||
}, pushRight, icon);
|
||||
}
|
||||
|
||||
public void AddEntityButton(IconInfo icon, Func<object, TableConfig, Task> callback) {
|
||||
PluginButtons.Add(new() {
|
||||
Icon = icon,
|
||||
Position = PluginButtonPosition.OnEntry,
|
||||
Handler = callback
|
||||
});
|
||||
}
|
||||
|
||||
public void AddEntityButton(IconInfo icon, Action<object, TableConfig> callback) {
|
||||
AddEntityButton(icon, (obj, cfg) => {
|
||||
callback.Invoke(obj, cfg);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public void AddEntityButton<TEntity>(IconInfo icon, Func<TEntity, TableConfig, Task> callback) {
|
||||
PluginButtons.Add(new() {
|
||||
Icon = icon,
|
||||
Position = PluginButtonPosition.OnEntry,
|
||||
Handler = (obj, cfg) => callback.Invoke((TEntity)obj, cfg),
|
||||
TableFilter = typeof(TEntity)
|
||||
});
|
||||
}
|
||||
|
||||
public void AddEntityButton<TEntity>(IconInfo icon, Action<TEntity, TableConfig> callback) {
|
||||
AddEntityButton<TEntity>(icon, (obj, cfg) => {
|
||||
callback.Invoke(obj, cfg);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public void AddPageButton<TEntity>(string title, Func<Task> callback, bool pushRight = false, IconInfo? icon = null) {
|
||||
PluginButtons.Add(new() {
|
||||
Title = title,
|
||||
Icon = icon,
|
||||
Position = pushRight ? PluginButtonPosition.TopRight : PluginButtonPosition.TopLeft,
|
||||
Handler = (_, _) => callback.Invoke(),
|
||||
TableFilter = typeof(TEntity)
|
||||
});
|
||||
}
|
||||
|
||||
public void AddPageButton<TEntity>(string title, Action callback, bool pushRight = false, IconInfo? icon = null) {
|
||||
AddPageButton<TEntity>(title, () => {
|
||||
callback.Invoke();
|
||||
return Task.CompletedTask;
|
||||
}, pushRight, icon);
|
||||
}
|
||||
}
|
||||
|
||||
public struct PluginButton {
|
||||
public PluginButtonPosition Position { get; set; }
|
||||
public Func<object, TableConfig, Task> Handler { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public IconInfo? Icon { get; set; }
|
||||
public Type? TableFilter { get; set; }
|
||||
|
||||
internal bool IsForTable(TableConfig? config) {
|
||||
if (config is null) return false;
|
||||
if (TableFilter is null) return true;
|
||||
return config.TableType == TableFilter;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PluginButtonPosition {
|
||||
TopLeft = 0,
|
||||
TopRight = 1,
|
||||
OnEntry = 2
|
||||
}
|
||||
|
||||
public struct DefaultButtonToggles() {
|
||||
public bool ShowRefreshButton { get; set; } = true;
|
||||
public bool ShowAddEntityButton { get; set; } = true;
|
||||
public bool ShowDeleteButton { get; set; } = true;
|
||||
public bool ShowEditButton { get; set; } = true;
|
||||
}
|
||||
9
src/HopFrame.Web/Plugins/Events/ValidationEvent.cs
Normal file
9
src/HopFrame.Web/Plugins/Events/ValidationEvent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Web.Components.Dialogs;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Events;
|
||||
|
||||
public sealed class ValidationEvent(HopFrameEditor sender) : HopFrameEditorEventArgs(sender) {
|
||||
public required IList<string> Errors { get; init; }
|
||||
public required PropertyConfig Property { get; init; }
|
||||
}
|
||||
7
src/HopFrame.Web/Plugins/IPluginOrchestrator.cs
Normal file
7
src/HopFrame.Web/Plugins/IPluginOrchestrator.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using HopFrame.Web.Plugins.Events;
|
||||
|
||||
namespace HopFrame.Web.Plugins;
|
||||
|
||||
public interface IPluginOrchestrator {
|
||||
public Task<TEvent> DispatchEvent<TEvent>(TEvent @event, CancellationToken ct = new()) where TEvent : HopFrameEventArgs;
|
||||
}
|
||||
211
src/HopFrame.Web/Plugins/Internal/ExporterPlugin.cs
Normal file
211
src/HopFrame.Web/Plugins/Internal/ExporterPlugin.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using System.Collections;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Services;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using HopFrame.Web.Plugins.Annotations;
|
||||
using HopFrame.Web.Plugins.Events;
|
||||
using HopFrame.Web.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Internal;
|
||||
|
||||
internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService toasts, IFileService files) {
|
||||
public const char Separator = ';';
|
||||
|
||||
[EventHandler]
|
||||
public void OnInit(TableInitializedEvent e) {
|
||||
if (e.Sender.DialogData is not null) return;
|
||||
|
||||
e.AddPageButton("Export", () => Export(e.Table), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowUpload());
|
||||
e.AddPageButton("Import", () => Import(e.Table, e.Sender), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowDownload());
|
||||
}
|
||||
|
||||
private async Task Export(TableConfig table) {
|
||||
var manager = explorer.GetTableManager(table.PropertyName);
|
||||
if (manager is null) {
|
||||
toasts.ShowError("Data could not be exported!");
|
||||
return;
|
||||
}
|
||||
|
||||
var data = await manager
|
||||
.LoadPage(0, int.MaxValue);
|
||||
|
||||
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).ToArray();
|
||||
|
||||
var csv = new StringBuilder(string.Join(Separator, properties.Select(prop => prop.Info.Name)) + '\n');
|
||||
foreach (var entry in data) {
|
||||
var row = new List<string>();
|
||||
|
||||
foreach (var property in properties) {
|
||||
row.Add(FormatProperty(property, entry));
|
||||
}
|
||||
|
||||
csv.Append(string.Join(Separator, row) + '\n');
|
||||
}
|
||||
|
||||
var result = csv.ToString();
|
||||
await files.DownloadFile($"{table.DisplayName}.csv", Encoding.UTF8.GetBytes(result));
|
||||
}
|
||||
|
||||
private async Task Import(TableConfig table, HopFrameTablePage target) {
|
||||
var file = await files.UploadFile();
|
||||
|
||||
var stream = file.OpenReadStream();
|
||||
var reader = new StreamReader(stream);
|
||||
|
||||
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).ToArray();
|
||||
var data = await reader.ReadToEndAsync();
|
||||
var rows = data.Split('\n');
|
||||
|
||||
reader.Dispose();
|
||||
await stream.DisposeAsync();
|
||||
|
||||
var headerProps = rows.First().Split(Separator);
|
||||
if (!headerProps.Any(h => properties.Any(prop => prop.Info.Name == h))) {
|
||||
toasts.ShowError("Table header in csv is not valid!");
|
||||
return;
|
||||
}
|
||||
|
||||
var elements = new List<object>();
|
||||
for (int rowIndex = 1; rowIndex < rows.Length; rowIndex++) {
|
||||
var row = rows[rowIndex];
|
||||
if (string.IsNullOrWhiteSpace(row)) continue;
|
||||
|
||||
var element = Activator.CreateInstance(table.TableType)!;
|
||||
|
||||
var rowValues = row.Split(Separator);
|
||||
for (int i = 0; i < headerProps.Length; i++) {
|
||||
var property = properties.FirstOrDefault(prop => prop.Info.Name == headerProps[i]);
|
||||
if (property is null) continue;
|
||||
|
||||
object? value = rowValues[i];
|
||||
if (string.IsNullOrWhiteSpace((string)value)) continue;
|
||||
|
||||
if (property.IsEnumerable) {
|
||||
if (!property.Info.PropertyType.IsGenericType) continue;
|
||||
|
||||
var formattedEnumerable = (string)value;
|
||||
if (formattedEnumerable == "[]") continue;
|
||||
var values = formattedEnumerable
|
||||
.TrimStart('[')
|
||||
.TrimEnd(']')
|
||||
.Split(',');
|
||||
|
||||
var addMethod = property.Info.PropertyType.GetMethod("Add");
|
||||
if (addMethod is null) continue;
|
||||
|
||||
var tableType = property.Info.PropertyType.GenericTypeArguments[0];
|
||||
var relationManager = explorer.GetTableManager(tableType);
|
||||
var primaryKeyType = GetPrimaryKeyType(tableType);
|
||||
if (relationManager is null || primaryKeyType is null) continue;
|
||||
|
||||
var enumerable = Activator.CreateInstance(property.Info.PropertyType);
|
||||
foreach (var key in values) {
|
||||
var entry = await relationManager.GetOne(ParseString(key, primaryKeyType)!);
|
||||
if (entry is null) continue;
|
||||
|
||||
addMethod.Invoke(enumerable, [entry]);
|
||||
}
|
||||
|
||||
property.Info.SetValue(element, enumerable);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.IsRelation) {
|
||||
var relationManager = explorer.GetTableManager(property.Info.PropertyType);
|
||||
var relationPrimaryKeyType = GetPrimaryKeyType(property.Info.PropertyType);
|
||||
if (relationManager is null || relationPrimaryKeyType is null) continue;
|
||||
value = await relationManager.GetOne(ParseString((string)value, relationPrimaryKeyType)!);
|
||||
}
|
||||
else if (property.Info.PropertyType == typeof(Guid)) {
|
||||
var success = Guid.TryParse((string)value, out var guid);
|
||||
if (success) value = guid;
|
||||
else toasts.ShowError($"'{value}' is not a valid guid");
|
||||
}
|
||||
else {
|
||||
value = ParseString((string)value, property.Info.PropertyType);
|
||||
}
|
||||
|
||||
property.Info.SetValue(element, value);
|
||||
}
|
||||
|
||||
elements.Add(element);
|
||||
}
|
||||
|
||||
var manager = explorer.GetTableManager(table.PropertyName);
|
||||
if (manager is null) {
|
||||
toasts.ShowError("Data could not be imported!");
|
||||
return;
|
||||
}
|
||||
|
||||
await manager.AddAll(elements);
|
||||
await target.Reload();
|
||||
}
|
||||
|
||||
private string FormatProperty(PropertyConfig property, object entity) {
|
||||
var value = property.Info.GetValue(entity);
|
||||
|
||||
if (value is null)
|
||||
return string.Empty;
|
||||
|
||||
if (property.IsEnumerable) {
|
||||
var enumerable = (IEnumerable)value;
|
||||
return '[' + string.Join(',', enumerable.OfType<object>().Select(o => SelectPrimaryKey(o, property) ?? o.ToString())) + ']';
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
return entity
|
||||
.GetType()
|
||||
.GetProperties()
|
||||
.FirstOrDefault(prop => prop
|
||||
.GetCustomAttributes(true)
|
||||
.Any(attr => attr is KeyAttribute))?
|
||||
.GetValue(entity)?
|
||||
.ToString();
|
||||
}
|
||||
|
||||
private Type? GetPrimaryKeyType(Type tableType) {
|
||||
var table = explorer.GetTable(tableType);
|
||||
if (table?.ContextConfig is RepositoryGroupConfig repoConfig) {
|
||||
return repoConfig.KeyProperty.PropertyType;
|
||||
}
|
||||
|
||||
return tableType
|
||||
.GetProperties()
|
||||
.FirstOrDefault(prop => prop
|
||||
.GetCustomAttributes(true)
|
||||
.Any(attr => attr is KeyAttribute))?
|
||||
.PropertyType;
|
||||
}
|
||||
|
||||
private object? ParseString(string input, Type targetType) {
|
||||
try {
|
||||
var parseMethod = targetType
|
||||
.GetMethods()
|
||||
.Where(method => method.Name.StartsWith("Parse"))
|
||||
.FirstOrDefault(method => method.GetParameters().SingleOrDefault()?.ParameterType == typeof(string));
|
||||
|
||||
if (parseMethod is not null)
|
||||
return parseMethod.Invoke(null, [input]);
|
||||
|
||||
return Convert.ChangeType(input, targetType);
|
||||
}
|
||||
catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
57
src/HopFrame.Web/Plugins/Internal/PluginOrchestrator.cs
Normal file
57
src/HopFrame.Web/Plugins/Internal/PluginOrchestrator.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using HopFrame.Web.Plugins.Annotations;
|
||||
using HopFrame.Web.Plugins.Events;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Web.Plugins.Internal;
|
||||
|
||||
internal sealed class PluginOrchestrator(IServiceProvider services) : IPluginOrchestrator {
|
||||
|
||||
public static void RegisterPlugin(IServiceCollection collection, Type plugin) {
|
||||
var methods = plugin.GetMethods()
|
||||
.Where(method => method.GetCustomAttributes(true)
|
||||
.Any(attr => attr is EventHandlerAttribute));
|
||||
|
||||
foreach (var method in methods) {
|
||||
var awaitable = method.ReturnType.IsAssignableFrom(typeof(Task));
|
||||
var eventType = method
|
||||
.GetParameters()
|
||||
.FirstOrDefault(param => param.ParameterType.IsAssignableTo(typeof(HopFrameEventArgs)))?.ParameterType;
|
||||
|
||||
if (eventType is null) continue;
|
||||
var container = new PluginEventContainer {
|
||||
EventType = eventType,
|
||||
IsAwaitable = awaitable,
|
||||
Handler = method
|
||||
};
|
||||
collection.AddSingleton(container);
|
||||
collection.AddScoped(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TEvent> DispatchEvent<TEvent>(TEvent @event, CancellationToken ct = new()) where TEvent : HopFrameEventArgs {
|
||||
var eventContainers = services.GetRequiredService<IEnumerable<PluginEventContainer>>()
|
||||
.Where(container => container.EventType == typeof(TEvent));
|
||||
|
||||
var eventType = typeof(TEvent);
|
||||
var tokenType = typeof(CancellationToken);
|
||||
foreach (var container in eventContainers) {
|
||||
var plugin = services.GetRequiredService(container.Handler.DeclaringType!);
|
||||
var parameters = new List<object?>();
|
||||
|
||||
foreach (var parameter in container.Handler.GetParameters()) {
|
||||
if (parameter.ParameterType == eventType)
|
||||
parameters.Add(@event);
|
||||
else if (parameter.ParameterType == tokenType)
|
||||
parameters.Add(ct);
|
||||
else parameters.Add(null);
|
||||
}
|
||||
|
||||
var result = container.Handler.Invoke(plugin, parameters.ToArray());
|
||||
|
||||
if (container.IsAwaitable)
|
||||
await (Task)result!;
|
||||
}
|
||||
|
||||
return @event;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
using HopFrame.Core;
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Callbacks;
|
||||
using HopFrame.Web.Components;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using HopFrame.Web.Plugins;
|
||||
using HopFrame.Web.Plugins.Internal;
|
||||
using HopFrame.Web.Services;
|
||||
using HopFrame.Web.Services.Implementation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
@@ -15,11 +22,12 @@ public static class ServiceCollectionExtensions {
|
||||
/// <param name="services">The service collection to add the services to</param>
|
||||
/// <param name="configurator">The configurator used to build the HopFrame configuration</param>
|
||||
/// <param name="fluentUiLibraryConfiguration">The configuration for the FluentUI components</param>
|
||||
/// <param name="addRazorComponents">Set this to false if you don't want to automatically configure razor components with interactive server components</param>
|
||||
/// <returns>The same service collection that is passed in</returns>
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator, LibraryConfiguration? fluentUiLibraryConfiguration = null) {
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator, LibraryConfiguration? fluentUiLibraryConfiguration = null, bool addRazorComponents = true) {
|
||||
var config = new HopFrameConfig();
|
||||
configurator.Invoke(new HopFrameConfigurator(config));
|
||||
return AddHopFrame(services, config, fluentUiLibraryConfiguration);
|
||||
configurator.Invoke(new HopFrameConfigurator(config, services));
|
||||
return AddHopFrame(services, config, fluentUiLibraryConfiguration, addRazorComponents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -28,22 +36,50 @@ public static class ServiceCollectionExtensions {
|
||||
/// <param name="services">The service collection to add the services to</param>
|
||||
/// <param name="config">The config used for the HopFrame admin ui</param>
|
||||
/// <param name="fluentUiLibraryConfiguration">The configuration for the FluentUI components</param>
|
||||
/// <param name="addRazorComponents">Set this to false if you don't want to automatically configure razor components with interactive server components</param>
|
||||
/// <returns>The same service collection that is passed in</returns>
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, HopFrameConfig config, LibraryConfiguration? fluentUiLibraryConfiguration = null) {
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, HopFrameConfig config, LibraryConfiguration? fluentUiLibraryConfiguration = null, bool addRazorComponents = true) {
|
||||
services.AddSingleton(config);
|
||||
services.AddHopFrameServices();
|
||||
services.AddFluentUIComponents(fluentUiLibraryConfiguration);
|
||||
|
||||
services.AddScoped<IPluginOrchestrator, PluginOrchestrator>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
services.TryAddScoped<ISearchSuggestionProvider, SearchSuggestionProvider>();
|
||||
|
||||
if (addRazorComponents) {
|
||||
services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps the HopFrame admin ui endpoints
|
||||
/// Adds the HopFrame admin ui endpoints
|
||||
/// </summary>
|
||||
/// <seealso cref="AddHopFramePages"/>
|
||||
[Obsolete($"Use '{nameof(AddHopFramePages)}' instead")]
|
||||
public static RazorComponentsEndpointConventionBuilder MapHopFramePages(this RazorComponentsEndpointConventionBuilder builder) {
|
||||
return AddHopFramePages(builder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the HopFrame admin ui endpoints
|
||||
/// </summary>
|
||||
public static RazorComponentsEndpointConventionBuilder AddHopFramePages(this RazorComponentsEndpointConventionBuilder builder) {
|
||||
builder
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddAdditionalAssemblies(typeof(HopFrameHome).Assembly);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplication MapHopFrame(this WebApplication app) {
|
||||
app.UseAntiforgery();
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
return app;
|
||||
}
|
||||
|
||||
}
|
||||
23
src/HopFrame.Web/Services/IFileService.cs
Normal file
23
src/HopFrame.Web/Services/IFileService.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace HopFrame.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides file handling capabilities for downloading and uploading files.
|
||||
/// </summary>
|
||||
public interface IFileService {
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a file download with the specified name and data.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the file to be downloaded.</param>
|
||||
/// <param name="data">The byte array representing the file's content.</param>
|
||||
public Task DownloadFile(string name, byte[] data);
|
||||
|
||||
/// <summary>
|
||||
/// Allows the user to upload a file and returns the uploaded file for processing.
|
||||
/// </summary>
|
||||
/// <returns>A task that returns an IBrowserFile representing the uploaded file.</returns>
|
||||
public Task<IBrowserFile> UploadFile();
|
||||
|
||||
}
|
||||
11
src/HopFrame.Web/Services/ISearchSuggestionProvider.cs
Normal file
11
src/HopFrame.Web/Services/ISearchSuggestionProvider.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using HopFrame.Core.Config;
|
||||
|
||||
namespace HopFrame.Web.Services;
|
||||
|
||||
public interface ISearchSuggestionProvider {
|
||||
|
||||
public IEnumerable<string> GenerateSearchSuggestions(TableConfig table, string searchText);
|
||||
|
||||
public string CompleteSearchSuggestion(TableConfig table, string searchText, string selectedSuggestion);
|
||||
|
||||
}
|
||||
31
src/HopFrame.Web/Services/Implementation/FileService.cs
Normal file
31
src/HopFrame.Web/Services/Implementation/FileService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace HopFrame.Web.Services.Implementation;
|
||||
|
||||
internal sealed class FileService(IJSRuntime runtime) : IFileService {
|
||||
|
||||
public async Task DownloadFile(string name, byte[] data) {
|
||||
using var stream = new DotNetStreamReference(new MemoryStream(data));
|
||||
|
||||
await runtime.InvokeVoidAsync("downloadFileFromStream", name, stream);
|
||||
}
|
||||
|
||||
public Task<IBrowserFile> UploadFile() {
|
||||
var result = new TaskCompletionSource<IBrowserFile>();
|
||||
|
||||
if (HopFrameTablePage.CurrentInstance is null)
|
||||
result.SetException(new InvalidOperationException("No table page visible"));
|
||||
|
||||
HopFrameTablePage.CurrentInstance!.OnFileUpload = files => {
|
||||
result.SetResult(files.First());
|
||||
HopFrameTablePage.CurrentInstance.OnFileUpload = null;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
runtime.InvokeVoidAsync("triggerClick", HopFrameTablePage.CurrentInstance.FileInputElement!.Element);
|
||||
return result.Task;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,3 +40,36 @@
|
||||
fluent-option {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
body {
|
||||
--body-font: "Segoe UI Variable", "Segoe UI", sans-serif;
|
||||
font-family: var(--body-font), sans-serif;
|
||||
font-size: var(--type-ramp-base-font-size);
|
||||
line-height: var(--type-ramp-base-line-height);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: var(--neutral-layer-4);
|
||||
color: var(--neutral-foreground-rest);
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--neutral-foreground-rest);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
footer a:focus {
|
||||
outline: 1px dashed;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.column-header.select-all > svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
18
testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj
Normal file
18
testing/HopFrame.Testing.Api/HopFrame.Testing.Api.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HopFrame.Testing\HopFrame.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
51
testing/HopFrame.Testing.Api/Program.cs
Normal file
51
testing/HopFrame.Testing.Api/Program.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using HopFrame.Testing;
|
||||
using HopFrame.Web;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddHopFrame(options => {
|
||||
options.AddDbContext<DatabaseContext>();
|
||||
});
|
||||
|
||||
builder.Services.AddDbContext<DatabaseContext>(options => {
|
||||
options.UseInMemoryDatabase("testing.web");
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment()) {
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
var summaries = new[] {
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
app.MapGet("/weatherforecast", () => {
|
||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
||||
new WeatherForecast
|
||||
(
|
||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
Random.Shared.Next(-20, 55),
|
||||
summaries[Random.Shared.Next(summaries.Length)]
|
||||
))
|
||||
.ToArray();
|
||||
return forecast;
|
||||
})
|
||||
.WithName("GetWeatherForecast");
|
||||
|
||||
app.MapHopFrame();
|
||||
|
||||
app.Run();
|
||||
|
||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) {
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
23
testing/HopFrame.Testing.Api/Properties/launchSettings.json
Normal file
23
testing/HopFrame.Testing.Api/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5115",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7129;http://localhost:5115",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
testing/HopFrame.Testing.Api/appsettings.json
Normal file
9
testing/HopFrame.Testing.Api/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
@page "/counter"
|
||||
@using HopFrame.Web.Components.Layout
|
||||
@rendermode InteractiveServer
|
||||
@layout HopFrameLayout
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbCont
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Post>()
|
||||
.HasOne<User>(p => p.Author)
|
||||
.WithMany(u => u.Posts)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.HasOne(p => p.Author)
|
||||
.WithMany(u => u.Posts);
|
||||
}
|
||||
}
|
||||
59
testing/HopFrame.Testing/Models/Guest.cs
Normal file
59
testing/HopFrame.Testing/Models/Guest.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using HopFrame.Core.Repositories;
|
||||
|
||||
namespace HopFrame.Testing.Models;
|
||||
|
||||
public class Guest {
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<Message> Messages { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GuestRepository : IHopFrameRepository<Guest, int> {
|
||||
|
||||
public List<Guest> Guests { get; } = new();
|
||||
|
||||
public async Task<IEnumerable<Guest>> LoadPage(int page, int perPage) {
|
||||
return Guests
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage);
|
||||
}
|
||||
|
||||
public async Task<SearchResult<Guest>> Search(string searchTerm, int page, int perPage) {
|
||||
var results = Guests
|
||||
.Where(message => message.Name.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
var totalPages = (int)Math.Ceiling(results.Length / (double)perPage);
|
||||
return new SearchResult<Guest>(results
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage), totalPages);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalPageCount(int perPage) {
|
||||
return (int)Math.Ceiling(Guests.Count / (double)perPage);
|
||||
}
|
||||
|
||||
public Task CreateItem(Guest item) {
|
||||
Guests.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task EditItem(Guest item) {
|
||||
var old = Guests.Find(m => m.Id == item.Id);
|
||||
if (old is not null)
|
||||
Guests.Remove(old);
|
||||
|
||||
Guests.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteItem(Guest item) {
|
||||
Guests.Remove(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<Guest?> GetOne(int key) {
|
||||
return Guests.Find(m => m.Id == key);
|
||||
}
|
||||
}
|
||||
59
testing/HopFrame.Testing/Models/Message.cs
Normal file
59
testing/HopFrame.Testing/Models/Message.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using HopFrame.Core.Repositories;
|
||||
|
||||
namespace HopFrame.Testing.Models;
|
||||
|
||||
public class Message {
|
||||
public required int MessageIdentifier { get; set; }
|
||||
public required User Sender { get; set; }
|
||||
public required Guest Receiver { get; set; }
|
||||
public required string Content { get; set; }
|
||||
}
|
||||
|
||||
public class MessageRepository : IHopFrameRepository<Message, int> {
|
||||
|
||||
public List<Message> Messages { get; } = new();
|
||||
|
||||
public async Task<IEnumerable<Message>> LoadPage(int page, int perPage) {
|
||||
return Messages
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage);
|
||||
}
|
||||
|
||||
public async Task<SearchResult<Message>> Search(string searchTerm, int page, int perPage) {
|
||||
var results = Messages
|
||||
.Where(message => message.Content.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
var totalPages = (int)Math.Ceiling(results.Length / (double)perPage);
|
||||
return new SearchResult<Message>(results
|
||||
.Skip(page * perPage)
|
||||
.Take(perPage), totalPages);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalPageCount(int perPage) {
|
||||
return (int)Math.Ceiling(Messages.Count / (double)perPage);
|
||||
}
|
||||
|
||||
public Task CreateItem(Message item) {
|
||||
Messages.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task EditItem(Message item) {
|
||||
var old = Messages.Find(m => m.MessageIdentifier == item.MessageIdentifier);
|
||||
if (old is not null)
|
||||
Messages.Remove(old);
|
||||
|
||||
Messages.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteItem(Message item) {
|
||||
Messages.Remove(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<Message?> GetOne(int key) {
|
||||
return Messages.Find(m => m.MessageIdentifier == key);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,4 @@ public class User {
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public virtual List<Post> Posts { get; set; } = new();
|
||||
|
||||
public override string ToString() {
|
||||
return Username;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
using System.Collections;
|
||||
using HopFrame.Testing;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
using HopFrame.Testing.Components;
|
||||
using HopFrame.Testing.Models;
|
||||
using HopFrame.Web;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Message = HopFrame.Testing.Models.Message;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -36,6 +35,11 @@ builder.Services.AddHopFrame(options => {
|
||||
.SetOrderIndex(3);
|
||||
|
||||
table.AddVirtualProperty("Name", (user, _) => $"{user.FirstName} {user.LastName}")
|
||||
/*.SetVirtualParser((model, input, _) => {
|
||||
var split = input.Split(' ');
|
||||
model.FirstName = split.FirstOrDefault();
|
||||
model.LastName = split.LastOrDefault();
|
||||
})*/
|
||||
.SetOrderIndex(2);
|
||||
|
||||
table.SetDisplayName("Benutzer");
|
||||
@@ -47,9 +51,14 @@ builder.Services.AddHopFrame(options => {
|
||||
.FormatEach<Post>((post, _) => post.Caption);
|
||||
});
|
||||
|
||||
context.Table<Post>()
|
||||
.ShowSearchSuggestions(false);
|
||||
|
||||
context.Table<Post>()
|
||||
.Property(p => p.Author)
|
||||
.Format((user, _) => $"{user.FirstName} {user.LastName}");
|
||||
//.Format((user, _) => $"{user.FirstName} {user.LastName}")
|
||||
.SetDisplayedProperty(u => u.Username)
|
||||
.SetValidator((_, _) => []);
|
||||
|
||||
context.Table<Post>()
|
||||
.Property(p => p.Id)
|
||||
@@ -60,6 +69,7 @@ builder.Services.AddHopFrame(options => {
|
||||
|
||||
context.Table<Post>()
|
||||
.Property(p => p.Content)
|
||||
.SetDisplayLength(100)
|
||||
.IsTextArea(true)
|
||||
/*.Validator(input => {
|
||||
var errors = new List<string>();
|
||||
@@ -73,11 +83,41 @@ builder.Services.AddHopFrame(options => {
|
||||
return errors;
|
||||
})*/;
|
||||
|
||||
context.Table<Post>()
|
||||
.SetOrderIndex(-1);
|
||||
/*context.Table<Post>()
|
||||
.SetOrderIndex(-1)
|
||||
.Ignore(true);*/
|
||||
});
|
||||
|
||||
options.AddCustomView("Counter", "/counter")
|
||||
.SetDescription("A custom view")
|
||||
.SetPolicy("counter.view");
|
||||
|
||||
options.AddExporters();
|
||||
|
||||
options.AddCustomRepository<GuestRepository, Guest, int>(g => g.Id, table => {
|
||||
table.SetDisplayName("Guests");
|
||||
|
||||
table.Property(g => g.Messages)
|
||||
.ForceRelation(true)
|
||||
.FormatEach<Message>((m, _) => m.Content);
|
||||
});
|
||||
|
||||
options.AddCustomRepository<MessageRepository, Message, int>(m => m.MessageIdentifier, table => {
|
||||
table.SetDisplayName("Messages");
|
||||
|
||||
table.Property(m => m.Receiver)
|
||||
.ForceRelation()
|
||||
.Format((u, _) => u.Name);
|
||||
|
||||
table.Property(m => m.Sender)
|
||||
.ForceRelation()
|
||||
.Format((u, _) => u.Username ?? string.Empty);
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<MessageRepository>();
|
||||
builder.Services.AddSingleton<GuestRepository>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
@@ -94,6 +134,6 @@ app.UseAntiforgery();
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.MapHopFramePages();
|
||||
.AddHopFramePages();
|
||||
|
||||
app.Run();
|
||||
@@ -8,7 +8,7 @@ public class DbContextConfiguratorTests {
|
||||
[Fact]
|
||||
public void Table_WithConfigurator_InvokesConfigurator() {
|
||||
// Arrange
|
||||
var dbContextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var dbContextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var configurator = new DbContextConfigurator<MockDbContext>(dbContextConfig);
|
||||
var mockConfigurator = new Mock<Action<TableConfigurator<MockModel>>>();
|
||||
|
||||
@@ -22,7 +22,7 @@ public class DbContextConfiguratorTests {
|
||||
[Fact]
|
||||
public void Table_ReturnsCorrectTableConfigurator() {
|
||||
// Arrange
|
||||
var dbContextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var dbContextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var configurator = new DbContextConfigurator<MockDbContext>(dbContextConfig);
|
||||
|
||||
// Act
|
||||
|
||||
@@ -9,7 +9,7 @@ public class PropertyConfiguratorTests {
|
||||
public void SetDisplayName_SetsNameProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
var displayName = "ID";
|
||||
|
||||
@@ -24,7 +24,7 @@ public class PropertyConfiguratorTests {
|
||||
public void List_SetsListAndSearchableProperties() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
|
||||
// Act
|
||||
@@ -39,7 +39,7 @@ public class PropertyConfiguratorTests {
|
||||
public void IsSortable_SetsSortableProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
|
||||
// Act
|
||||
@@ -53,7 +53,7 @@ public class PropertyConfiguratorTests {
|
||||
public void IsSearchable_SetsSearchableProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
|
||||
// Act
|
||||
@@ -67,7 +67,7 @@ public class PropertyConfiguratorTests {
|
||||
public void SetDisplayedProperty_SetsDisplayedProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<MockModel>(propertyConfig);
|
||||
Expression<Func<MockModel, int>> propertyExpression = model => model.Id;
|
||||
|
||||
@@ -83,7 +83,7 @@ public class PropertyConfiguratorTests {
|
||||
public void Format_SetsFormatter() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
Func<int, IServiceProvider, string> formatter = (val, _) => val.ToString();
|
||||
|
||||
@@ -98,7 +98,7 @@ public class PropertyConfiguratorTests {
|
||||
public void SetParser_SetsParser() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
Func<string, IServiceProvider, int> parser = (str, _) => int.Parse(str);
|
||||
|
||||
@@ -113,7 +113,7 @@ public class PropertyConfiguratorTests {
|
||||
public void SetEditable_SetsEditableProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
|
||||
// Act
|
||||
@@ -127,7 +127,7 @@ public class PropertyConfiguratorTests {
|
||||
public void SetCreatable_SetsCreatableProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
|
||||
// Act
|
||||
@@ -141,7 +141,7 @@ public class PropertyConfiguratorTests {
|
||||
public void DisplayValue_SetsDisplayValueProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
|
||||
// Act
|
||||
@@ -155,7 +155,7 @@ public class PropertyConfiguratorTests {
|
||||
public void IsTextArea_SetsTextAreaProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
|
||||
// Act
|
||||
@@ -169,7 +169,7 @@ public class PropertyConfiguratorTests {
|
||||
public void SetTextAreaRows_SetsTextAreaRowsProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
var rows = 10;
|
||||
|
||||
@@ -184,7 +184,7 @@ public class PropertyConfiguratorTests {
|
||||
public void SetValidator_SetsValidator() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
Func<int, IServiceProvider, IEnumerable<string>> validator = (_, _) => new List<string>();
|
||||
|
||||
@@ -199,7 +199,7 @@ public class PropertyConfiguratorTests {
|
||||
public void SetOrderIndex_SetsOrderProperty() {
|
||||
// Arrange
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!,
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0), 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0), 0);
|
||||
var configurator = new PropertyConfigurator<int>(propertyConfig);
|
||||
var orderIndex = 1;
|
||||
|
||||
@@ -213,7 +213,7 @@ public class PropertyConfiguratorTests {
|
||||
[Fact]
|
||||
public void Constructor_SetsTableProperty() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
|
||||
// Act
|
||||
var propertyConfig = new PropertyConfig(typeof(MockModel).GetProperty("Id")!, tableConfig, 0);
|
||||
|
||||
@@ -9,7 +9,7 @@ public class TableConfiguratorTests {
|
||||
public void Ignore_SetsIgnoredProperty() {
|
||||
// Arrange
|
||||
var tableConfig =
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
|
||||
// Act
|
||||
@@ -22,7 +22,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void Property_ReturnsCorrectPropertyConfigurator() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
Expression<Func<MockModel, int>> propertyExpression = model => model.Id;
|
||||
|
||||
@@ -35,7 +35,7 @@ public class TableConfiguratorTests {
|
||||
|
||||
public void Property_WithConfigurator_ReturnsCorrectPropertyConfigurator() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
Expression<Func<MockModel, int>> propertyExpression = model => model.Id;
|
||||
|
||||
@@ -52,7 +52,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void AddVirtualProperty_AddsVirtualPropertyToConfig() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
Func<MockModel, IServiceProvider, string> template = (model, _) => model.Name!;
|
||||
|
||||
@@ -63,14 +63,14 @@ public class TableConfiguratorTests {
|
||||
var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName");
|
||||
Assert.NotNull(virtualProperty);
|
||||
Assert.NotNull(propertyConfigurator);
|
||||
Assert.True(virtualProperty.IsListingProperty);
|
||||
Assert.True(virtualProperty.IsVirtualProperty);
|
||||
Assert.Equal("VirtualName", virtualProperty.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddVirtualProperty_WithConfigurator_AddsVirtualPropertyToConfig() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
Func<MockModel, IServiceProvider, string> template = (model, _) => model.Name!;
|
||||
|
||||
@@ -84,14 +84,14 @@ public class TableConfiguratorTests {
|
||||
var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName");
|
||||
Assert.NotNull(virtualProperty);
|
||||
Assert.NotNull(propertyConfigurator);
|
||||
Assert.True(virtualProperty.IsListingProperty);
|
||||
Assert.True(virtualProperty.IsVirtualProperty);
|
||||
Assert.Equal("VirtualName", virtualProperty.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetDisplayName_SetsDisplayNameProperty() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
var displayName = "Mock Model Display Name";
|
||||
|
||||
@@ -105,7 +105,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void SetDescription_SetsDescriptionProperty() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
var description = "Mock Model Description";
|
||||
|
||||
@@ -119,7 +119,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void SetOrderIndex_SetsOrderIndexProperty() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
var orderIndex = 1;
|
||||
|
||||
@@ -133,7 +133,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void SetViewPolicy_SetsViewPolicyProperty() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
var policy = "ViewPolicy";
|
||||
|
||||
@@ -147,7 +147,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void SetUpdatePolicy_SetsUpdatePolicyProperty() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
var policy = "UpdatePolicy";
|
||||
|
||||
@@ -161,7 +161,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void SetCreatePolicy_SetsCreatePolicyProperty() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
var policy = "CreatePolicy";
|
||||
|
||||
@@ -175,7 +175,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void SetDeletePolicy_SetsDeletePolicyProperty() {
|
||||
// Arrange
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "MockModels", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "MockModels", 0);
|
||||
var configurator = new TableConfigurator<MockModel>(tableConfig);
|
||||
var policy = "DeletePolicy";
|
||||
|
||||
@@ -189,7 +189,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void Constructor_WithKeyProperty_DisablesEdit() {
|
||||
// Act
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel2), "Models2", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel2), "Models2", 0);
|
||||
var prop = tableConfig.Properties.SingleOrDefault(prop => prop.Info.Name == nameof(MockModel2.Id));
|
||||
|
||||
// Assert
|
||||
@@ -200,7 +200,7 @@ public class TableConfiguratorTests {
|
||||
[Fact]
|
||||
public void Constructor_WithGeneratedProperty_DisablesEditAndCreate() {
|
||||
// Act
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel2), "Models2", 0);
|
||||
var tableConfig = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel2), "Models2", 0);
|
||||
var prop = tableConfig.Properties.SingleOrDefault(prop => prop.Info.Name == nameof(MockModel2.Number));
|
||||
|
||||
// Assert
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Core.Services;
|
||||
using HopFrame.Core.Services.Implementations;
|
||||
using HopFrame.Tests.Core.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
@@ -11,7 +13,7 @@ public class ContextExplorerTests {
|
||||
public void GetTables_ReturnsNonIgnoredTables() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig1 = contextConfig.Tables[0];
|
||||
var tableConfig2 = contextConfig.Tables[1];
|
||||
config.Contexts.Add(contextConfig);
|
||||
@@ -33,7 +35,7 @@ public class ContextExplorerTests {
|
||||
public void GetTable_ByDisplayName_ReturnsCorrectTable() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig = contextConfig.Tables[0];
|
||||
config.Contexts.Add(contextConfig);
|
||||
tableConfig.DisplayName = "TestTable";
|
||||
@@ -54,7 +56,7 @@ public class ContextExplorerTests {
|
||||
public void GetTable_ByDisplayName_ReturnsNullIfTableNotFound() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig = contextConfig.Tables[0];
|
||||
config.Contexts.Add(contextConfig);
|
||||
tableConfig.DisplayName = "TestTable";
|
||||
@@ -74,7 +76,7 @@ public class ContextExplorerTests {
|
||||
public void GetTable_ByType_ReturnsCorrectTable() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig = contextConfig.Tables[0];
|
||||
config.Contexts.Add(contextConfig);
|
||||
|
||||
@@ -94,7 +96,7 @@ public class ContextExplorerTests {
|
||||
public void GetTable_ByType_ReturnsNullIfTableNotFound() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig = contextConfig.Tables[0];
|
||||
config.Contexts.Add(contextConfig);
|
||||
|
||||
@@ -113,7 +115,7 @@ public class ContextExplorerTests {
|
||||
public void GetTableManager_ReturnsCorrectTableManager() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig = new TableConfig(contextConfig, typeof(MockModel), "Models", 0);
|
||||
contextConfig.Tables.Add(tableConfig);
|
||||
config.Contexts.Add(contextConfig);
|
||||
@@ -121,6 +123,7 @@ public class ContextExplorerTests {
|
||||
var dbContext = new MockDbContext();
|
||||
var provider = new Mock<IServiceProvider>();
|
||||
provider.Setup(p => p.GetService(typeof(MockDbContext))).Returns(dbContext);
|
||||
provider.Setup(p => p.GetService(typeof(ISearchExpressionBuilder))).Returns(new Mock<ISearchExpressionBuilder>().Object);
|
||||
var contextExplorer = new ContextExplorer(config, provider.Object, new Logger<ContextExplorer>(new LoggerFactory()));
|
||||
|
||||
// Act
|
||||
@@ -135,7 +138,7 @@ public class ContextExplorerTests {
|
||||
public void GetTableManager_ReturnsNullIfDbContextNotFound() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig = new TableConfig(contextConfig, typeof(MockModel), "MockModels", 0);
|
||||
contextConfig.Tables.Add(tableConfig);
|
||||
config.Contexts.Add(contextConfig);
|
||||
@@ -154,7 +157,7 @@ public class ContextExplorerTests {
|
||||
public void GetTableManager_ReturnsNullIfTableNotFound() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig = new TableConfig(contextConfig, typeof(MockModel), "Models", 0);
|
||||
contextConfig.Tables.Add(tableConfig);
|
||||
config.Contexts.Add(contextConfig);
|
||||
@@ -175,7 +178,7 @@ public class ContextExplorerTests {
|
||||
public void SeedTableData_SetsTableSeededFlag() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig = contextConfig.Tables[0];
|
||||
config.Contexts.Add(contextConfig);
|
||||
|
||||
@@ -194,7 +197,7 @@ public class ContextExplorerTests {
|
||||
public void SeedTableData_SetsTablePropertiesCorrectly() {
|
||||
// Arrange
|
||||
var config = new HopFrameConfig();
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext));
|
||||
var contextConfig = new DbContextConfig(typeof(MockDbContext), null!);
|
||||
var tableConfig = contextConfig.Tables[0];
|
||||
var tableConfig2 = contextConfig.Tables[1];
|
||||
config.Contexts.Add(contextConfig);
|
||||
|
||||
@@ -17,69 +17,69 @@ public class DisplayPropertyTests {
|
||||
var contextMock = new Mock<DbContext>();
|
||||
_providerMock = new Mock<IServiceProvider>();
|
||||
_explorerMock = new Mock<IContextExplorer>();
|
||||
_config = new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0);
|
||||
_config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0);
|
||||
_tableManager =
|
||||
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]
|
||||
public void DisplayProperty_ReturnsEmptyString_WhenItemIsNull() {
|
||||
public async Task DisplayProperty_ReturnsEmptyString_WhenItemIsNull() {
|
||||
// Arrange
|
||||
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0);
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(null, prop);
|
||||
var result = await _tableManager.DisplayProperty(null, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_UsesFormatter_WhenListingProperty() {
|
||||
public async Task DisplayProperty_UsesFormatter_WhenListingProperty() {
|
||||
// Arrange
|
||||
var item = "test";
|
||||
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) {
|
||||
IsListingProperty = true,
|
||||
Formatter = (obj, provider) => ((string)obj).ToUpper()
|
||||
IsVirtualProperty = true,
|
||||
Formatter = (obj, provider) => Task.FromResult(((string)obj).ToUpper())
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop);
|
||||
var result = await _tableManager.DisplayProperty(item, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("TEST", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_UsesValueFormatter_WhenNotListingProperty() {
|
||||
public async Task DisplayProperty_UsesValueFormatter_WhenNotListingProperty() {
|
||||
// Arrange
|
||||
var item = "test";
|
||||
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) {
|
||||
Formatter = (obj, provider) => ((int)obj).ToString("D4")
|
||||
Formatter = (obj, provider) => Task.FromResult(((int)obj).ToString("D4"))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop);
|
||||
var result = await _tableManager.DisplayProperty(item, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("0004", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_ReturnsValueAsString_WhenNoFormatter() {
|
||||
public async Task DisplayProperty_ReturnsValueAsString_WhenNoFormatter() {
|
||||
// Arrange
|
||||
var item = "test";
|
||||
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0);
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop);
|
||||
var result = await _tableManager.DisplayProperty(item, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("4", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_ReturnsEnumerableCount_WhenEnumerableProperty() {
|
||||
public async Task DisplayProperty_ReturnsEnumerableCount_WhenEnumerableProperty() {
|
||||
// Arrange
|
||||
var item = new { List = new List<int> { 1, 2, 3 } };
|
||||
var prop = new PropertyConfig(item.GetType().GetProperty("List")!, _config, 0) {
|
||||
@@ -87,14 +87,14 @@ public class DisplayPropertyTests {
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop);
|
||||
var result = await _tableManager.DisplayProperty(item, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("3", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_UsesDisplayedProperty_WhenNoDirectFormatter() {
|
||||
public async Task DisplayProperty_UsesDisplayedProperty_WhenNoDirectFormatter() {
|
||||
// Arrange
|
||||
var item = new { Inner = new { Key = 42 } };
|
||||
var innerPropInfo = item.Inner.GetType().GetProperty("Key");
|
||||
@@ -106,48 +106,48 @@ public class DisplayPropertyTests {
|
||||
|
||||
_explorerMock
|
||||
.Setup(e => e.GetTable(item.Inner.GetType()))
|
||||
.Returns(new TableConfig(new DbContextConfig(typeof(MockDbContext)), typeof(MockModel), "Models", 0) {
|
||||
.Returns(new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0) {
|
||||
Properties = { innerPropConfig }
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop);
|
||||
var result = await _tableManager.DisplayProperty(item, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("42", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_ReturnsEmptyString_WhenPropValueIsNull() {
|
||||
public async Task DisplayProperty_ReturnsEmptyString_WhenPropValueIsNull() {
|
||||
// Arrange
|
||||
var item = new { Name = (string?)null };
|
||||
var prop = new PropertyConfig(item.GetType().GetProperty("Name")!, _config, 0);
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop);
|
||||
var result = await _tableManager.DisplayProperty(item, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_UsesEnumerableFormatter_WhenEnumerableAndValueProvided() {
|
||||
public async Task DisplayProperty_UsesEnumerableFormatter_WhenEnumerableAndValueProvided() {
|
||||
// Arrange
|
||||
var item = new { List = new List<int> { 1, 2, 3 } };
|
||||
var prop = new PropertyConfig(item.GetType().GetProperty("List")!, _config, 0) {
|
||||
IsEnumerable = true,
|
||||
EnumerableFormatter = (obj, provider) => string.Join(",", ((IEnumerable<int>)obj))
|
||||
EnumerableFormatter = (obj, provider) => Task.FromResult(string.Join(",", ((IEnumerable<int>)obj)))
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop, item.List);
|
||||
var result = await _tableManager.DisplayProperty(item, prop, null, item.List);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("1,2,3", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_ReturnsEmptyString_WhenDisplayedPropertyAndInnerConfigIsNull() {
|
||||
public async Task DisplayProperty_ReturnsEmptyString_WhenDisplayedPropertyAndInnerConfigIsNull() {
|
||||
// Arrange
|
||||
var item = new { Inner = new { Key = 42 } };
|
||||
var innerPropInfo = item.Inner.GetType().GetProperty("Key");
|
||||
@@ -161,14 +161,14 @@ public class DisplayPropertyTests {
|
||||
.Returns((TableConfig?)null);
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop);
|
||||
var result = await _tableManager.DisplayProperty(item, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("{ Key = 42 }", result); // Returns the value as string if inner config is null
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_ReturnsKeyValue_WhenDisplayedPropertyIsNull() {
|
||||
public async Task DisplayProperty_ReturnsKeyValue_WhenDisplayedPropertyIsNull() {
|
||||
// Arrange
|
||||
var item = new { Inner = new { Key = 42 } };
|
||||
var propInfo = item.GetType().GetProperty("Inner");
|
||||
@@ -177,21 +177,21 @@ public class DisplayPropertyTests {
|
||||
var keyProperty = item.Inner.GetType().GetProperty("Key");
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop);
|
||||
var result = await _tableManager.DisplayProperty(item, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("{ Key = 42 }", result); // Returns key value as string if DisplayedProperty is null
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayProperty_ReturnsToStringValue_WhenNoKeyOrDisplayedProperty() {
|
||||
public async Task DisplayProperty_ReturnsToStringValue_WhenNoKeyOrDisplayedProperty() {
|
||||
// Arrange
|
||||
var item = new { Inner = new { Name = "Test" } };
|
||||
var propInfo = item.GetType().GetProperty("Inner");
|
||||
var prop = new PropertyConfig(propInfo!, _config, 0);
|
||||
|
||||
// Act
|
||||
var result = _tableManager.DisplayProperty(item, prop);
|
||||
var result = await _tableManager.DisplayProperty(item, prop);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("{ Name = Test }", result); // Returns ToString value of inner property
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user