diff --git a/debug/TestApplication/DatabaseContext.cs b/debug/TestApplication/DatabaseContext.cs index 1ecf409..2e57c79 100644 --- a/debug/TestApplication/DatabaseContext.cs +++ b/debug/TestApplication/DatabaseContext.cs @@ -9,6 +9,8 @@ public class DatabaseContext(DbContextOptions options) : DbCont public DbSet Posts { get; set; } + public DbSet Typers { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -22,5 +24,8 @@ public class DatabaseContext(DbContextOptions options) : DbCont .HasMany(u => u.Posts) .WithOne(p => p.Sender) .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasKey(t => t.Id); } } \ No newline at end of file diff --git a/debug/TestApplication/Models/Typer.cs b/debug/TestApplication/Models/Typer.cs new file mode 100644 index 0000000..16597e0 --- /dev/null +++ b/debug/TestApplication/Models/Typer.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace TestApplication.Models; + +public class Typer { + [Key] + public Guid Id { get; set; } + public int Number { get; set; } + public bool Toggle { get; set; } + public DateTime DateTime { get; set; } + public DateOnly DateOnly { get; set; } + public TimeOnly TimeOnly { get; set; } + public ListSortDirection SortDirection { get; set; } + public string? Text { get; set; } + [EmailAddress] + public string? Mail { get; set; } + public string? LongText { get; set; } + public string? Password { get; set; } + public string? PhoneNumber { get; set; } + public List List { get; set; } = new(); + public List SortDirections { get; set; } = new(); +} \ No newline at end of file diff --git a/debug/TestApplication/Program.cs b/debug/TestApplication/Program.cs index bb3933d..9e0b3a1 100644 --- a/debug/TestApplication/Program.cs +++ b/debug/TestApplication/Program.cs @@ -1,3 +1,4 @@ +using HopFrame.Core.Configuration; using HopFrame.Core.EFCore; using HopFrame.Web; using Microsoft.EntityFrameworkCore; @@ -30,6 +31,17 @@ builder.Services.AddHopFrame(config => { config.Table(table => { table.SetDescription("The posts dataset. It contains all posts sent via the application."); }); + + config.Table(table => { + table.Property(t => t.LongText) + .SetType(PropertyType.TextArea); + + table.Property(t => t.Password) + .SetType(PropertyType.Password); + + table.Property(t => t.PhoneNumber) + .SetType(PropertyType.PhoneNumber); + }); }); var app = builder.Build(); diff --git a/src/HopFrame.Core/Configurators/TableConfigurator.cs b/src/HopFrame.Core/Configurators/TableConfigurator.cs index c250f3d..2305596 100644 --- a/src/HopFrame.Core/Configurators/TableConfigurator.cs +++ b/src/HopFrame.Core/Configurators/TableConfigurator.cs @@ -47,7 +47,7 @@ public class TableConfigurator(TableConfig config) where TModel : class } /// - public PropertyConfigurator Property(Expression> propertyExpression) { + public PropertyConfigurator Property(Expression> propertyExpression) { var propertyName = ExpressionHelper.GetPropertyInfo(propertyExpression).Name; var prop = Config.Properties.FirstOrDefault(p => p.Identifier == propertyName); @@ -58,7 +58,7 @@ public class TableConfigurator(TableConfig config) where TModel : class } /// - public TableConfigurator SetPreferredProperty(Expression> propertyExpression) { + public TableConfigurator SetPreferredProperty(Expression> propertyExpression) { var propertyName = ExpressionHelper.GetPropertyInfo(propertyExpression).Name; var prop = Config.Properties.FirstOrDefault(p => p.Identifier == propertyName); diff --git a/src/HopFrame.Web/Components/Components/DeleteConfirmationDialog.razor b/src/HopFrame.Web/Components/Components/DeleteConfirmationDialog.razor new file mode 100644 index 0000000..4f14d09 --- /dev/null +++ b/src/HopFrame.Web/Components/Components/DeleteConfirmationDialog.razor @@ -0,0 +1,23 @@ + + + Confirm + + + Do you really want to delete this entry? + + + Cancel + Delete + + + +@code { + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + private void Submit() => MudDialog.Close(DialogResult.Ok(true)); + + private void Cancel() => MudDialog.Cancel(); + +} diff --git a/src/HopFrame.Web/Components/Components/Editor.razor b/src/HopFrame.Web/Components/Components/Editor.razor new file mode 100644 index 0000000..f61e3c6 --- /dev/null +++ b/src/HopFrame.Web/Components/Components/Editor.razor @@ -0,0 +1,35 @@ + + + + + @if (Mode == EditorMode.Creator) { + Add @Config.TableType.Name + } + else { + Edit @Config.TableType.Name + } + + + + + @foreach (var property in GetProperties()) { + + } + + + + + Save + Cancel + + + + \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Components/Editor.razor.cs b/src/HopFrame.Web/Components/Components/Editor.razor.cs new file mode 100644 index 0000000..a0213eb --- /dev/null +++ b/src/HopFrame.Web/Components/Components/Editor.razor.cs @@ -0,0 +1,63 @@ +using HopFrame.Core.Configuration; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace HopFrame.Web.Components.Components; + +public partial class Editor(IDialogService dialogs) : ComponentBase { + + private enum EditorMode { + Editor, + Creator + } + + [Parameter] + public required TableConfig Config { get; set; } + + private bool IsVisible { get; set; } + + private object? Entry { get; set; } + + private EditorMode Mode { get; set; } + + private TaskCompletionSource Completion { get; set; } = null!; + + public Task Present(object? entry) { + Completion = new (); + Mode = entry is null ? EditorMode.Creator : EditorMode.Editor; + Entry = entry ?? Activator.CreateInstance(Config.TableType); + StateHasChanged(); + IsVisible = true; + return Completion.Task; + } + + private async Task Submit() { + var dialog = await dialogs.ShowAsync(); + var result = await dialog.Result; + + if (result is not null && !result.Canceled) { + ApplyChanges(); + IsVisible = false; + Completion.SetResult(Entry); + } + } + + private void Cancel() { + IsVisible = false; + Completion.SetResult(null); + } + + private IEnumerable GetProperties() { + var query = (IEnumerable)Config.Properties; + + if (Mode == EditorMode.Creator) + query = query.Where(p => p.Creatable); + + return query.OrderBy(p => p.OrderIndex); + } + + private void ApplyChanges() { + + } + +} \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Components/ModifyConfirmationDialog.razor b/src/HopFrame.Web/Components/Components/ModifyConfirmationDialog.razor new file mode 100644 index 0000000..29dd133 --- /dev/null +++ b/src/HopFrame.Web/Components/Components/ModifyConfirmationDialog.razor @@ -0,0 +1,23 @@ + + + Confirm + + + Do you really want to save the changes? + + + Cancel + Save + + + +@code { + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + private void Submit() => MudDialog.Close(DialogResult.Ok(true)); + + private void Cancel() => MudDialog.Cancel(); + +} \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Components/PropertyInput.razor b/src/HopFrame.Web/Components/Components/PropertyInput.razor new file mode 100644 index 0000000..e9eab81 --- /dev/null +++ b/src/HopFrame.Web/Components/Components/PropertyInput.razor @@ -0,0 +1,106 @@ +@using HopFrame.Core.Configuration + +@switch ((PropertyType)((byte)Config.PropertyType & 0x0F)) { + case PropertyType.Numeric: + + break; + + case PropertyType.Boolean: + + break; + + case PropertyType.DateTime: + + + + + + + + break; + + case PropertyType.DateOnly: + + break; + + case PropertyType.TimeOnly: + + break; + + case PropertyType.Email: + + break; + + case PropertyType.Password: + + break; + + case PropertyType.PhoneNumber: + + break; + + default: + + break; +} + +@code { + + [Parameter] + public object? Value { get; set; } + + [Parameter] + public required PropertyConfig Config { get; set; } + + [Parameter] + public Variant Variant { get; set; } + +} \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Components/Table.razor b/src/HopFrame.Web/Components/Components/Table.razor index 26e5570..8e3c32f 100644 --- a/src/HopFrame.Web/Components/Components/Table.razor +++ b/src/HopFrame.Web/Components/Components/Table.razor @@ -1,6 +1,6 @@ @rendermode InteractiveServer - - Add + + Add + @@ -45,13 +49,13 @@ @foreach (var prop in OrderedProperties) { - @context[prop.Identifier] + @context.Columns[prop.Identifier] } - - + + diff --git a/src/HopFrame.Web/Components/Components/Table.razor.cs b/src/HopFrame.Web/Components/Components/Table.razor.cs index f2f9d18..6d2d507 100644 --- a/src/HopFrame.Web/Components/Components/Table.razor.cs +++ b/src/HopFrame.Web/Components/Components/Table.razor.cs @@ -8,14 +8,28 @@ namespace HopFrame.Web.Components.Components; public partial class Table(IEntityAccessor accessor, IConfigAccessor configAccessor) : ComponentBase { + private readonly struct TableEntry { + public object Entry { get; init; } + public Dictionary Columns { get; init; } + } + [Parameter] public required TableConfig Config { get; set; } + [Parameter] + public EventCallback OnAdd { get; set; } + + [Parameter] + public EventCallback OnDelete { get; set; } + + [Parameter] + public EventCallback OnEdit { get; set; } + private IHopFrameRepository Repository { get; set; } = null!; private PropertyConfig[] OrderedProperties { get; set; } = null!; - private MudTable> Manager { get; set; } = null!; + private MudTable Manager { get; set; } = null!; private Dictionary> SortDirections { get; set; } = new(); @@ -38,22 +52,18 @@ public partial class Table(IEntityAccessor accessor, IConfigAccessor configAcces } } - private List> PrepareData(object[] entries) { - var list = new List>(); - - foreach (var entry in entries) { - var dict = new Dictionary(); - foreach (var prop in OrderedProperties) { - dict.Add(prop.Identifier, accessor.GetValue(entry, prop) ?? string.Empty); - } - - list.Add(dict); + public Task Reload() => Manager.ReloadServerData(); + + private Dictionary PrepareData(object entry) { + var dict = new Dictionary(); + foreach (var prop in OrderedProperties) { + dict.Add(prop.Identifier, accessor.GetValue(entry, prop) ?? string.Empty); } - return list; + return dict; } - private async Task>> Reload(TableState state, CancellationToken ct) { + private async Task> ReloadTable(TableState state, CancellationToken ct) { IEnumerable entries; if (string.IsNullOrWhiteSpace(_searchText)) @@ -65,11 +75,14 @@ public partial class Table(IEntityAccessor accessor, IConfigAccessor configAcces var sortProp = Config.Properties.First(p => p.Identifier == _currentSort.Value.Key); entries = accessor.SortDataByProperty(entries, sortProp, _currentSort.Value.Value == SortDirection.Descending); } - - var data = PrepareData(entries.ToArray()); + + var data = entries.Select(e => new TableEntry { + Entry = e, + Columns = PrepareData(e) + }); var total = await Repository.CountAsync(ct); - return new TableData> { + return new TableData { TotalItems = total, Items = data }; @@ -80,7 +93,11 @@ public partial class Table(IEntityAccessor accessor, IConfigAccessor configAcces await Manager.ReloadServerData(); } + private bool _currentlyReloading; private async Task OnSort(PropertyConfig property, SortDirection direction) { + if (_currentlyReloading) return; + _currentlyReloading = true; + if (direction != SortDirection.None) { foreach (var reference in SortDirections .Where(d => d.Key != property.Identifier)) { @@ -98,5 +115,21 @@ public partial class Table(IEntityAccessor accessor, IConfigAccessor configAcces } await Manager.ReloadServerData(); + _currentlyReloading = false; + } + + private async Task OnAddClick() { + if (OnAdd.HasDelegate) + await OnAdd.InvokeAsync(); + } + + private async Task OnEditClick(object entry) { + if (OnEdit.HasDelegate) + await OnEdit.InvokeAsync(entry); + } + + private async Task OnDeleteClick(object entry) { + if (OnDelete.HasDelegate) + await OnDelete.InvokeAsync(entry); } } \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Pages/TablePage.razor b/src/HopFrame.Web/Components/Pages/TablePage.razor index c411824..341deb1 100644 --- a/src/HopFrame.Web/Components/Pages/TablePage.razor +++ b/src/HopFrame.Web/Components/Pages/TablePage.razor @@ -2,11 +2,19 @@ @using HopFrame.Web.Components.Components @rendermode InteractiveServer @layout HopFrameLayout +@inherits CancellableComponent - + HopFrame - @Table.DisplayName -
+ + +
diff --git a/src/HopFrame.Web/Components/Pages/TablePage.razor.cs b/src/HopFrame.Web/Components/Pages/TablePage.razor.cs index 0fe78df..6fa6d81 100644 --- a/src/HopFrame.Web/Components/Pages/TablePage.razor.cs +++ b/src/HopFrame.Web/Components/Pages/TablePage.razor.cs @@ -1,16 +1,24 @@ using HopFrame.Core.Configuration; +using HopFrame.Core.Repositories; using HopFrame.Core.Services; +using HopFrame.Web.Components.Components; using Microsoft.AspNetCore.Components; +using MudBlazor; namespace HopFrame.Web.Components.Pages; -public partial class TablePage(IConfigAccessor accessor, NavigationManager navigator) : ComponentBase { - private const int PerPage = 25; +public partial class TablePage(IConfigAccessor accessor, NavigationManager navigator, IDialogService dialogs, ISnackbar snackbar) : CancellableComponent { [Parameter] public string TableRoute { get; set; } = null!; - public TableConfig Table { get; set; } = null!; + private TableConfig Table { get; set; } = null!; + + private IHopFrameRepository Repository { get; set; } = null!; + + private Table TableComponent { get; set; } = null!; + + private Editor EditorComponent { get; set; } = null!; protected override void OnInitialized() { base.OnInitialized(); @@ -23,5 +31,36 @@ public partial class TablePage(IConfigAccessor accessor, NavigationManager navig } Table = table; + Repository = accessor.LoadRepository(table); + snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomLeft; + } + + private async Task OnAdd() { + var entry = await EditorComponent.Present(null); + if (entry is null) return; + + await Repository.CreateGenericAsync(entry, TokenSource.Token); + await TableComponent.Reload(); + snackbar.Add("Entry added", Severity.Success); + } + + private async Task OnEdit(object entry) { + var newEntry = await EditorComponent.Present(entry); + if (newEntry is null) return; + + await Repository.UpdateGenericAsync(newEntry, TokenSource.Token); + await TableComponent.Reload(); + snackbar.Add("Entry updated", Severity.Success); + } + + private async Task OnDelete(object entry) { + var dialog = await dialogs.ShowAsync(); + var result = await dialog.Result; + + if (result is not null && !result.Canceled) { + await Repository.DeleteGenericAsync(entry, TokenSource.Token); + await TableComponent.Reload(); + snackbar.Add("Entry deleted", Severity.Success); + } } } \ No newline at end of file