Added edit modal

This commit is contained in:
Leon Hoppe
2025-01-15 14:58:15 +01:00
parent ad4d9c65d6
commit d4018cceec
12 changed files with 179 additions and 19 deletions

View File

@@ -10,6 +10,9 @@ public class PropertyConfig(PropertyInfo info) {
public bool Sortable { get; set; } = true;
public bool Searchable { get; set; } = true;
public PropertyInfo? DisplayedProperty { get; set; }
public Func<object, string>? Formatter { get; set; }
public bool Editable { get; set; } = true;
public bool Creatable { get; set; } = true;
}
public class PropertyConfig<TProp>(PropertyConfig config) {
@@ -39,5 +42,20 @@ public class PropertyConfig<TProp>(PropertyConfig config) {
config.DisplayedProperty = TableConfig<TProp>.GetPropertyInfo(propertyExpression);
return this;
}
public PropertyConfig<TProp> Format(Func<TProp, string> formatter) {
config.Formatter = obj => formatter.Invoke((TProp)obj);
return this;
}
public PropertyConfig<TProp> Editable(bool editable) {
config.Editable = editable;
return this;
}
public PropertyConfig<TProp> Creatable(bool creatable) {
config.Creatable = creatable;
return this;
}
}

View File

@@ -1,4 +1,6 @@
using System.Linq.Expressions;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq.Expressions;
using System.Reflection;
namespace HopFrame.Core.Config;
@@ -17,7 +19,18 @@ public class TableConfig {
ContextConfig = config;
foreach (var info in tableType.GetProperties()) {
Properties.Add(new PropertyConfig(info));
var propConfig = new PropertyConfig(info);
if (info.GetCustomAttributes(true).Any(a => a is DatabaseGeneratedAttribute)) {
propConfig.Creatable = false;
propConfig.Editable = false;
}
if (info.GetCustomAttributes(true).Any(a => a is KeyAttribute)) {
propConfig.Editable = false;
}
Properties.Add(propConfig);
}
}
}

View File

@@ -9,5 +9,5 @@ public interface ITableManager {
public int TotalPages(int perPage = 20);
public Task DeleteItem(object item);
public string DisplayProperty(object item, PropertyInfo info, TableConfig? tableConfig);
public string DisplayProperty(object? item, PropertyInfo info, TableConfig? tableConfig);
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using HopFrame.Core.Config;
using Microsoft.EntityFrameworkCore;
@@ -50,14 +51,27 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
return false;
}
public string DisplayProperty(object item, PropertyInfo info, TableConfig? tableConfig) {
public string DisplayProperty(object? item, PropertyInfo info, TableConfig? tableConfig) {
if (item is null) return string.Empty;
var prop = tableConfig?.Properties.Find(prop => prop.Info.Name == info.Name);
if (prop is null) return item.ToString() ?? string.Empty;
var propValue = prop.Info.GetValue(item);
if (propValue is null || prop.DisplayedProperty is null)
return propValue?.ToString() ?? string.Empty;
if (propValue is null)
return string.Empty;
if (prop.Formatter is not null) {
return prop.Formatter.Invoke(propValue);
}
if (prop.DisplayedProperty is null) {
var key = prop.Info.PropertyType
.GetProperties()
.Where(p => p.GetCustomAttributes(true).Any(a => a is KeyAttribute))
.FirstOrDefault();
return key?.GetValue(propValue)?.ToString() ?? propValue.ToString() ?? string.Empty;
}
var innerConfig = explorer.GetTable(propValue.GetType());
return DisplayProperty(propValue, prop.DisplayedProperty, innerConfig);

View File

@@ -0,0 +1,59 @@
@implements IDialogContentComponent<EditorDialogData>
@using HopFrame.Web.Models
@using HopFrame.Core.Services
@using HopFrame.Web.Helpers
<FluentDialogBody>
@foreach (var property in Content.Config.Properties) {
if (!_currentlyEditing && !property.Creatable) continue;
<div style="margin-bottom: 20px">
@if (property.Info.PropertyType.IsNumeric()) {
<FluentNumberField
TValue="double"
Label="@property.Name"
Value="@(Convert.ToDouble(property.Info.GetValue(Content.CurrentObject)))"
Style="width: 100%;"
Disabled="@(!property.Editable)"
/>
} else if (property.Info.PropertyType == typeof(bool)) {
<FluentSwitch
Label="@property.Name"
Value="@(Convert.ToBoolean(property.Info.GetValue(Content.CurrentObject)))"
/>
}
else {
<FluentTextField
Label="@property.Name"
Value="@_manager?.DisplayProperty(Content.CurrentObject, property.Info, Content.Config)"
Style="width: 100%;"
Disabled="@(!property.Editable)"
/>
}
</div>
}
</FluentDialogBody>
@inject IContextExplorer Explorer
@code {
[Parameter]
public required EditorDialogData Content { get; set; }
[CascadingParameter]
public required FluentDialog Dialog { get; set; }
private ITableManager? _manager;
private bool _currentlyEditing;
protected override void OnInitialized() {
if (Dialog.Instance is null) return;
_currentlyEditing = Content.CurrentObject is not null;
Dialog.Instance.Parameters.Title = Content.CurrentObject is null ? "Add entry" : "Edit entry";
Dialog.Instance.Parameters.PreventScroll = true;
Dialog.Instance.Parameters.Width = "500px";
Dialog.Instance.Parameters.PrimaryAction = "Save";
_manager = Explorer.GetTableManager(Content.Config.PropertyName);
}
}

View File

@@ -4,30 +4,38 @@
@using HopFrame.Core.Config
@using HopFrame.Core.Services
@using HopFrame.Web.Models
@using Microsoft.JSInterop
@using System.Text.Json
<FluentDialogProvider />
<div style="display: flex; flex-direction: column; height: 100%">
<FluentToolbar Class="hopframe-toolbar">
<h3>@_config?.PropertyName</h3>
<FluentSpacer />
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" />
<FluentButton>Add Entry</FluentButton>
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entry</FluentButton>
</FluentToolbar>
<div style="overflow-y: auto; flex-grow: 1">
<FluentDataGrid Items="_currentlyDisplayedModels?.AsQueryable()">
@{ var dataIndex = 0; }
@foreach (var property in _config!.Properties.Where(prop => prop.List)) {
<PropertyColumn Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property.Info, _config)" Style="min-width: max-content; min-height: 43px" Sortable="@property.Sortable"/>
<PropertyColumn
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property.Info, _config)"
Style="min-width: max-content; min-height: 43px;"
Sortable="@property.Sortable"
/>
}
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 43px; min-width: max-content">
@{ var currentElement = _currentlyDisplayedModels!.ElementAt(dataIndex); }
<FluentButton aria-label="Edit entry">
@{ var currentElement = _currentlyDisplayedModels!.ElementAtOrDefault(dataIndex); }
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(currentElement); }">
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
</FluentButton>
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(currentElement); }">
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(currentElement!); }">
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
</FluentButton>
@@ -38,7 +46,7 @@
</TemplateColumn>
</FluentDataGrid>
@if (_currentlyDisplayedModels?.Any() == true) {
@if (_totalPages > 1) {
<div class="hopframe-paginator">
<FluentButton BackgroundColor="transparent" OnClick="() => ChangePage(_currentPage - 1)">
<FluentIcon Value="@(new Icons.Regular.Size20.ArrowPrevious())" Color="Color.Neutral" />
@@ -78,6 +86,7 @@
@inject IContextExplorer Explorer
@inject NavigationManager Navigator
@inject IJSRuntime Js
@inject IDialogService Dialogs
@code {
@@ -106,7 +115,9 @@
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
await Js.InvokeVoidAsync("removeBg");
try {
await Js.InvokeVoidAsync("removeBg");
}catch (Exception) {}
}
private CancellationTokenSource _searchCancel = new();
@@ -132,6 +143,10 @@
}
private async Task DeleteEntry(object element) { //TODO: display confirmation
var dialog = await Dialogs.ShowConfirmationAsync("Do you really want to delete this entry?");
var result = await dialog.Result;
if (result.Cancelled) return;
await _manager!.DeleteItem(element);
if (!string.IsNullOrEmpty(_searchTerm)) {
@@ -141,4 +156,8 @@
OnInitialized();
}
}
private async Task CreateOrEdit(object? element) {
var panel = await Dialogs.ShowPanelAsync<HopFrameEditor>(new EditorDialogData(_config!, element), new());
}
}

View File

@@ -0,0 +1,22 @@
namespace HopFrame.Web.Helpers;
internal static class TypeExtensions {
public static bool IsNumeric(this Type o) {
switch (Type.GetTypeCode(o)) {
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Single:
return true;
default:
return false;
}
}
}

View File

@@ -0,0 +1,8 @@
using HopFrame.Core.Config;
namespace HopFrame.Web.Models;
public sealed class EditorDialogData(TableConfig config, object? current = null) {
public object? CurrentObject { get; } = current;
public TableConfig Config { get; } = config;
}

View File

@@ -9,3 +9,4 @@
@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons
@using Microsoft.FluentUI.AspNetCore.Components.Extensions
@using HopFrame.Web.Components.Layout
@using HopFrame.Web.Components.Dialogs