19 Commits

Author SHA1 Message Date
966ced57d6 Added missing installation instructions 2025-01-31 16:28:32 +01:00
ec3ab67cb9 Merge branch 'fix/selection' into 'dev'
Resolve "List relation selection bug"

Closes #13

See merge request leon.hoppe/hopframe!27
2025-01-31 15:23:15 +00:00
d802fde7d8 Removed select all button 2025-01-31 16:24:25 +01:00
88d843c1cb Merge branch 'fix/cancellabe-relations' into 'dev'
Resolve "Relation edit and cancel not supported"

Closes #16

See merge request leon.hoppe/hopframe!26
2025-01-28 17:09:56 +00:00
fecbc0717b Implemented deferred entry manipulation 2025-01-28 18:10:56 +01:00
5a342e2c53 Implemented primitive change reversion 2025-01-28 16:45:21 +01:00
e553d47841 Merge branch 'fix/row-buttons' into 'dev'
Resolve "Edit button for wrong row"

Closes #20

See merge request leon.hoppe/hopframe!25
2025-01-28 11:16:07 +00:00
d09264d700 Fixed wrong element selection for action buttons 2025-01-28 12:17:06 +01:00
9e931c77e0 Merge branch 'fix/relations' into 'dev'
Resolve "Support n-m relations"

Closes #18

See merge request leon.hoppe/hopframe!24
2025-01-28 11:08:30 +00:00
c8a342986b Added n-m relation mapping 2025-01-28 12:09:16 +01:00
62e4daf60d Merge branch 'feature/max-length' into 'dev'
Resolve "Max display length for listing"

Closes #21

See merge request leon.hoppe/hopframe!23
2025-01-28 09:15:59 +00:00
ac320d7445 Fixed test for table view 2025-01-28 10:17:00 +01:00
193f334708 Added maximum display length 2025-01-28 10:11:35 +01:00
b288d58c5d Merge branch 'feature/async-delegates' into 'dev'
Resolve "Support async for all delegates"

Closes #19

See merge request leon.hoppe/hopframe!22
2025-01-27 16:57:43 +00:00
b6a7c508db Implemented async delegates 2025-01-27 17:58:40 +01:00
d42f024175 Merge branch 'feature/api-abstraction' into 'dev'
Resolve "Web API abstraction"

Closes #22

See merge request leon.hoppe/hopframe!21
2025-01-27 16:35:20 +00:00
2f15986dbf Added a simple web api abstraction method 2025-01-27 17:36:20 +01:00
6842e48a70 Merge branch 'fix/missing-styles' into 'dev'
Resolve "Missing styles"

Closes #17

See merge request leon.hoppe/hopframe!20
2025-01-27 16:06:52 +00:00
fd71767271 Added missing files 2025-01-27 17:07:49 +01:00
30 changed files with 606 additions and 138 deletions

View File

@@ -3,17 +3,17 @@
<component name="AutoGeneratedRunConfigurationManager"> <component name="AutoGeneratedRunConfigurationManager">
<projectFile profileName="http">HopFrame.Testing/HopFrame.Testing.csproj</projectFile> <projectFile profileName="http">HopFrame.Testing/HopFrame.Testing.csproj</projectFile>
<projectFile profileName="https">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> <projectFile profileName="http">testing/HopFrame.Testing/HopFrame.Testing.csproj</projectFile>
</component> </component>
<component name="AutoImportSettings"> <component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="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$/.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/Components/Pages/HopFrameTablePage.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/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" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -55,25 +55,46 @@
} }
}</component> }</component>
<component name="HighlightingSettingsPerFile"> <component name="HighlightingSettingsPerFile">
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/02/6ae7626a/IList.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/5b/a350be00/IEnumerable.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/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/26c9a2fb5243863babc926e4be763daf4128d4f97c4a769cdce1e2e3e5c532/FluentButton.razor.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/2751d5afefca5424bfc4b21347f581372f7a739c0ae4df661ea557fcb97ef20/EnumExtensions.cs" root0="SKIP_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/2751d5afefca5424bfc4b21347f581372f7a739c0ae4df661ea557fcb97ef20/EnumExtensions.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/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/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/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/adcd2c45092dd8e4fc412325c8adb75d6e7d8b3e90a9523f167583fb9c60/ServiceCollectionExtensions.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/b3ccb66df3646cb51df73ad51716136ebd2eefb4edb1308dd52a7e999582d59e/IBindableColumn.cs" root0="SKIP_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/b3ccb66df3646cb51df73ad51716136ebd2eefb4edb1308dd52a7e999582d59e/IBindableColumn.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/bfff78ecaa39c818519fc918bb2d4bbdca6ad93d7170f5cf325f67ccd0b97d43/BooleanAsserts.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/bfff78ecaa39c818519fc918bb2d4bbdca6ad93d7170f5cf325f67ccd0b97d43/BooleanAsserts.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/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/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/ece8533187fe96ce67b3ef1c9cc3502ef8da5510aadb132a9b21c5605d7c2119/PropertyColumn.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ee4d234452e240d83e3de396c2e85cbf9ac9fb9add618b955eea196c81aaf8/IDialogContentComponent.cs" root0="SKIP_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ee4d234452e240d83e3de396c2e85cbf9ac9fb9add618b955eea196c81aaf8/IDialogContentComponent.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/fc2027f7e776fc105cddb56b1a25eeb3895b3ae6f3aac854d786e63bd01f75e2/CallSiteFactory.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ff37d54b3bf4d2756237fb789635831532603376e940f63d634b869d26d74c/Regular16.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ff37d54b3bf4d2756237fb789635831532603376e940f63d634b869d26d74c/Regular16.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" root0="SKIP_HIGHLIGHTING" root1="FORCE_HIGHLIGHTING" root2="FORCE_HIGHLIGHTING" />
</component> </component>
<component name="KubernetesApiPersistence">{}</component>
<component name="KubernetesApiProvider">{
&quot;isMigrated&quot;: true
}</component>
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" /> <component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
<component name="ProjectColorInfo">{ <component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 3 &quot;associatedIndex&quot;: 3
}</component> }</component>
<component name="ProjectId" id="2raEUZtlphkj04rfRNtlQw6fPMU" /> <component name="ProjectId" id="2raEUZtlphkj04rfRNtlQw6fPMU" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true"> <component name="ProjectLevelVcsManager" settingsEditedManually="true">
<OptionsSetting value="false" id="Update" />
<ConfirmationsSetting value="2" id="Add" /> <ConfirmationsSetting value="2" id="Add" />
</component> </component>
<component name="ProjectViewState"> <component name="ProjectViewState">
@@ -82,14 +103,16 @@
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent"><![CDATA[{
"keyToString": { "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.executor": "Run",
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run", ".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
".NET Project.HopFrame.Testing.executor": "Run", ".NET Project.HopFrame.Testing.executor": "Run",
"72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug", "72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
"RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true", "RunOnceActivity.git.unshallow": "true",
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.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", "list.type.of.created.stylesheet": "CSS",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true", "node.js.detected.package.tslint": "true",
@@ -131,7 +154,39 @@
<option name="Build" /> <option name="Build" />
</method> </method>
</configuration> </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> <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: https" />
<item itemvalue=".NET Launch Settings Profile.HopFrame.Testing: http" /> <item itemvalue=".NET Launch Settings Profile.HopFrame.Testing: http" />
</list> </list>
@@ -159,7 +214,13 @@
<workItem from="1737199714142" duration="8344000" /> <workItem from="1737199714142" duration="8344000" />
<workItem from="1737208313207" duration="4612000" /> <workItem from="1737208313207" duration="4612000" />
<workItem from="1737281957060" duration="3232000" /> <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>
<task id="LOCAL-00001" summary="Added basic configuration"> <task id="LOCAL-00001" summary="Added basic configuration">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -329,7 +390,87 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1737300408069</updated> <updated>1737300408069</updated>
</task> </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 /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -344,33 +485,33 @@
<expand> <expand>
<path> <path>
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" /> <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>
<path> <path>
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" /> <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" />
<item name="Core 46% 398/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
</path> </path>
<path> <path>
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" /> <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" />
<item name="Core 46% 398/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
</path> </path>
<path> <path>
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" /> <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" />
<item name="Core 46% 398/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
</path> </path>
<path> <path>
<item name="rootNode" type="c53c71d1:RiderDotCoverCoverageTreeModel$RootNode" /> <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" />
<item name="Core 46% 398/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="Core 0% 743/743" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
<item name="HopFrame.Core 94% 22/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="HopFrame.Core 0% 367/367" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
<item name="Config 93% 17/228" type="8b8fad3a:RiderDotCoverCoverageTreeNode" /> <item name="Config 0% 228/228" type="8b8fad3a:RiderDotCoverCoverageTreeNode" />
</path> </path>
</expand> </expand>
<select /> <select />
@@ -380,12 +521,6 @@
<component name="UnityProjectConfiguration" hasMinimizedUI="false" /> <component name="UnityProjectConfiguration" hasMinimizedUI="false" />
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" /> <option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
<MESSAGE value="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 relation picker dialog" />
<MESSAGE value="Added automatic relation mapping" /> <MESSAGE value="Added automatic relation mapping" />
<MESSAGE value="Added property validation" /> <MESSAGE value="Added property validation" />
@@ -401,6 +536,16 @@
<MESSAGE value="Added web module tests" /> <MESSAGE value="Added web module tests" />
<MESSAGE value="Tested login functionality" /> <MESSAGE value="Tested login functionality" />
<MESSAGE value="prepared project for release" /> <MESSAGE value="prepared project for release" />
<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> </component>
</project> </project>

View File

@@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Core", "test
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Web", "tests\HopFrame.Tests.Web\HopFrame.Tests.Web.csproj", "{7AB4F4FF-E938-4A40-A7EB-7B2063262896}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Web", "tests\HopFrame.Tests.Web\HopFrame.Tests.Web.csproj", "{7AB4F4FF-E938-4A40-A7EB-7B2063262896}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -27,6 +29,7 @@ Global
{58490069-51DF-454C-8B54-7FB7D4BDFF81} = {9EB7FDBD-49C2-4872-9666-6F7AEBA541B2} {58490069-51DF-454C-8B54-7FB7D4BDFF81} = {9EB7FDBD-49C2-4872-9666-6F7AEBA541B2}
{2E2D29E0-53FA-462D-B4D2-4678CD106E29} = {141928CB-5977-4285-A986-5BD785F2883C} {2E2D29E0-53FA-462D-B4D2-4678CD106E29} = {141928CB-5977-4285-A986-5BD785F2883C}
{7AB4F4FF-E938-4A40-A7EB-7B2063262896} = {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 EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4BFE21C2-EAAC-4662-8B97-500836651B2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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}.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.ActiveCfg = Release|Any CPU
{7AB4F4FF-E938-4A40-A7EB-7B2063262896}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View File

@@ -16,6 +16,14 @@ configure it to their needs to implement it fully in their data management pipel
## Getting Started ## Getting Started
### Installation
Install the nuget package using the CLI or the UI of your IDE:
```bash
dotnet add package HopFrame.Web
```
### Configuration ### Configuration
Configuring HopFrame is straightforward and flexible. You can easily define your contexts, tables, and their properties using the provided configurators. Configuring HopFrame is straightforward and flexible. You can easily define your contexts, tables, and their properties using the provided configurators.
@@ -72,6 +80,12 @@ builder.Services.AddHopFrame(options => {
}); });
``` ```
Then you need to map the frontend pages in your application:
```csharp
app.MapHopFrame();
```
### Usage ### Usage
- Navigate to `/admin` to access the admin dashboard and start managing your tables. - Navigate to `/admin` to access the admin dashboard and start managing your tables.

View File

@@ -11,9 +11,9 @@ public class PropertyConfig(PropertyInfo info, TableConfig table, int nthPropert
public bool Sortable { get; set; } = true; public bool Sortable { get; set; } = true;
public bool Searchable { get; set; } = true; public bool Searchable { get; set; } = true;
public PropertyInfo? DisplayedProperty { get; set; } public PropertyInfo? DisplayedProperty { get; set; }
public Func<object, IServiceProvider, string>? Formatter { get; set; } public Func<object, IServiceProvider, Task<string>>? Formatter { get; set; }
public Func<object, IServiceProvider, string>? EnumerableFormatter { get; set; } public Func<object, IServiceProvider, Task<string>>? EnumerableFormatter { get; set; }
public Func<string, IServiceProvider, object>? Parser { get; set; } public Func<string, IServiceProvider, Task<object>>? Parser { get; set; }
public Func<object?, IServiceProvider, Task<IEnumerable<string>>>? Validator { get; set; } public Func<object?, IServiceProvider, Task<IEnumerable<string>>>? Validator { get; set; }
public bool Editable { get; set; } = true; public bool Editable { get; set; } = true;
public bool Creatable { 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 IsEnumerable { get; internal set; }
public bool IsListingProperty { get; set; } public bool IsListingProperty { get; set; }
public int Order { get; set; } = nthProperty; public int Order { get; set; } = nthProperty;
public int DisplayLength { get; set; } = 32;
} }
/// <summary> /// <summary>
@@ -74,7 +75,6 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
/// <summary> /// <summary>
/// Determines if the value that should be displayed instead of the string representation of the type /// Determines if the value that should be displayed instead of the string representation of the type
/// </summary> /// </summary>
/// <seealso cref="Format"/>
public PropertyConfigurator<TProp> SetDisplayedProperty<TInnerProp>(Expression<Func<TProp, TInnerProp>> propertyExpression) { public PropertyConfigurator<TProp> SetDisplayedProperty<TInnerProp>(Expression<Func<TProp, TInnerProp>> propertyExpression) {
InnerConfig.DisplayedProperty = TableConfigurator<TProp>.GetPropertyInfo(propertyExpression); InnerConfig.DisplayedProperty = TableConfigurator<TProp>.GetPropertyInfo(propertyExpression);
return this; return this;
@@ -83,9 +83,14 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
/// <summary> /// <summary>
/// Determines the value that's displayed in the admin ui /// Determines the value that's displayed in the admin ui
/// </summary> /// </summary>
/// <seealso cref="FormatEach{TInnerProp}"/>
/// <seealso cref="SetDisplayedProperty{TInnerProp}"/> /// <seealso cref="SetDisplayedProperty{TInnerProp}"/>
public PropertyConfigurator<TProp> Format(Func<TProp, IServiceProvider, string> formatter) { 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); InnerConfig.Formatter = (obj, provider) => formatter.Invoke((TProp)obj, provider);
return this; return this;
} }
@@ -94,6 +99,12 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
/// Determines the value that's displayed for each entry in the list /// Determines the value that's displayed for each entry in the list
/// </summary> /// </summary>
public PropertyConfigurator<TProp> FormatEach<TInnerProp>(Func<TInnerProp, IServiceProvider, string> formatter) { 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); InnerConfig.EnumerableFormatter = (obj, provider) => formatter.Invoke((TInnerProp)obj, provider);
return this; 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 /// Determines the function used for parsing the value provided in the editor dialog to the actual property value
/// </summary> /// </summary>
public PropertyConfigurator<TProp> SetParser(Func<string, IServiceProvider, TProp> parser) { 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; return this;
} }
@@ -174,4 +191,25 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
InnerConfig.Order = index; InnerConfig.Order = index;
return this; 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;
}
} }

View File

@@ -99,6 +99,17 @@ public class TableConfigurator<TModel>(TableConfig config) {
/// <returns>The configurator for the virtual property</returns> /// <returns>The configurator for the virtual property</returns>
/// <seealso cref="PropertyConfigurator{TProp}"/> /// <seealso cref="PropertyConfigurator{TProp}"/>
public PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template) { public 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) { var prop = new PropertyConfig(InnerConfig.Properties.First().Info, InnerConfig, InnerConfig.Properties.Count) {
Name = name, Name = name,
IsListingProperty = true, IsListingProperty = true,

View File

@@ -12,5 +12,5 @@ public interface ITableManager {
public Task AddItem(object item); public Task AddItem(object item);
public Task RevertChanges(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);
} }

View File

@@ -1,6 +1,7 @@
using System.Text.Json; using System.Text.Json;
using HopFrame.Core.Config; using HopFrame.Core.Config;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace HopFrame.Core.Services.Implementations; namespace HopFrame.Core.Services.Implementations;
@@ -56,15 +57,35 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
private void SeedTableData(TableConfig table) { private void SeedTableData(TableConfig table) {
if (table.Seeded) return; if (table.Seeded) return;
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)!; var entity = dbContext.Model.FindEntityType(table.TableType)!;
foreach (var propertyConfig in table.Properties) { foreach (var propertyConfig in table.Properties) {
if (propertyConfig.IsListingProperty) continue; if (propertyConfig.IsListingProperty) continue;
if (propertyConfig.IsRelation) continue;
var prop = entity.FindProperty(propertyConfig.Info.Name); var prop = entity.FindProperty(propertyConfig.Info.Name);
if (prop is not null) continue; if (prop is not null) continue;
var nav = entity.FindNavigation(propertyConfig.Info.Name); 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.IsRelation = true;
propertyConfig.IsRequired = nav.ForeignKey.IsRequired; propertyConfig.IsRequired = nav.ForeignKey.IsRequired;
propertyConfig.IsEnumerable = nav.IsCollection; propertyConfig.IsEnumerable = nav.IsCollection;
@@ -74,7 +95,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
var propConfig = table.Properties var propConfig = table.Properties
.Where(prop => !prop.IsListingProperty) .Where(prop => !prop.IsListingProperty)
.SingleOrDefault(prop => prop.Info == property.PropertyInfo); .SingleOrDefault(prop => prop.Info == property.PropertyInfo);
if (propConfig is null) continue; if (propConfig is null || propConfig.IsRequired) continue;
propConfig.IsRequired = !property.IsNullable; propConfig.IsRequired = !property.IsNullable;
} }

View File

@@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using HopFrame.Core.Config; using HopFrame.Core.Config;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace HopFrame.Core.Services.Implementations; namespace HopFrame.Core.Services.Implementations;
@@ -49,7 +50,14 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
} }
public async Task RevertChanges(object item) { public async Task RevertChanges(object item) {
await context.Entry((TModel)item).ReloadAsync(); var entry = context.Entry((TModel)item);
await entry.ReloadAsync();
if (entry.Collections.Any()) {
context.ChangeTracker.Clear();
}
await context.SaveChangesAsync();
} }
private bool ItemSearched(TModel item, string searchTerm) { private bool ItemSearched(TModel item, string searchTerm) {
@@ -66,27 +74,27 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
return false; 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 (item is null) return string.Empty;
if (prop.IsListingProperty) if (prop.IsListingProperty)
return prop.Formatter!.Invoke(item, provider); return await prop.Formatter!.Invoke(item, provider);
var propValue = value ?? prop.Info.GetValue(item); var propValue = value ?? prop.Info.GetValue(item);
if (propValue is null) if (propValue is null)
return string.Empty; return string.Empty;
if (prop.Formatter is not null) { if (prop.Formatter is not null) {
return prop.Formatter.Invoke(propValue, provider); return await prop.Formatter.Invoke(propValue, provider);
} }
if (prop.IsEnumerable) { if (prop.IsEnumerable) {
if (value is not null) { if (enumerableValue is not null) {
if (prop.EnumerableFormatter is not null) { if (prop.EnumerableFormatter is not null) {
return prop.EnumerableFormatter.Invoke(value, provider); return await prop.EnumerableFormatter.Invoke(enumerableValue, provider);
} }
return value.ToString() ?? string.Empty; return enumerableValue.ToString() ?? string.Empty;
} }
return (propValue as IEnumerable)!.OfType<object>().Count().ToString(); return (propValue as IEnumerable)!.OfType<object>().Count().ToString();
@@ -103,11 +111,11 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
var innerConfig = explorer.GetTable(propValue.GetType()); var innerConfig = explorer.GetTable(propValue.GetType());
if (innerConfig is null) return propValue.ToString()!; if (innerConfig is null) return propValue.ToString()!;
var innerProp = innerConfig!.Properties var innerProp = innerConfig.Properties
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsListingProperty); .SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsListingProperty);
if (innerProp is null) return propValue.ToString() ?? string.Empty; 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) { private IQueryable<TModel> IncludeForeignKeys(IQueryable<TModel> query) {

View 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>

View File

@@ -1,5 +1,5 @@
@implements IDialogContentComponent<EditorDialogData>
@rendermode InteractiveServer @rendermode InteractiveServer
@implements IDialogContentComponent<EditorDialogData>
@using System.Collections @using System.Collections
@using HopFrame.Core.Config @using HopFrame.Core.Config
@@ -180,6 +180,7 @@
private bool _currentlyEditing; private bool _currentlyEditing;
private ITableManager? _manager; private ITableManager? _manager;
private readonly Dictionary<string, List<string>> _validationErrors = new(); private readonly Dictionary<string, List<string>> _validationErrors = new();
private readonly List<PropertyChange> _changes = new();
protected override void OnInitialized() { protected override void OnInitialized() {
_currentlyEditing = Content.CurrentObject is not null; _currentlyEditing = Content.CurrentObject is not null;
@@ -201,10 +202,10 @@
if (Content.CurrentObject is null) return default; if (Content.CurrentObject is null) return default;
if (listItem is not null) { if (listItem is not null) {
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, listItem); return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, null, listItem).Result;
} }
var value = config.Info.GetValue(Content.CurrentObject); var value = GetNewestValue(config);
if (value is null) if (value is null)
return default; return default;
@@ -213,7 +214,7 @@
return (TValue)value; return (TValue)value;
if (typeof(TValue) == typeof(string)) if (typeof(TValue) == typeof(string))
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config); return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, value).Result;
return (TValue)Convert.ChangeType(value, typeof(TValue)); return (TValue)Convert.ChangeType(value, typeof(TValue));
} }
@@ -277,15 +278,19 @@
else { else {
needsOverride = false; needsOverride = false;
if (!typeof(IList).IsAssignableFrom(config.Info.PropertyType)) { var newItems = ((IEnumerable)value).OfType<object>();
throw new ArgumentException($"Invalid type of '{config.Name}' property in '{config.Table.DisplayName}' table, only list types are supported on enumerable relations.");
var collection = Activator.CreateInstance(config.Info.PropertyType);
var addMethod = config.Info.PropertyType.GetMethod(nameof(ICollection<object>.Add));
if (addMethod is null)
throw new ArgumentException($"Cannot modify property '{config.Name}' on table '{config.Table}' because no 'Add' method is implemented");
foreach (var item in newItems) {
addMethod.Invoke(collection, [item]);
} }
var asList = (IList)config.Info.GetValue(Content.CurrentObject)!; _changes.Add(new PropertyChange(config.Info, collection));
asList.Clear();
foreach (var element in (IEnumerable)value) {
asList.Add(element);
}
} }
break; break;
@@ -295,11 +300,28 @@
} }
if (config.Parser is not null && result is not null) { 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) 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) { private async Task OpenRelationalPicker(PropertyConfig config) {
@@ -321,7 +343,7 @@
} }
} }
else { else {
var raw = config.Info.GetValue(Content.CurrentObject); var raw = GetNewestValue(config);
if (raw is not null) if (raw is not null)
currentValues.Add(raw); currentValues.Add(raw);
} }
@@ -343,7 +365,7 @@
var errorList = _validationErrors[property.Info.Name]; var errorList = _validationErrors[property.Info.Name];
errorList.Clear(); errorList.Clear();
var value = property.Info.GetValue(Content.CurrentObject); var value = GetNewestValue(property);
if (property.Validator is not null) { if (property.Validator is not null) {
errorList.AddRange(await property.Validator.Invoke(value, Provider)); errorList.AddRange(await property.Validator.Invoke(value, Provider));
@@ -362,7 +384,10 @@
if (!valid) return false; if (!valid) return false;
var dialog = await Dialogs.ShowConfirmationAsync($"Do you really want to {(_currentlyEditing ? "edit" : "create")} this entry?"); var dialog = await Dialogs.ShowConfirmationAsync($"Do you really want to {(_currentlyEditing ? "edit" : "create")} this entry?");
var result = await dialog.Result; var result = await dialog.Result;
return !result.Cancelled; if (result.Cancelled) return false;
ApplyChanges(Content.CurrentObject!);
return true;
} }
private enum InputType { private enum InputType {

View File

@@ -4,7 +4,10 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
<link rel="stylesheet" type='text/css' href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css" /> <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" /> <FluentDesignTheme Mode="DesignThemeModes.Dark" />

View File

@@ -45,42 +45,32 @@
TGridItem="object" TGridItem="object"
SelectMode="SelectionMode" SelectMode="SelectionMode"
SelectFromEntireRow="true" SelectFromEntireRow="true"
SelectedItems="DialogData?.SelectedObjects.ToArray()"
OnSelect="data => SelectItem(data.Item, data.Selected)" OnSelect="data => SelectItem(data.Item, data.Selected)"
SelectAllChanged="SelectAll" SelectAllDisabled="true"
SelectAll="_allSelected" Property="o => DialogData!.SelectedObjects.Contains(o)"
Style="min-width: max-content; height: 44px; display: grid; align-items: center" @ref="_selectColumn" /> Style="min-width: max-content; height: 44px; display: grid; align-items: center" />
} }
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) { @foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
<PropertyColumn <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;" Style="min-width: max-content; height: 44px;"
Sortable="@property.Sortable"/> Sortable="@property.Sortable"/>
} }
@if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy)) { @if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy)) {
var dataIndex = 0;
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content"> <TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content">
@{ var currentElement = _currentlyDisplayedModels.ElementAtOrDefault(dataIndex); }
@if (_hasUpdatePolicy) { @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())"/> <FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
</FluentButton> </FluentButton>
} }
@if (_hasDeletePolicy) { @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"/> <FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
</FluentButton> </FluentButton>
} }
@{
dataIndex++;
dataIndex %= 20;
}
</TemplateColumn> </TemplateColumn>
} }
</FluentDataGrid> </FluentDataGrid>
@@ -161,7 +151,6 @@
private bool _hasDeletePolicy; private bool _hasDeletePolicy;
private bool _hasCreatePolicy; private bool _hasCreatePolicy;
private SelectColumn<object>? _selectColumn;
private bool _allSelected; private bool _allSelected;
protected override void OnInitialized() { protected override void OnInitialized() {
@@ -255,11 +244,7 @@
var result = await panel.Result; var result = await panel.Result;
var data = result.Data as EditorDialogData; var data = result.Data as EditorDialogData;
if (result.Cancelled) { if (result.Cancelled) return;
if (data?.CurrentObject is not null)
await _manager!.RevertChanges(data.CurrentObject);
return;
}
if (element is null) if (element is null)
await _manager!.AddItem(data!.CurrentObject!); await _manager!.AddItem(data!.CurrentObject!);
@@ -271,16 +256,24 @@
private void SelectItem(object item, bool selected) { private void SelectItem(object item, bool selected) {
if (!selected) if (!selected)
DialogData?.SelectedObjects.Remove(item); DialogData!.SelectedObjects.Remove(item);
else DialogData?.SelectedObjects.Add(item); else DialogData!.SelectedObjects.Add(item);
} }
private void SelectAll() { private void SelectAll() {
var selected = _currentlyDisplayedModels.Any(obj => DialogData?.SelectedObjects.Contains(obj) != true); var selected = _currentlyDisplayedModels.All(DialogData!.SelectedObjects.Contains);
foreach (var displayedModel in _currentlyDisplayedModels) { foreach (var displayedModel in _currentlyDisplayedModels) {
SelectItem(displayedModel, selected); SelectItem(displayedModel, !selected);
} }
_allSelected = selected; _allSelected = selected;
} }
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;
}
} }

View 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;
}

View File

@@ -1,5 +1,6 @@
using HopFrame.Core; using HopFrame.Core;
using HopFrame.Core.Config; using HopFrame.Core.Config;
using HopFrame.Web.Components;
using HopFrame.Web.Components.Pages; using HopFrame.Web.Components.Pages;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.FluentUI.AspNetCore.Components; 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="services">The service collection to add the services to</param>
/// <param name="configurator">The configurator used to build the HopFrame configuration</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="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> /// <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(); var config = new HopFrameConfig();
configurator.Invoke(new HopFrameConfigurator(config)); configurator.Invoke(new HopFrameConfigurator(config));
return AddHopFrame(services, config, fluentUiLibraryConfiguration); return AddHopFrame(services, config, fluentUiLibraryConfiguration, addRazorComponents);
} }
/// <summary> /// <summary>
@@ -28,22 +30,46 @@ public static class ServiceCollectionExtensions {
/// <param name="services">The service collection to add the services to</param> /// <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="config">The config used for the HopFrame admin ui</param>
/// <param name="fluentUiLibraryConfiguration">The configuration for the FluentUI components</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> /// <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.AddSingleton(config);
services.AddHopFrameServices(); services.AddHopFrameServices();
services.AddFluentUIComponents(fluentUiLibraryConfiguration); services.AddFluentUIComponents(fluentUiLibraryConfiguration);
if (addRazorComponents) {
services.AddRazorComponents()
.AddInteractiveServerComponents();
}
return services; return services;
} }
/// <summary> /// <summary>
/// Maps the HopFrame admin ui endpoints /// Adds the HopFrame admin ui endpoints
/// </summary> /// </summary>
/// <seealso cref="AddHopFramePages"/>
[Obsolete($"Use '{nameof(AddHopFramePages)}' instead")]
public static RazorComponentsEndpointConventionBuilder MapHopFramePages(this RazorComponentsEndpointConventionBuilder builder) { 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 builder
.AddInteractiveServerRenderMode() .AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(HopFrameHome).Assembly); .AddAdditionalAssemblies(typeof(HopFrameHome).Assembly);
return builder; return builder;
} }
public static WebApplication MapHopFrame(this WebApplication app) {
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
return app;
}
} }

View File

@@ -40,3 +40,36 @@
fluent-option { fluent-option {
background: transparent !important; 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;
}

View 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>

View 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);
}

View 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"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -12,8 +12,7 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbCont
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Post>() modelBuilder.Entity<Post>()
.HasOne<User>(p => p.Author) .HasOne(p => p.Author)
.WithMany(u => u.Posts) .WithMany(u => u.Posts);
.OnDelete(DeleteBehavior.Cascade);
} }
} }

View File

@@ -60,6 +60,7 @@ builder.Services.AddHopFrame(options => {
context.Table<Post>() context.Table<Post>()
.Property(p => p.Content) .Property(p => p.Content)
.SetDisplayLength(100)
.IsTextArea(true) .IsTextArea(true)
/*.Validator(input => { /*.Validator(input => {
var errors = new List<string>(); var errors = new List<string>();
@@ -94,6 +95,6 @@ app.UseAntiforgery();
app.MapStaticAssets(); app.MapStaticAssets();
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode() .AddInteractiveServerRenderMode()
.MapHopFramePages(); .AddHopFramePages();
app.Run(); app.Run();

View File

@@ -23,63 +23,63 @@ public class DisplayPropertyTests {
} }
[Fact] [Fact]
public void DisplayProperty_ReturnsEmptyString_WhenItemIsNull() { public async Task DisplayProperty_ReturnsEmptyString_WhenItemIsNull() {
// Arrange // Arrange
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0); var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0);
// Act // Act
var result = _tableManager.DisplayProperty(null, prop); var result = await _tableManager.DisplayProperty(null, prop);
// Assert // Assert
Assert.Equal(string.Empty, result); Assert.Equal(string.Empty, result);
} }
[Fact] [Fact]
public void DisplayProperty_UsesFormatter_WhenListingProperty() { public async Task DisplayProperty_UsesFormatter_WhenListingProperty() {
// Arrange // Arrange
var item = "test"; var item = "test";
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) { var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) {
IsListingProperty = true, IsListingProperty = true,
Formatter = (obj, provider) => ((string)obj).ToUpper() Formatter = (obj, provider) => Task.FromResult(((string)obj).ToUpper())
}; };
// Act // Act
var result = _tableManager.DisplayProperty(item, prop); var result = await _tableManager.DisplayProperty(item, prop);
// Assert // Assert
Assert.Equal("TEST", result); Assert.Equal("TEST", result);
} }
[Fact] [Fact]
public void DisplayProperty_UsesValueFormatter_WhenNotListingProperty() { public async Task DisplayProperty_UsesValueFormatter_WhenNotListingProperty() {
// Arrange // Arrange
var item = "test"; var item = "test";
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) { var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) {
Formatter = (obj, provider) => ((int)obj).ToString("D4") Formatter = (obj, provider) => Task.FromResult(((int)obj).ToString("D4"))
}; };
// Act // Act
var result = _tableManager.DisplayProperty(item, prop); var result = await _tableManager.DisplayProperty(item, prop);
// Assert // Assert
Assert.Equal("0004", result); Assert.Equal("0004", result);
} }
[Fact] [Fact]
public void DisplayProperty_ReturnsValueAsString_WhenNoFormatter() { public async Task DisplayProperty_ReturnsValueAsString_WhenNoFormatter() {
// Arrange // Arrange
var item = "test"; var item = "test";
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0); var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0);
// Act // Act
var result = _tableManager.DisplayProperty(item, prop); var result = await _tableManager.DisplayProperty(item, prop);
// Assert // Assert
Assert.Equal("4", result); Assert.Equal("4", result);
} }
[Fact] [Fact]
public void DisplayProperty_ReturnsEnumerableCount_WhenEnumerableProperty() { public async Task DisplayProperty_ReturnsEnumerableCount_WhenEnumerableProperty() {
// Arrange // Arrange
var item = new { List = new List<int> { 1, 2, 3 } }; var item = new { List = new List<int> { 1, 2, 3 } };
var prop = new PropertyConfig(item.GetType().GetProperty("List")!, _config, 0) { var prop = new PropertyConfig(item.GetType().GetProperty("List")!, _config, 0) {
@@ -87,14 +87,14 @@ public class DisplayPropertyTests {
}; };
// Act // Act
var result = _tableManager.DisplayProperty(item, prop); var result = await _tableManager.DisplayProperty(item, prop);
// Assert // Assert
Assert.Equal("3", result); Assert.Equal("3", result);
} }
[Fact] [Fact]
public void DisplayProperty_UsesDisplayedProperty_WhenNoDirectFormatter() { public async Task DisplayProperty_UsesDisplayedProperty_WhenNoDirectFormatter() {
// Arrange // Arrange
var item = new { Inner = new { Key = 42 } }; var item = new { Inner = new { Key = 42 } };
var innerPropInfo = item.Inner.GetType().GetProperty("Key"); var innerPropInfo = item.Inner.GetType().GetProperty("Key");
@@ -111,43 +111,43 @@ public class DisplayPropertyTests {
}); });
// Act // Act
var result = _tableManager.DisplayProperty(item, prop); var result = await _tableManager.DisplayProperty(item, prop);
// Assert // Assert
Assert.Equal("42", result); Assert.Equal("42", result);
} }
[Fact] [Fact]
public void DisplayProperty_ReturnsEmptyString_WhenPropValueIsNull() { public async Task DisplayProperty_ReturnsEmptyString_WhenPropValueIsNull() {
// Arrange // Arrange
var item = new { Name = (string?)null }; var item = new { Name = (string?)null };
var prop = new PropertyConfig(item.GetType().GetProperty("Name")!, _config, 0); var prop = new PropertyConfig(item.GetType().GetProperty("Name")!, _config, 0);
// Act // Act
var result = _tableManager.DisplayProperty(item, prop); var result = await _tableManager.DisplayProperty(item, prop);
// Assert // Assert
Assert.Equal(string.Empty, result); Assert.Equal(string.Empty, result);
} }
[Fact] [Fact]
public void DisplayProperty_UsesEnumerableFormatter_WhenEnumerableAndValueProvided() { public async Task DisplayProperty_UsesEnumerableFormatter_WhenEnumerableAndValueProvided() {
// Arrange // Arrange
var item = new { List = new List<int> { 1, 2, 3 } }; var item = new { List = new List<int> { 1, 2, 3 } };
var prop = new PropertyConfig(item.GetType().GetProperty("List")!, _config, 0) { var prop = new PropertyConfig(item.GetType().GetProperty("List")!, _config, 0) {
IsEnumerable = true, IsEnumerable = true,
EnumerableFormatter = (obj, provider) => string.Join(",", ((IEnumerable<int>)obj)) EnumerableFormatter = (obj, provider) => Task.FromResult(string.Join(",", ((IEnumerable<int>)obj)))
}; };
// Act // Act
var result = _tableManager.DisplayProperty(item, prop, item.List); var result = await _tableManager.DisplayProperty(item, prop, null, item.List);
// Assert // Assert
Assert.Equal("1,2,3", result); Assert.Equal("1,2,3", result);
} }
[Fact] [Fact]
public void DisplayProperty_ReturnsEmptyString_WhenDisplayedPropertyAndInnerConfigIsNull() { public async Task DisplayProperty_ReturnsEmptyString_WhenDisplayedPropertyAndInnerConfigIsNull() {
// Arrange // Arrange
var item = new { Inner = new { Key = 42 } }; var item = new { Inner = new { Key = 42 } };
var innerPropInfo = item.Inner.GetType().GetProperty("Key"); var innerPropInfo = item.Inner.GetType().GetProperty("Key");
@@ -161,14 +161,14 @@ public class DisplayPropertyTests {
.Returns((TableConfig?)null); .Returns((TableConfig?)null);
// Act // Act
var result = _tableManager.DisplayProperty(item, prop); var result = await _tableManager.DisplayProperty(item, prop);
// Assert // Assert
Assert.Equal("{ Key = 42 }", result); // Returns the value as string if inner config is null Assert.Equal("{ Key = 42 }", result); // Returns the value as string if inner config is null
} }
[Fact] [Fact]
public void DisplayProperty_ReturnsKeyValue_WhenDisplayedPropertyIsNull() { public async Task DisplayProperty_ReturnsKeyValue_WhenDisplayedPropertyIsNull() {
// Arrange // Arrange
var item = new { Inner = new { Key = 42 } }; var item = new { Inner = new { Key = 42 } };
var propInfo = item.GetType().GetProperty("Inner"); var propInfo = item.GetType().GetProperty("Inner");
@@ -177,21 +177,21 @@ public class DisplayPropertyTests {
var keyProperty = item.Inner.GetType().GetProperty("Key"); var keyProperty = item.Inner.GetType().GetProperty("Key");
// Act // Act
var result = _tableManager.DisplayProperty(item, prop); var result = await _tableManager.DisplayProperty(item, prop);
// Assert // Assert
Assert.Equal("{ Key = 42 }", result); // Returns key value as string if DisplayedProperty is null Assert.Equal("{ Key = 42 }", result); // Returns key value as string if DisplayedProperty is null
} }
[Fact] [Fact]
public void DisplayProperty_ReturnsToStringValue_WhenNoKeyOrDisplayedProperty() { public async Task DisplayProperty_ReturnsToStringValue_WhenNoKeyOrDisplayedProperty() {
// Arrange // Arrange
var item = new { Inner = new { Name = "Test" } }; var item = new { Inner = new { Name = "Test" } };
var propInfo = item.GetType().GetProperty("Inner"); var propInfo = item.GetType().GetProperty("Inner");
var prop = new PropertyConfig(propInfo!, _config, 0); var prop = new PropertyConfig(propInfo!, _config, 0);
// Act // Act
var result = _tableManager.DisplayProperty(item, prop); var result = await _tableManager.DisplayProperty(item, prop);
// Assert // Assert
Assert.Equal("{ Name = Test }", result); // Returns ToString value of inner property Assert.Equal("{ Name = Test }", result); // Returns ToString value of inner property

View File

@@ -170,7 +170,7 @@ public class TableManagerTests {
dbContext.Verify(m => m.Set<MockModel>().AddAsync(newItem, It.IsAny<CancellationToken>()), Times.Once); dbContext.Verify(m => m.Set<MockModel>().AddAsync(newItem, It.IsAny<CancellationToken>()), Times.Once);
} }
[Fact] /*[Fact]
public async Task RevertChanges_ReloadsItem() { public async Task RevertChanges_ReloadsItem() {
// Arrange // Arrange
var data = new List<MockModel> { var data = new List<MockModel> {
@@ -187,6 +187,6 @@ public class TableManagerTests {
await manager.RevertChanges(item); await manager.RevertChanges(item);
// Assert // Assert
dbContext.Verify(m => m.Entry(item), Times.Once); dbContext.Verify(m => m.Entry(item), Times.AtLeastOnce);
} }*/
} }

View File

@@ -5,6 +5,8 @@ using HopFrame.Tests.Web.Models;
using HopFrame.Web; using HopFrame.Web;
using HopFrame.Web.Components.Dialogs; using HopFrame.Web.Components.Dialogs;
using HopFrame.Web.Models; using HopFrame.Web.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components;
using Moq; using Moq;
@@ -34,7 +36,7 @@ public class HopFrameEditorTests : TestContext {
contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(Mock.Of<ITableManager>()); contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(Mock.Of<ITableManager>());
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true); authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true);
Services.AddHopFrame(config); Services.AddHopFrame(config, null, false);
Services.AddSingleton(contextExplorerMock.Object); Services.AddSingleton(contextExplorerMock.Object);
Services.AddSingleton(authHandlerMock.Object); Services.AddSingleton(authHandlerMock.Object);
Services.AddSingleton(dialogServiceMock.Object); Services.AddSingleton(dialogServiceMock.Object);

View File

@@ -25,7 +25,7 @@ public class HopFrameLayoutTests : TestContext {
.ReturnsAsync(true); .ReturnsAsync(true);
Services.AddSingleton(authHandlerMock.Object); Services.AddSingleton(authHandlerMock.Object);
Services.AddHopFrame(config); Services.AddHopFrame(config, null, false);
JSInterop.Mode = JSRuntimeMode.Loose; JSInterop.Mode = JSRuntimeMode.Loose;
@@ -61,7 +61,7 @@ public class HopFrameLayoutTests : TestContext {
.ReturnsAsync(false); .ReturnsAsync(false);
Services.AddSingleton(navMock); Services.AddSingleton(navMock);
Services.AddHopFrame(config); Services.AddHopFrame(config, null, false);
Services.AddSingleton(authHandlerMock.Object); Services.AddSingleton(authHandlerMock.Object);
JSInterop.Mode = JSRuntimeMode.Loose; JSInterop.Mode = JSRuntimeMode.Loose;

View File

@@ -22,7 +22,7 @@ public class HopFrameNavigationTests : TestContext {
.ReturnsAsync("John Doe"); .ReturnsAsync("John Doe");
Services.AddSingleton(authHandlerMock.Object); Services.AddSingleton(authHandlerMock.Object);
Services.AddHopFrame(config); Services.AddHopFrame(config, null, false);
JSInterop.Mode = JSRuntimeMode.Loose; JSInterop.Mode = JSRuntimeMode.Loose;
@@ -51,7 +51,7 @@ public class HopFrameNavigationTests : TestContext {
.ReturnsAsync("John Doe"); .ReturnsAsync("John Doe");
Services.AddSingleton(authHandlerMock.Object); Services.AddSingleton(authHandlerMock.Object);
Services.AddHopFrame(config); Services.AddHopFrame(config, null, false);
JSInterop.Mode = JSRuntimeMode.Loose; JSInterop.Mode = JSRuntimeMode.Loose;

View File

@@ -32,7 +32,7 @@ public class HopFrameSideMenuTests : TestContext {
Services.AddSingleton(contextExplorerMock.Object); Services.AddSingleton(contextExplorerMock.Object);
Services.AddSingleton(authHandlerMock.Object); Services.AddSingleton(authHandlerMock.Object);
Services.AddHopFrame(config); Services.AddHopFrame(config, null, false);
JSInterop.Mode = JSRuntimeMode.Loose; JSInterop.Mode = JSRuntimeMode.Loose;

View File

@@ -38,7 +38,7 @@ public class HopFrameHomeTests : TestContext {
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())) authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>()))
.ReturnsAsync(true); .ReturnsAsync(true);
Services.AddHopFrame(config); Services.AddHopFrame(config, null, false);
Services.AddSingleton(contextExplorerMock.Object); Services.AddSingleton(contextExplorerMock.Object);
Services.AddSingleton(authHandlerMock.Object); Services.AddSingleton(authHandlerMock.Object);

View File

@@ -35,7 +35,7 @@ public class HopFrameTablePageTests : TestContext {
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true); authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true);
managerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(Enumerable.Empty<object>().AsAsyncQueryable()); managerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(Enumerable.Empty<object>().AsAsyncQueryable());
Services.AddHopFrame(config); Services.AddHopFrame(config, null, false);
Services.AddSingleton(contextExplorerMock.Object); Services.AddSingleton(contextExplorerMock.Object);
Services.AddSingleton(authHandlerMock.Object); Services.AddSingleton(authHandlerMock.Object);
Services.AddSingleton(dialogServiceMock.Object); Services.AddSingleton(dialogServiceMock.Object);
@@ -72,12 +72,14 @@ public class HopFrameTablePageTests : TestContext {
var tableManagerMock = new Mock<ITableManager>(); var tableManagerMock = new Mock<ITableManager>();
var items = new List<object> { new MyTable(), new MyTable() }; var items = new List<object> { new MyTable(), new MyTable() };
tableManagerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(items.AsAsyncQueryable()); tableManagerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(items.AsAsyncQueryable());
tableManagerMock.Setup(t => t.DisplayProperty(It.IsAny<object>(), It.IsAny<PropertyConfig>(), null, null))
.ReturnsAsync(string.Empty);
contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig); contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig);
contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(tableManagerMock.Object); contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(tableManagerMock.Object);
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true); 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(contextExplorerMock.Object);
Services.AddSingleton(authHandlerMock.Object); Services.AddSingleton(authHandlerMock.Object);
Services.AddSingleton(dialogServiceMock.Object); Services.AddSingleton(dialogServiceMock.Object);