Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 966ced57d6 | |||
| ec3ab67cb9 | |||
| d802fde7d8 | |||
| 88d843c1cb | |||
| fecbc0717b | |||
| 5a342e2c53 | |||
| e553d47841 | |||
| d09264d700 | |||
| 9e931c77e0 | |||
| c8a342986b | |||
| 62e4daf60d | |||
| ac320d7445 | |||
| 193f334708 | |||
| b288d58c5d | |||
| b6a7c508db | |||
| d42f024175 | |||
| 2f15986dbf | |||
| 6842e48a70 | |||
| fd71767271 |
203
.idea/.idea.HopFrame/.idea/workspace.xml
generated
203
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -3,17 +3,17 @@
|
||||
<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$/.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/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" />
|
||||
@@ -55,25 +55,46 @@
|
||||
}
|
||||
}</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/8b/db8582a3/IList`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/1b81cb3be224213a6a73519b6e340a628d9a1fb8629c351a186a26f6376669/List.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/439c4ee753b23e743cc14119593bc889751f9eb0b38997577d8e4c47c4fed/ToCollection.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/642391624bd5c30b3411a11434588aba4906207335166b784bf3a4325f6c7/NavigationEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/7ad7d2d0ae865063993eb8a03427815ea3bdb6a774e0a2f95512e9f669a4f489/MemberEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/87c584767b46b5fd42769be76547105558e6690f785614efddca134b2d682/Type.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/8d5d6cbff46ddc7b152381f92ae1ae51d3e7b57b14dd23840a11f5aaaaed396/InternalEntityEntry.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/d04a416cac8afac0341a8be0e859b230f2eae64924298eef48c317ba35916/RenderTreeBuilder.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/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/ff37d54b3bf4d2756237fb789635831532603376e940f63d634b869d26d74c/Regular16.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" root0="SKIP_HIGHLIGHTING" root1="FORCE_HIGHLIGHTING" root2="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">
|
||||
<OptionsSetting value="false" id="Update" />
|
||||
<ConfirmationsSetting value="2" id="Add" />
|
||||
</component>
|
||||
<component name="ProjectViewState">
|
||||
@@ -82,14 +103,16 @@
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"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",
|
||||
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
||||
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
||||
"git-widget-placeholder": "release/v3.0.0",
|
||||
"git-widget-placeholder": "!27 on fix/selection",
|
||||
"list.type.of.created.stylesheet": "CSS",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
@@ -131,7 +154,39 @@
|
||||
<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_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="" />
|
||||
<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_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="" />
|
||||
<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,7 +214,13 @@
|
||||
<workItem from="1737199714142" duration="8344000" />
|
||||
<workItem from="1737208313207" duration="4612000" />
|
||||
<workItem from="1737281957060" duration="3232000" />
|
||||
<workItem from="1737293153907" duration="7750000" />
|
||||
<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="1624000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Added basic configuration">
|
||||
<option name="closed" value="true" />
|
||||
@@ -329,7 +390,87 @@
|
||||
<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>
|
||||
<option name="localTasksCounter" value="32" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -344,33 +485,33 @@
|
||||
<expand>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 32% 1123/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Total 0% 1657/1657" 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="Total 0% 1657/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 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="Total 0% 1657/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 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="Total 0% 1657/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" />
|
||||
<item name="Total 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 0% 1657/1657" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
<item name="Config 0% 228/228" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
|
||||
</path>
|
||||
</expand>
|
||||
<select />
|
||||
@@ -380,12 +521,6 @@
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<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" />
|
||||
@@ -401,6 +536,16 @@
|
||||
<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="Included readme file in projects" />
|
||||
<MESSAGE value="Added missing files" />
|
||||
<MESSAGE value="Added a simple web api abstraction method" />
|
||||
<MESSAGE value="Implemented async delegates" />
|
||||
<MESSAGE value="Added maximum display length" />
|
||||
<MESSAGE value="Fixed test for table view" />
|
||||
<MESSAGE value="Added n-m relation mapping" />
|
||||
<MESSAGE value="Fixed wrong element selection for action buttons" />
|
||||
<MESSAGE value="Implemented primitive change reversion" />
|
||||
<MESSAGE value="Implemented deferred entry manipulation" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Implemented deferred entry manipulation" />
|
||||
</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
|
||||
|
||||
14
README.md
14
README.md
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
@@ -25,6 +25,7 @@ public class PropertyConfig(PropertyInfo info, TableConfig table, int nthPropert
|
||||
public bool IsEnumerable { get; internal set; }
|
||||
public bool IsListingProperty { get; set; }
|
||||
public int Order { get; set; } = nthProperty;
|
||||
public int DisplayLength { get; set; } = 32;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,7 +75,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 +83,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 +99,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 +113,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 +191,25 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,17 @@ public class TableConfigurator<TModel>(TableConfig config) {
|
||||
/// <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) {
|
||||
Name = name,
|
||||
IsListingProperty = true,
|
||||
Formatter = (obj, provider) => Task.FromResult(template.Invoke((TModel)obj, provider))
|
||||
};
|
||||
InnerConfig.Properties.Add(prop);
|
||||
return new PropertyConfigurator<string>(prop);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AddVirtualProperty(string,System.Func{TModel,System.IServiceProvider,string})"/>
|
||||
public PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, Task<string>> template) {
|
||||
var prop = new PropertyConfig(InnerConfig.Properties.First().Info, InnerConfig, InnerConfig.Properties.Count) {
|
||||
Name = name,
|
||||
IsListingProperty = true,
|
||||
|
||||
@@ -12,5 +12,5 @@ public interface ITableManager {
|
||||
public Task AddItem(object item);
|
||||
public Task RevertChanges(object item);
|
||||
|
||||
public string DisplayProperty(object? item, PropertyConfig prop, object? value = null);
|
||||
public Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -56,15 +57,35 @@ 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)!;
|
||||
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.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;
|
||||
@@ -74,7 +95,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
var propConfig = table.Properties
|
||||
.Where(prop => !prop.IsListingProperty)
|
||||
.SingleOrDefault(prop => prop.Info == property.PropertyInfo);
|
||||
if (propConfig is null) continue;
|
||||
if (propConfig is null || propConfig.IsRequired) continue;
|
||||
propConfig.IsRequired = !property.IsNullable;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using HopFrame.Core.Config;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
@@ -49,7 +50,14 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
||||
}
|
||||
|
||||
public async Task RevertChanges(object item) {
|
||||
await context.Entry((TModel)item).ReloadAsync();
|
||||
var entry = context.Entry((TModel)item);
|
||||
await entry.ReloadAsync();
|
||||
|
||||
if (entry.Collections.Any()) {
|
||||
context.ChangeTracker.Clear();
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private bool ItemSearched(TModel item, string searchTerm) {
|
||||
@@ -66,27 +74,27 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
return await prop.Formatter!.Invoke(item, provider);
|
||||
|
||||
var propValue = value ?? prop.Info.GetValue(item);
|
||||
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 +111,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
|
||||
var innerProp = innerConfig.Properties
|
||||
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsListingProperty);
|
||||
|
||||
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,5 +1,5 @@
|
||||
@implements IDialogContentComponent<EditorDialogData>
|
||||
@rendermode InteractiveServer
|
||||
@implements IDialogContentComponent<EditorDialogData>
|
||||
|
||||
@using System.Collections
|
||||
@using HopFrame.Core.Config
|
||||
@@ -180,6 +180,7 @@
|
||||
private bool _currentlyEditing;
|
||||
private ITableManager? _manager;
|
||||
private readonly Dictionary<string, List<string>> _validationErrors = new();
|
||||
private readonly List<PropertyChange> _changes = new();
|
||||
|
||||
protected override void OnInitialized() {
|
||||
_currentlyEditing = Content.CurrentObject is not null;
|
||||
@@ -201,10 +202,10 @@
|
||||
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 +214,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 +278,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 +300,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.Info.SetValue(entry, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private object? GetNewestValue(PropertyConfig config) {
|
||||
var value = config.Info.GetValue(Content.CurrentObject);
|
||||
|
||||
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) {
|
||||
@@ -321,7 +343,7 @@
|
||||
}
|
||||
}
|
||||
else {
|
||||
var raw = config.Info.GetValue(Content.CurrentObject);
|
||||
var raw = GetNewestValue(config);
|
||||
if (raw is not null)
|
||||
currentValues.Add(raw);
|
||||
}
|
||||
@@ -343,7 +365,7 @@
|
||||
|
||||
var errorList = _validationErrors[property.Info.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));
|
||||
@@ -362,7 +384,10 @@
|
||||
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 {
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
@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" />
|
||||
|
||||
|
||||
@@ -45,42 +45,32 @@
|
||||
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" />
|
||||
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); }">
|
||||
<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!); }">
|
||||
<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>
|
||||
@@ -161,7 +151,6 @@
|
||||
private bool _hasDeletePolicy;
|
||||
private bool _hasCreatePolicy;
|
||||
|
||||
private SelectColumn<object>? _selectColumn;
|
||||
private bool _allSelected;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
@@ -255,11 +244,7 @@
|
||||
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)
|
||||
await _manager!.AddItem(data!.CurrentObject!);
|
||||
@@ -271,16 +256,24 @@
|
||||
|
||||
private void SelectItem(object item, bool 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);
|
||||
var selected = _currentlyDisplayedModels.All(DialogData!.SelectedObjects.Contains);
|
||||
foreach (var displayedModel in _currentlyDisplayedModels) {
|
||||
SelectItem(displayedModel, selected);
|
||||
SelectItem(displayedModel, !selected);
|
||||
}
|
||||
|
||||
_allSelected = 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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using HopFrame.Core;
|
||||
using HopFrame.Core.Config;
|
||||
using HopFrame.Web.Components;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
@@ -15,11 +16,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);
|
||||
return AddHopFrame(services, config, fluentUiLibraryConfiguration, addRazorComponents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -28,22 +30,46 @@ 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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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": "*"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ builder.Services.AddHopFrame(options => {
|
||||
|
||||
context.Table<Post>()
|
||||
.Property(p => p.Content)
|
||||
.SetDisplayLength(100)
|
||||
.IsTextArea(true)
|
||||
/*.Validator(input => {
|
||||
var errors = new List<string>();
|
||||
@@ -94,6 +95,6 @@ app.UseAntiforgery();
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.MapHopFramePages();
|
||||
.AddHopFramePages();
|
||||
|
||||
app.Run();
|
||||
@@ -23,63 +23,63 @@ public class DisplayPropertyTests {
|
||||
}
|
||||
|
||||
[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()
|
||||
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");
|
||||
@@ -111,43 +111,43 @@ public class DisplayPropertyTests {
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
@@ -170,7 +170,7 @@ public class TableManagerTests {
|
||||
dbContext.Verify(m => m.Set<MockModel>().AddAsync(newItem, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
/*[Fact]
|
||||
public async Task RevertChanges_ReloadsItem() {
|
||||
// Arrange
|
||||
var data = new List<MockModel> {
|
||||
@@ -187,6 +187,6 @@ public class TableManagerTests {
|
||||
await manager.RevertChanges(item);
|
||||
|
||||
// Assert
|
||||
dbContext.Verify(m => m.Entry(item), Times.Once);
|
||||
}
|
||||
dbContext.Verify(m => m.Entry(item), Times.AtLeastOnce);
|
||||
}*/
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using HopFrame.Tests.Web.Models;
|
||||
using HopFrame.Web;
|
||||
using HopFrame.Web.Components.Dialogs;
|
||||
using HopFrame.Web.Models;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
using Moq;
|
||||
@@ -34,7 +36,7 @@ public class HopFrameEditorTests : TestContext {
|
||||
contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(Mock.Of<ITableManager>());
|
||||
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true);
|
||||
|
||||
Services.AddHopFrame(config);
|
||||
Services.AddHopFrame(config, null, false);
|
||||
Services.AddSingleton(contextExplorerMock.Object);
|
||||
Services.AddSingleton(authHandlerMock.Object);
|
||||
Services.AddSingleton(dialogServiceMock.Object);
|
||||
|
||||
@@ -25,7 +25,7 @@ public class HopFrameLayoutTests : TestContext {
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Services.AddSingleton(authHandlerMock.Object);
|
||||
Services.AddHopFrame(config);
|
||||
Services.AddHopFrame(config, null, false);
|
||||
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
@@ -61,7 +61,7 @@ public class HopFrameLayoutTests : TestContext {
|
||||
.ReturnsAsync(false);
|
||||
|
||||
Services.AddSingleton(navMock);
|
||||
Services.AddHopFrame(config);
|
||||
Services.AddHopFrame(config, null, false);
|
||||
Services.AddSingleton(authHandlerMock.Object);
|
||||
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
@@ -22,7 +22,7 @@ public class HopFrameNavigationTests : TestContext {
|
||||
.ReturnsAsync("John Doe");
|
||||
|
||||
Services.AddSingleton(authHandlerMock.Object);
|
||||
Services.AddHopFrame(config);
|
||||
Services.AddHopFrame(config, null, false);
|
||||
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
@@ -51,7 +51,7 @@ public class HopFrameNavigationTests : TestContext {
|
||||
.ReturnsAsync("John Doe");
|
||||
|
||||
Services.AddSingleton(authHandlerMock.Object);
|
||||
Services.AddHopFrame(config);
|
||||
Services.AddHopFrame(config, null, false);
|
||||
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public class HopFrameSideMenuTests : TestContext {
|
||||
|
||||
Services.AddSingleton(contextExplorerMock.Object);
|
||||
Services.AddSingleton(authHandlerMock.Object);
|
||||
Services.AddHopFrame(config);
|
||||
Services.AddHopFrame(config, null, false);
|
||||
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ public class HopFrameHomeTests : TestContext {
|
||||
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Services.AddHopFrame(config);
|
||||
Services.AddHopFrame(config, null, false);
|
||||
Services.AddSingleton(contextExplorerMock.Object);
|
||||
Services.AddSingleton(authHandlerMock.Object);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ public class HopFrameTablePageTests : TestContext {
|
||||
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true);
|
||||
managerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(Enumerable.Empty<object>().AsAsyncQueryable());
|
||||
|
||||
Services.AddHopFrame(config);
|
||||
Services.AddHopFrame(config, null, false);
|
||||
Services.AddSingleton(contextExplorerMock.Object);
|
||||
Services.AddSingleton(authHandlerMock.Object);
|
||||
Services.AddSingleton(dialogServiceMock.Object);
|
||||
@@ -72,12 +72,14 @@ public class HopFrameTablePageTests : TestContext {
|
||||
var tableManagerMock = new Mock<ITableManager>();
|
||||
var items = new List<object> { new MyTable(), new MyTable() };
|
||||
tableManagerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(items.AsAsyncQueryable());
|
||||
tableManagerMock.Setup(t => t.DisplayProperty(It.IsAny<object>(), It.IsAny<PropertyConfig>(), null, null))
|
||||
.ReturnsAsync(string.Empty);
|
||||
|
||||
contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig);
|
||||
contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(tableManagerMock.Object);
|
||||
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true);
|
||||
|
||||
Services.AddHopFrame(new HopFrameConfig());
|
||||
Services.AddHopFrame(new HopFrameConfig(), null, false);
|
||||
Services.AddSingleton(contextExplorerMock.Object);
|
||||
Services.AddSingleton(authHandlerMock.Object);
|
||||
Services.AddSingleton(dialogServiceMock.Object);
|
||||
|
||||
Reference in New Issue
Block a user