13 Commits

Author SHA1 Message Date
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
27 changed files with 481 additions and 102 deletions

View File

@@ -3,17 +3,16 @@
<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">
<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" />
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="Added n-m relation mapping">
<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" />
@@ -33,7 +32,7 @@
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="dev" />
<entry key="$PROJECT_DIR$" value="feature/max-length" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -62,12 +61,23 @@
<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/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/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.Core/Config/PropertyConfig.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Core/Config/PropertyConfig.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Core/Config/PropertyConfig.cs" root0="SKIP_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" />
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs" root0="SKIP_HIGHLIGHTING" />
</component>
<component name="KubernetesApiPersistence">{}</component>
<component name="KubernetesApiProvider">{
&quot;isMigrated&quot;: true
}</component>
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 3
@@ -82,14 +92,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": "!24 on fix/relations",
"list.type.of.created.stylesheet": "CSS",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
@@ -131,7 +143,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 +203,11 @@
<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="7142000" />
</task>
<task id="LOCAL-00001" summary="Added basic configuration">
<option name="closed" value="true" />
@@ -329,7 +377,63 @@
<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>
<option name="localTasksCounter" value="29" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -344,33 +448,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,9 +484,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" />
@@ -401,6 +502,13 @@
<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" />
<option name="LAST_COMMIT_MESSAGE" value="Added n-m relation mapping" />
</component>
</project>

View File

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

View File

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

View File

@@ -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,

View File

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

View File

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

View File

@@ -66,24 +66,24 @@ 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) {
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 (prop.EnumerableFormatter is not null) {
return prop.EnumerableFormatter.Invoke(value, provider);
return await prop.EnumerableFormatter.Invoke(value, provider);
}
return value.ToString() ?? string.Empty;
@@ -103,11 +103,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) {

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

@@ -201,7 +201,7 @@
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, listItem).Result;
}
var value = config.Info.GetValue(Content.CurrentObject);
@@ -213,7 +213,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).Result;
return (TValue)Convert.ChangeType(value, typeof(TValue));
}
@@ -295,7 +295,7 @@
}
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)

View File

@@ -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" />

View File

@@ -54,33 +54,24 @@
@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>
@@ -283,4 +274,13 @@
_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.Substring(0, config.DisplayLength) + "...";
return display;
}
}

View File

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

View File

@@ -40,3 +40,32 @@
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;
}

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);
modelBuilder.Entity<Post>()
.HasOne<User>(p => p.Author)
.WithMany(u => u.Posts)
.OnDelete(DeleteBehavior.Cascade);
.HasOne(p => p.Author)
.WithMany(u => u.Posts);
}
}

View File

@@ -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();

View File

@@ -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, 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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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))
.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);