Added editor functionallity + propper enum rendering
All checks were successful
HopFrame CI / build (push) Successful in 56s
HopFrame CI / test (push) Successful in 59s

This commit is contained in:
2026-02-28 14:32:00 +01:00
parent 0a00146a35
commit e3bd34d6ec
7 changed files with 197 additions and 34 deletions

View File

@@ -23,7 +23,8 @@ builder.Services.AddHopFrame(config => {
table.SetDescription("The user dataset. It contains all information for the users of the application."); table.SetDescription("The user dataset. It contains all information for the users of the application.");
table.Property(u => u.Password) table.Property(u => u.Password)
.Listable(false); .Listable(false)
.SetType(PropertyType.Password);
table.SetPreferredProperty(u => u.Username); table.SetPreferredProperty(u => u.Username);
}); });

View File

@@ -46,8 +46,10 @@ internal static class ConfigurationHelper {
Table = table Table = table
}; };
if (property.CustomAttributes.Any(a => a.AttributeType == typeof(KeyAttribute))) if (property.CustomAttributes.Any(a => a.AttributeType == typeof(KeyAttribute))) {
table.PreferredProperty = config.Identifier; table.PreferredProperty = config.Identifier;
config.Editable = false;
}
return config; return config;
} }

View File

@@ -6,11 +6,18 @@ namespace HopFrame.Core.Services;
public interface IEntityAccessor { public interface IEntityAccessor {
/// <summary> /// <summary>
/// Returns the formatted content of the property, ready to be displayed /// Returns the formatted value of the property, ready to be displayed
/// </summary> /// </summary>
/// <param name="model">The model to pull the property from</param> /// <param name="model">The model to pull the property from</param>
/// <param name="property">The property that shall be extracted</param> /// <param name="property">The property that shall be extracted</param>
public string? GetValue(object model, PropertyConfig property); public string? GetValue(object model, PropertyConfig property);
/// <summary>
/// Returns the real value of the property, ready to be displayed
/// </summary>
/// <param name="model">The model to pull the property from</param>
/// <param name="property">The property that shall be extracted</param>
public object? GetValueRaw(object model, PropertyConfig property);
/// <summary> /// <summary>
/// Formats the property to be displayed properly /// Formats the property to be displayed properly
@@ -25,7 +32,7 @@ public interface IEntityAccessor {
/// <param name="model">The model to save the property to</param> /// <param name="model">The model to save the property to</param>
/// <param name="property">The property that shall be modified</param> /// <param name="property">The property that shall be modified</param>
/// <param name="value">The new value of the property</param> /// <param name="value">The new value of the property</param>
public void SetValue(object model, PropertyConfig property, object value); public void SetValue(object model, PropertyConfig property, object? value);
/// <summary> /// <summary>
/// Sorts the provided dataset by the specified property /// Sorts the provided dataset by the specified property

View File

@@ -7,12 +7,19 @@ namespace HopFrame.Core.Services.Implementation;
internal class EntityAccessor(IConfigAccessor accessor) : IEntityAccessor { internal class EntityAccessor(IConfigAccessor accessor) : IEntityAccessor {
public string? GetValue(object model, PropertyConfig property) { public string? GetValue(object model, PropertyConfig property) {
var value = GetValueRaw(model, property);
if (value is null)
return null;
return FormatValue(value, property);
}
public object? GetValueRaw(object model, PropertyConfig property) {
var prop = model.GetType().GetProperty(property.Identifier); var prop = model.GetType().GetProperty(property.Identifier);
if (prop is null) if (prop is null)
return null; return null;
var value = prop.GetValue(model); return prop.GetValue(model);
return FormatValue(value, property);
} }
public string? FormatValue(object? value, PropertyConfig property) { public string? FormatValue(object? value, PropertyConfig property) {
@@ -34,12 +41,12 @@ internal class EntityAccessor(IConfigAccessor accessor) : IEntityAccessor {
return value.ToString(); return value.ToString();
} }
public void SetValue(object model, PropertyConfig property, object value) { public void SetValue(object model, PropertyConfig property, object? value) {
var prop = model.GetType().GetProperty(property.Identifier); var prop = model.GetType().GetProperty(property.Identifier);
if (prop is null) if (prop is null)
return; return;
if (value.GetType() != property.Type) if (value?.GetType() != property.Type)
value = Convert.ChangeType(value, property.Type); value = Convert.ChangeType(value, property.Type);
prop.SetValue(model, value); prop.SetValue(model, value);

View File

@@ -18,13 +18,19 @@
} }
</MudText> </MudText>
<MudStack Spacing="5" Style="overflow-y: auto"> @if (Entry is not null) {
<MudFocusTrap> <MudStack Spacing="5" Style="overflow-y: auto">
@foreach (var property in GetProperties()) { <MudFocusTrap>
<PropertyInput Config="property" Variant="Variant.Filled" /> @foreach (var property in GetProperties()) {
} <PropertyInput
</MudFocusTrap> Config="property"
</MudStack> Variant="Variant.Filled"
Value="@(GetPropertyValue(property))"
ValueChanged="@(v => OnPropertyUpdated(property, v))"/>
}
</MudFocusTrap>
</MudStack>
}
<MudStack Row="true" Style="margin-top: auto"> <MudStack Row="true" Style="margin-top: auto">
<MudButton Color="Color.Primary" OnClick="@(Submit)">Save</MudButton> <MudButton Color="Color.Primary" OnClick="@(Submit)">Save</MudButton>

View File

@@ -1,10 +1,11 @@
using HopFrame.Core.Configuration; using HopFrame.Core.Configuration;
using HopFrame.Core.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
namespace HopFrame.Web.Components.Components; namespace HopFrame.Web.Components.Components;
public partial class Editor(IDialogService dialogs) : ComponentBase { public partial class Editor(IDialogService dialogs, IEntityAccessor accessor) : ComponentBase {
private enum EditorMode { private enum EditorMode {
Editor, Editor,
@@ -15,17 +16,20 @@ public partial class Editor(IDialogService dialogs) : ComponentBase {
public required TableConfig Config { get; set; } public required TableConfig Config { get; set; }
private bool IsVisible { get; set; } private bool IsVisible { get; set; }
private object? Entry { get; set; } private object? Entry { get; set; }
private EditorMode Mode { get; set; } private EditorMode Mode { get; set; }
private TaskCompletionSource<object?> Completion { get; set; } = null!; private TaskCompletionSource<object?> Completion { get; set; } = null!;
private Dictionary<string, object?> UpdatedValues { get; set; } = null!;
public Task<object?> Present(object? entry) { public Task<object?> Present(object? entry) {
Completion = new (); Completion = new ();
Mode = entry is null ? EditorMode.Creator : EditorMode.Editor; Mode = entry is null ? EditorMode.Creator : EditorMode.Editor;
Entry = entry ?? Activator.CreateInstance(Config.TableType); Entry = entry ?? Activator.CreateInstance(Config.TableType)!;
UpdatedValues = new();
StateHasChanged(); StateHasChanged();
IsVisible = true; IsVisible = true;
return Completion.Task; return Completion.Task;
@@ -56,8 +60,19 @@ public partial class Editor(IDialogService dialogs) : ComponentBase {
return query.OrderBy(p => p.OrderIndex); return query.OrderBy(p => p.OrderIndex);
} }
private object? GetPropertyValue(PropertyConfig property) {
return accessor.GetValueRaw(Entry!, property);
}
private void OnPropertyUpdated(PropertyConfig property, object? value) {
UpdatedValues[property.Identifier] = value;
}
private void ApplyChanges() { private void ApplyChanges() {
foreach (var propUpdate in UpdatedValues) {
var property = Config.Properties.First(p => p.Identifier == propUpdate.Key);
accessor.SetValue(Entry!, property, propUpdate.Value);
}
} }
} }

View File

@@ -1,11 +1,14 @@
@using HopFrame.Core.Configuration @using System.Collections
@using HopFrame.Core.Configuration
@switch ((PropertyType)((byte)Config.PropertyType & 0x0F)) { @switch ((PropertyType)((byte)Config.PropertyType & 0x0F)) {
case PropertyType.Numeric: case PropertyType.Numeric:
<MudNumericField <MudNumericField
T="double" T="double"
Value="@(Convert.ToDouble(Value))"
ValueChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName" Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)" Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)" Disabled="@(!Config.Editable)"
Variant="Variant"/> Variant="Variant"/>
break; break;
@@ -13,8 +16,9 @@
case PropertyType.Boolean: case PropertyType.Boolean:
<MudSwitch <MudSwitch
T="bool" T="bool"
Value="@((bool)(Value ?? false))"
ValueChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName" Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Disabled="@(!Config.Editable)"/> Disabled="@(!Config.Editable)"/>
break; break;
@@ -22,14 +26,18 @@
<MudField Label="@Config.DisplayName" Variant="Variant.Outlined" Style="display: flex"> <MudField Label="@Config.DisplayName" Variant="Variant.Outlined" Style="display: flex">
<MudStack Row="true"> <MudStack Row="true">
<MudDatePicker <MudDatePicker
Date="_date"
DateChanged="@(v => OnValueChanged(v))"
Label="Date" Label="Date"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)" Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)" Disabled="@(!Config.Editable)"
Variant="Variant"/> Variant="Variant"/>
<MudTimePicker <MudTimePicker
Time="_time"
TimeChanged="@(v => OnValueChanged(v))"
Label="Time" Label="Time"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)" Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)" Disabled="@(!Config.Editable)"
Variant="Variant"/> Variant="Variant"/>
</MudStack> </MudStack>
@@ -38,16 +46,20 @@
case PropertyType.DateOnly: case PropertyType.DateOnly:
<MudDatePicker <MudDatePicker
Date="_date"
DateChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName" Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)" Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)" Disabled="@(!Config.Editable)"
Variant="Variant"/> Variant="Variant"/>
break; break;
case PropertyType.TimeOnly: case PropertyType.TimeOnly:
<MudTimePicker <MudTimePicker
Time="_time"
TimeChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName" Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)" Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)" Disabled="@(!Config.Editable)"
Variant="Variant"/> Variant="Variant"/>
break; break;
@@ -55,9 +67,11 @@
case PropertyType.Email: case PropertyType.Email:
<MudTextField <MudTextField
T="string" T="string"
Value="Value?.ToString()"
ValueChanged="@(v => OnValueChanged(v))"
InputType="InputType.Email" InputType="InputType.Email"
Label="@Config.DisplayName" Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)" Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)" Disabled="@(!Config.Editable)"
Variant="Variant"/> Variant="Variant"/>
break; break;
@@ -65,28 +79,61 @@
case PropertyType.Password: case PropertyType.Password:
<MudTextField <MudTextField
T="string" T="string"
InputType="InputType.Password" Value="Value?.ToString()"
ValueChanged="@(v => OnValueChanged(v))"
InputType="@_passwordInputType"
Label="@Config.DisplayName" Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)" Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)" Disabled="@(!Config.Editable)"
Variant="Variant"/> Variant="Variant"
Adornment="Adornment.End"
AdornmentIcon="@_passwordIcon"
OnAdornmentClick="@(OnPasswordIconClick)"/>
break; break;
case PropertyType.PhoneNumber: case PropertyType.PhoneNumber:
<MudTextField <MudTextField
T="string" T="string"
Value="Value?.ToString()"
ValueChanged="@(v => OnValueChanged(v))"
InputType="InputType.Telephone" InputType="InputType.Telephone"
Label="@Config.DisplayName" Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)" Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)" Disabled="@(!Config.Editable)"
Variant="Variant"/> Variant="Variant"/>
break; break;
case PropertyType.Enum:
<MudSelect
T="object"
Value="Value?.ToString()"
ValueChanged="@(v => OnValueChanged(v))"
MultiSelection="@((Config.PropertyType & PropertyType.List) != 0)"
SelectedValuesChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName"
Clearable="@((Config.PropertyType & (PropertyType.Nullable | PropertyType.List)) != 0)"
Disabled="@(!Config.Editable)"
Variant="Variant">
@if ((Config.PropertyType & PropertyType.List) != 0) {
@foreach (var value in Enum.GetValues(Config.Type.GenericTypeArguments[0])) {
<MudSelectItem Value="value">@Enum.GetName(Config.Type.GenericTypeArguments[0], value)</MudSelectItem>
}
}
else {
@foreach (var value in Enum.GetValues(Config.Type)) {
<MudSelectItem Value="value">@Enum.GetName(Config.Type, value)</MudSelectItem>
}
}
</MudSelect>
break;
default: default:
<MudTextField <MudTextField
T="string" T="string"
Value="Value?.ToString()"
ValueChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName" Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)" Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)" Disabled="@(!Config.Editable)"
Variant="Variant"/> Variant="Variant"/>
break; break;
@@ -95,12 +142,90 @@
@code { @code {
[Parameter] [Parameter]
public object? Value { get; set; } public object? Value {
get;
set {
field = value;
if (value is null)
return;
switch ((PropertyType)((byte)Config.PropertyType & 0x0F)) {
case PropertyType.DateTime:
_date = (DateTime)value;
_time = TimeOnly.FromDateTime((DateTime)value).ToTimeSpan();
break;
case PropertyType.DateOnly:
_date = ((DateOnly)value).ToDateTime(TimeOnly.MinValue);
break;
case PropertyType.TimeOnly:
_time = ((TimeOnly)value).ToTimeSpan();
break;
}
}
}
[Parameter] [Parameter]
public required PropertyConfig Config { get; set; } public required PropertyConfig Config { get; set; }
[Parameter] [Parameter]
public Variant Variant { get; set; } public Variant Variant { get; set; }
[Parameter]
public EventCallback<object?> ValueChanged { get; set; }
private DateTime _date;
private TimeSpan _time;
private InputType _passwordInputType = InputType.Password;
private string _passwordIcon = Icons.Material.Filled.VisibilityOff;
private async Task OnValueChanged(object? value) {
if (value is DateTime dt) {
_date = dt;
switch ((PropertyType)((byte)Config.PropertyType & 0x0F)) {
case PropertyType.DateOnly:
value = DateOnly.FromDateTime(dt);
break;
case PropertyType.DateTime:
value = DateOnly.FromDateTime(dt).ToDateTime(TimeOnly.FromTimeSpan(_time));
break;
}
}
if (value is TimeSpan ts) {
_time = ts;
switch ((PropertyType)((byte)Config.PropertyType & 0x0F)) {
case PropertyType.DateOnly:
value = TimeOnly.FromTimeSpan(ts);
break;
case PropertyType.DateTime:
value = DateOnly.FromDateTime(_date).ToDateTime(TimeOnly.FromTimeSpan(ts));
break;
}
}
if ((PropertyType)((byte)Config.PropertyType & 0x0F) == PropertyType.Enum && (Config.PropertyType & PropertyType.List) != 0) {
var list = value as IReadOnlyCollection<object?>;
var newValue = Activator.CreateInstance(Config.Type) as IList;
foreach (var entry in list!) {
newValue!.Add(entry);
}
value = newValue;
}
if (ValueChanged.HasDelegate)
await ValueChanged.InvokeAsync(value);
}
private void OnPasswordIconClick() {
_passwordIcon = _passwordInputType == InputType.Password ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility;
_passwordInputType = _passwordInputType == InputType.Password ? InputType.Text : InputType.Password;
}
} }