Added edit modal
This commit is contained in:
@@ -10,6 +10,9 @@ public class PropertyConfig(PropertyInfo info) {
|
|||||||
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, string>? Formatter { get; set; }
|
||||||
|
public bool Editable { get; set; } = true;
|
||||||
|
public bool Creatable { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PropertyConfig<TProp>(PropertyConfig config) {
|
public class PropertyConfig<TProp>(PropertyConfig config) {
|
||||||
@@ -40,4 +43,19 @@ public class PropertyConfig<TProp>(PropertyConfig config) {
|
|||||||
return this;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Linq.Expressions;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace HopFrame.Core.Config;
|
namespace HopFrame.Core.Config;
|
||||||
@@ -17,7 +19,18 @@ public class TableConfig {
|
|||||||
ContextConfig = config;
|
ContextConfig = config;
|
||||||
|
|
||||||
foreach (var info in tableType.GetProperties()) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ public interface ITableManager {
|
|||||||
public int TotalPages(int perPage = 20);
|
public int TotalPages(int perPage = 20);
|
||||||
public Task DeleteItem(object item);
|
public Task DeleteItem(object item);
|
||||||
|
|
||||||
public string DisplayProperty(object item, PropertyInfo info, TableConfig? tableConfig);
|
public string DisplayProperty(object? item, PropertyInfo info, TableConfig? tableConfig);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using HopFrame.Core.Config;
|
using HopFrame.Core.Config;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -50,14 +51,27 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
|||||||
return false;
|
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;
|
if (item is null) return string.Empty;
|
||||||
var prop = tableConfig?.Properties.Find(prop => prop.Info.Name == info.Name);
|
var prop = tableConfig?.Properties.Find(prop => prop.Info.Name == info.Name);
|
||||||
if (prop is null) return item.ToString() ?? string.Empty;
|
if (prop is null) return item.ToString() ?? string.Empty;
|
||||||
|
|
||||||
var propValue = prop.Info.GetValue(item);
|
var propValue = prop.Info.GetValue(item);
|
||||||
if (propValue is null || prop.DisplayedProperty is null)
|
if (propValue is null)
|
||||||
return propValue?.ToString() ?? string.Empty;
|
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());
|
var innerConfig = explorer.GetTable(propValue.GetType());
|
||||||
return DisplayProperty(propValue, prop.DisplayedProperty, innerConfig);
|
return DisplayProperty(propValue, prop.DisplayedProperty, innerConfig);
|
||||||
|
|||||||
59
src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor
Normal file
59
src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,30 +4,38 @@
|
|||||||
|
|
||||||
@using HopFrame.Core.Config
|
@using HopFrame.Core.Config
|
||||||
@using HopFrame.Core.Services
|
@using HopFrame.Core.Services
|
||||||
|
@using HopFrame.Web.Models
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
|
@using System.Text.Json
|
||||||
|
|
||||||
|
<FluentDialogProvider />
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; height: 100%">
|
<div style="display: flex; flex-direction: column; height: 100%">
|
||||||
<FluentToolbar Class="hopframe-toolbar">
|
<FluentToolbar Class="hopframe-toolbar">
|
||||||
<h3>@_config?.PropertyName</h3>
|
<h3>@_config?.PropertyName</h3>
|
||||||
<FluentSpacer />
|
<FluentSpacer />
|
||||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" />
|
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" />
|
||||||
<FluentButton>Add Entry</FluentButton>
|
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entry</FluentButton>
|
||||||
</FluentToolbar>
|
</FluentToolbar>
|
||||||
|
|
||||||
<div style="overflow-y: auto; flex-grow: 1">
|
<div style="overflow-y: auto; flex-grow: 1">
|
||||||
<FluentDataGrid Items="_currentlyDisplayedModels?.AsQueryable()">
|
<FluentDataGrid Items="_currentlyDisplayedModels?.AsQueryable()">
|
||||||
@{ var dataIndex = 0; }
|
@{ var dataIndex = 0; }
|
||||||
@foreach (var property in _config!.Properties.Where(prop => prop.List)) {
|
@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">
|
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 43px; min-width: max-content">
|
||||||
@{ var currentElement = _currentlyDisplayedModels!.ElementAt(dataIndex); }
|
@{ var currentElement = _currentlyDisplayedModels!.ElementAtOrDefault(dataIndex); }
|
||||||
<FluentButton aria-label="Edit entry">
|
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(currentElement); }">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
||||||
</FluentButton>
|
</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"/>
|
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
|
|
||||||
@@ -38,7 +46,7 @@
|
|||||||
</TemplateColumn>
|
</TemplateColumn>
|
||||||
</FluentDataGrid>
|
</FluentDataGrid>
|
||||||
|
|
||||||
@if (_currentlyDisplayedModels?.Any() == true) {
|
@if (_totalPages > 1) {
|
||||||
<div class="hopframe-paginator">
|
<div class="hopframe-paginator">
|
||||||
<FluentButton BackgroundColor="transparent" OnClick="() => ChangePage(_currentPage - 1)">
|
<FluentButton BackgroundColor="transparent" OnClick="() => ChangePage(_currentPage - 1)">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size20.ArrowPrevious())" Color="Color.Neutral" />
|
<FluentIcon Value="@(new Icons.Regular.Size20.ArrowPrevious())" Color="Color.Neutral" />
|
||||||
@@ -78,6 +86,7 @@
|
|||||||
@inject IContextExplorer Explorer
|
@inject IContextExplorer Explorer
|
||||||
@inject NavigationManager Navigator
|
@inject NavigationManager Navigator
|
||||||
@inject IJSRuntime Js
|
@inject IJSRuntime Js
|
||||||
|
@inject IDialogService Dialogs
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
@@ -106,7 +115,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||||
|
try {
|
||||||
await Js.InvokeVoidAsync("removeBg");
|
await Js.InvokeVoidAsync("removeBg");
|
||||||
|
}catch (Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenSource _searchCancel = new();
|
private CancellationTokenSource _searchCancel = new();
|
||||||
@@ -132,6 +143,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteEntry(object element) { //TODO: display confirmation
|
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);
|
await _manager!.DeleteItem(element);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(_searchTerm)) {
|
if (!string.IsNullOrEmpty(_searchTerm)) {
|
||||||
@@ -141,4 +156,8 @@
|
|||||||
OnInitialized();
|
OnInitialized();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CreateOrEdit(object? element) {
|
||||||
|
var panel = await Dialogs.ShowPanelAsync<HopFrameEditor>(new EditorDialogData(_config!, element), new());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
22
src/HopFrame.Web/Helpers/TypeExtensions.cs
Normal file
22
src/HopFrame.Web/Helpers/TypeExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/HopFrame.Web/Models/EditorDialogData.cs
Normal file
8
src/HopFrame.Web/Models/EditorDialogData.cs
Normal 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;
|
||||||
|
}
|
||||||
@@ -9,3 +9,4 @@
|
|||||||
@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons
|
@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons
|
||||||
@using Microsoft.FluentUI.AspNetCore.Components.Extensions
|
@using Microsoft.FluentUI.AspNetCore.Components.Extensions
|
||||||
@using HopFrame.Web.Components.Layout
|
@using HopFrame.Web.Components.Layout
|
||||||
|
@using HopFrame.Web.Components.Dialogs
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ public class Post {
|
|||||||
|
|
||||||
[ForeignKey("author")]
|
[ForeignKey("author")]
|
||||||
public User? Author { get; set; }
|
public User? Author { get; set; }
|
||||||
|
|
||||||
|
public bool Published { get; set; }
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,4 @@ public class User {
|
|||||||
public string? Password { get; set; }
|
public string? Password { get; set; }
|
||||||
public string? FirstName { get; set; }
|
public string? FirstName { get; set; }
|
||||||
public string? LastName { get; set; }
|
public string? LastName { get; set; }
|
||||||
|
|
||||||
public override string ToString() {
|
|
||||||
return Id.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -36,9 +36,17 @@ builder.Services.AddHopFrame(options => {
|
|||||||
.Sortable(false);
|
.Sortable(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* context.Table<Post>()
|
||||||
|
.Property(p => p.Author)
|
||||||
|
.DisplayedProperty(u => u!.Username); */
|
||||||
context.Table<Post>()
|
context.Table<Post>()
|
||||||
.Property(p => p.Author)
|
.Property(p => p.Author)
|
||||||
.DisplayedProperty(u => u!.Username);
|
.Format(user => $"{user?.FirstName} {user?.LastName}");
|
||||||
|
|
||||||
|
context.Table<Post>()
|
||||||
|
.Property(p => p.Id)
|
||||||
|
.SetDisplayName("ID")
|
||||||
|
.Editable(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user