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.Property(u => u.Password)
.Listable(false);
.Listable(false)
.SetType(PropertyType.Password);
table.SetPreferredProperty(u => u.Username);
});

View File

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

View File

@@ -6,11 +6,18 @@ namespace HopFrame.Core.Services;
public interface IEntityAccessor {
/// <summary>
/// Returns the formatted content of the property, ready to be displayed
/// Returns the formatted 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 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>
/// 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="property">The property that shall be modified</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>
/// 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 {
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);
if (prop is null)
return null;
var value = prop.GetValue(model);
return FormatValue(value, property);
return prop.GetValue(model);
}
public string? FormatValue(object? value, PropertyConfig property) {
@@ -34,12 +41,12 @@ internal class EntityAccessor(IConfigAccessor accessor) : IEntityAccessor {
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);
if (prop is null)
return;
if (value.GetType() != property.Type)
if (value?.GetType() != property.Type)
value = Convert.ChangeType(value, property.Type);
prop.SetValue(model, value);

View File

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

View File

@@ -1,10 +1,11 @@
using HopFrame.Core.Configuration;
using HopFrame.Core.Services;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace HopFrame.Web.Components.Components;
public partial class Editor(IDialogService dialogs) : ComponentBase {
public partial class Editor(IDialogService dialogs, IEntityAccessor accessor) : ComponentBase {
private enum EditorMode {
Editor,
@@ -15,17 +16,20 @@ public partial class Editor(IDialogService dialogs) : ComponentBase {
public required TableConfig Config { get; set; }
private bool IsVisible { get; set; }
private object? Entry { get; set; }
private EditorMode Mode { get; set; }
private TaskCompletionSource<object?> Completion { get; set; } = null!;
private Dictionary<string, object?> UpdatedValues { get; set; } = null!;
public Task<object?> Present(object? entry) {
Completion = new ();
Mode = entry is null ? EditorMode.Creator : EditorMode.Editor;
Entry = entry ?? Activator.CreateInstance(Config.TableType);
Entry = entry ?? Activator.CreateInstance(Config.TableType)!;
UpdatedValues = new();
StateHasChanged();
IsVisible = true;
return Completion.Task;
@@ -56,8 +60,19 @@ public partial class Editor(IDialogService dialogs) : ComponentBase {
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() {
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)) {
case PropertyType.Numeric:
<MudNumericField
T="double"
Value="@(Convert.ToDouble(Value))"
ValueChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)"
Variant="Variant"/>
break;
@@ -13,8 +16,9 @@
case PropertyType.Boolean:
<MudSwitch
T="bool"
Value="@((bool)(Value ?? false))"
ValueChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Disabled="@(!Config.Editable)"/>
break;
@@ -22,14 +26,18 @@
<MudField Label="@Config.DisplayName" Variant="Variant.Outlined" Style="display: flex">
<MudStack Row="true">
<MudDatePicker
Date="_date"
DateChanged="@(v => OnValueChanged(v))"
Label="Date"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)"
Variant="Variant"/>
<MudTimePicker
Time="_time"
TimeChanged="@(v => OnValueChanged(v))"
Label="Time"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)"
Variant="Variant"/>
</MudStack>
@@ -38,16 +46,20 @@
case PropertyType.DateOnly:
<MudDatePicker
Date="_date"
DateChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)"
Variant="Variant"/>
break;
case PropertyType.TimeOnly:
<MudTimePicker
Time="_time"
TimeChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)"
Variant="Variant"/>
break;
@@ -55,9 +67,11 @@
case PropertyType.Email:
<MudTextField
T="string"
Value="Value?.ToString()"
ValueChanged="@(v => OnValueChanged(v))"
InputType="InputType.Email"
Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)"
Variant="Variant"/>
break;
@@ -65,28 +79,61 @@
case PropertyType.Password:
<MudTextField
T="string"
InputType="InputType.Password"
Value="Value?.ToString()"
ValueChanged="@(v => OnValueChanged(v))"
InputType="@_passwordInputType"
Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)"
Variant="Variant"/>
Variant="Variant"
Adornment="Adornment.End"
AdornmentIcon="@_passwordIcon"
OnAdornmentClick="@(OnPasswordIconClick)"/>
break;
case PropertyType.PhoneNumber:
<MudTextField
T="string"
Value="Value?.ToString()"
ValueChanged="@(v => OnValueChanged(v))"
InputType="InputType.Telephone"
Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)"
Variant="Variant"/>
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:
<MudTextField
T="string"
Value="Value?.ToString()"
ValueChanged="@(v => OnValueChanged(v))"
Label="@Config.DisplayName"
Required="@((Config.PropertyType & PropertyType.Nullable) != 0)"
Required="@((Config.PropertyType & PropertyType.Nullable) == 0)"
Disabled="@(!Config.Editable)"
Variant="Variant"/>
break;
@@ -95,12 +142,90 @@
@code {
[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]
public required PropertyConfig Config { get; set; }
[Parameter]
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;
}
}