diff --git a/.idea/.idea.HopFrame/.idea/workspace.xml b/.idea/.idea.HopFrame/.idea/workspace.xml index e34095f..6d154b7 100644 --- a/.idea/.idea.HopFrame/.idea/workspace.xml +++ b/.idea/.idea.HopFrame/.idea/workspace.xml @@ -10,17 +10,16 @@ - - - - + - - - + + + + + + + @@ -80,24 +81,24 @@ - { - "keyToString": { - ".NET Launch Settings Profile.HopFrame.Testing.executor": "Run", - ".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run", - ".NET Project.HopFrame.Testing.executor": "Run", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.git.unshallow": "true", - "git-widget-placeholder": "feature/setup", - "list.type.of.created.stylesheet": "CSS", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "preferences.environmentSetup", - "vue.rearranger.settings.migration": "true" + +}]]> @@ -187,6 +198,7 @@ - \ No newline at end of file diff --git a/src/HopFrame.Core/Config/PropertyConfig.cs b/src/HopFrame.Core/Config/PropertyConfig.cs index cca2090..14555e7 100644 --- a/src/HopFrame.Core/Config/PropertyConfig.cs +++ b/src/HopFrame.Core/Config/PropertyConfig.cs @@ -11,8 +11,11 @@ public class PropertyConfig(PropertyInfo info) { public bool Searchable { get; set; } = true; public PropertyInfo? DisplayedProperty { get; set; } public Func? Formatter { get; set; } + public Func? Parser { get; set; } + public Func? Template { get; set; } public bool Editable { get; set; } = true; public bool Creatable { get; set; } = true; + public bool DisplayValue { get; set; } = true; } public class PropertyConfig(PropertyConfig config) { @@ -22,8 +25,8 @@ public class PropertyConfig(PropertyConfig config) { return this; } - public PropertyConfig List(bool display) { - config.List = display; + public PropertyConfig List(bool list) { + config.List = list; config.Searchable = false; return this; } @@ -48,6 +51,16 @@ public class PropertyConfig(PropertyConfig config) { return this; } + public PropertyConfig ValueParser(Func parser) { + config.Parser = str => parser.Invoke(str)!; + return this; + } + + public PropertyConfig ValueTemplate(Func template) { + config.Template = () => template.Invoke()!; + return this; + } + public PropertyConfig Editable(bool editable) { config.Editable = editable; return this; @@ -57,5 +70,10 @@ public class PropertyConfig(PropertyConfig config) { config.Creatable = creatable; return this; } + + public PropertyConfig DisplayValue(bool display) { + config.DisplayValue = display; + return this; + } } diff --git a/src/HopFrame.Core/Services/ITableManager.cs b/src/HopFrame.Core/Services/ITableManager.cs index 9683121..00a1e07 100644 --- a/src/HopFrame.Core/Services/ITableManager.cs +++ b/src/HopFrame.Core/Services/ITableManager.cs @@ -8,6 +8,9 @@ public interface ITableManager { public (IEnumerable, int) Search(string searchTerm, int page = 0, int perPage = 20); public int TotalPages(int perPage = 20); public Task DeleteItem(object item); + public Task EditItem(object item); + public Task AddItem(object item); + public Task RevertChanges(object item); public string DisplayProperty(object? item, PropertyInfo info, TableConfig? tableConfig); } \ No newline at end of file diff --git a/src/HopFrame.Core/Services/Implementations/TableManager.cs b/src/HopFrame.Core/Services/Implementations/TableManager.cs index a25c8da..179fe32 100644 --- a/src/HopFrame.Core/Services/Implementations/TableManager.cs +++ b/src/HopFrame.Core/Services/Implementations/TableManager.cs @@ -37,6 +37,20 @@ internal sealed class TableManager(DbContext context, TableConfig config await context.SaveChangesAsync(); } + public async Task EditItem(object item) { + await context.SaveChangesAsync(); + } + + public async Task AddItem(object item) { + var table = context.Set(); + await table.AddAsync((TModel)item); + await context.SaveChangesAsync(); + } + + public async Task RevertChanges(object item) { + await context.Entry((TModel)item).ReloadAsync(); + } + private bool ItemSearched(TModel item, string searchTerm) { foreach (var property in config.Properties) { if (!property.Searchable) continue; diff --git a/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor b/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor index b803c91..067f9f0 100644 --- a/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor +++ b/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor @@ -1,8 +1,10 @@ @implements IDialogContentComponent -@using HopFrame.Web.Models +@using HopFrame.Core.Config @using HopFrame.Core.Services +@using HopFrame.Web.Models @using HopFrame.Web.Helpers +@using Microsoft.EntityFrameworkCore.Internal @foreach (var property in Content.Config.Properties) { @@ -13,23 +15,69 @@ - } else if (property.Info.PropertyType == typeof(bool)) { + ValueChanged="@(v => SetPropertyValue(property, v, InputType.Number))" /> + } + else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.Boolean) { + Value="GetPropertyValue(property)" + Disabled="@(!property.Editable)" + ValueChanged="@(v => SetPropertyValue(property, v, InputType.Switch))" /> + } + else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.DateTime) { +
+
+ +
+
+ +
+
+ } + else if (property.Info.PropertyType == typeof(DateOnly)) { + + } + else if (property.Info.PropertyType == typeof(TimeOnly)) { + + } + else if (property.Info.PropertyType.IsEnum) { + } else { + ValueChanged="@(v => SetPropertyValue(property, v, InputType.Text))" /> } } @@ -44,16 +92,103 @@ [CascadingParameter] public required FluentDialog Dialog { get; set; } - private ITableManager? _manager; private bool _currentlyEditing; + private ITableManager? _manager; 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.Title = (_currentlyEditing ? "Edit " : "Add ") + Content.Config.TableType.Name; Dialog.Instance.Parameters.PreventScroll = true; Dialog.Instance.Parameters.Width = "500px"; Dialog.Instance.Parameters.PrimaryAction = "Save"; _manager = Explorer.GetTableManager(Content.Config.PropertyName); + Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType); + } + + private TValue? GetPropertyValue(PropertyConfig config) { //TODO: handle relational types + if (!config.DisplayValue) return default; + if (Content.CurrentObject is null) return default; + var value = config.Info.GetValue(Content.CurrentObject); + + var newlyGenerated = false; + if (config.Info.PropertyType.IsDefaultValue(value) && config.Template is not null) { + value = config.Template.Invoke(); + newlyGenerated = true; + } + + if (value is null) + return default; + + if (config.Info.PropertyType == typeof(TValue)) + return (TValue)value; + + if (typeof(TValue) == typeof(string)) { + if (!newlyGenerated) + return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config.Info, Content.Config); + + return (TValue)(object)value.ToString()!; + } + + return (TValue)Convert.ChangeType(value, typeof(TValue)); + } + + private void SetPropertyValue(PropertyConfig config, object? value, InputType senderType) { + object? result = null; + + if (value is not null) { + switch (senderType) { + case InputType.Number: + result = Convert.ChangeType(value, config.Info.PropertyType); + break; + + case InputType.Text: + result = Convert.ToString(value); + break; + + case InputType.Switch: + result = Convert.ToBoolean(value); + break; + + case InputType.Enum: + result = Enum.Parse(config.Info.PropertyType, (string)value); + break; + + case InputType.Date: + if (config.Info.PropertyType == typeof(DateTime)) { + var newDate = (DateOnly)value; + var dateTime = GetPropertyValue(config); + result = new DateTime(newDate.Year, newDate.Month, newDate.Day, dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, dateTime.Microsecond); + } + else result = (DateOnly)value; + break; + + case InputType.Time: + if (config.Info.PropertyType == typeof(DateTime)) { + var newTime = (TimeOnly)value; + var dateTime = GetPropertyValue(config); + result = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, newTime.Hour, newTime.Minute, newTime.Second, newTime.Millisecond, newTime.Microsecond); + } + else result = (TimeOnly)value; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(senderType), senderType, null); + } + } + + if (config.Parser is not null) { + result = config.Parser(result!.ToString()!); + } + + config.Info.SetValue(Content.CurrentObject, result); + } + + private enum InputType { + Number, + Switch, + Date, + Time, + Enum, + Text } } diff --git a/src/HopFrame.Web/Components/Pages/HopFrameListView.razor b/src/HopFrame.Web/Components/Pages/HopFrameListView.razor index b3c80b7..fe520f4 100644 --- a/src/HopFrame.Web/Components/Pages/HopFrameListView.razor +++ b/src/HopFrame.Web/Components/Pages/HopFrameListView.razor @@ -76,8 +76,7 @@ const elements = document.querySelectorAll(".col-sort-button"); const style = new CSSStyleSheet(); style.replaceSync(".control { background: none !important; }"); - elements.forEach(e => e.shadowRoot.adoptedStyleSheets.push(style)); - console.log(elements); + elements.forEach(e => e?.shadowRoot?.adoptedStyleSheets?.push(style)); } removeBg(); @@ -97,19 +96,19 @@ private ITableManager? _manager; private IEnumerable? _currentlyDisplayedModels; - private int _currentPage = 0; + private int _currentPage; private int _totalPages; private string? _searchTerm; protected override void OnInitialized() { - _config = Explorer.GetTable(TableName); + _config ??= Explorer.GetTable(TableName); if (_config is null) { Navigator.NavigateTo("/admin", true); return; } - _manager = Explorer.GetTableManager(_config.PropertyName); + _manager ??= Explorer.GetTableManager(_config.PropertyName); _currentlyDisplayedModels = _manager!.LoadPage(_currentPage).ToArray(); _totalPages = _manager.TotalPages(); } @@ -117,7 +116,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { try { await Js.InvokeVoidAsync("removeBg"); - }catch (Exception) {} + } + catch (Exception) { + // ignored + } } private CancellationTokenSource _searchCancel = new(); @@ -128,36 +130,52 @@ _searchCancel = new(); await Task.Delay(500, _searchCancel.Token); - (_currentlyDisplayedModels, _totalPages) = _manager!.Search(_searchTerm); + (var query, _totalPages) = _manager!.Search(_searchTerm); + _currentlyDisplayedModels = query.ToArray(); } - private void ChangePage(int page) { - if (page < 0 || page > _totalPages - 1) return; - _currentPage = page; + private void Reload() { if (!string.IsNullOrEmpty(_searchTerm)) { - (_currentlyDisplayedModels, _totalPages) = _manager!.Search(_searchTerm, page); - } - else { - _currentlyDisplayedModels = _manager!.LoadPage(page); - } - } - - 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)) { - (_currentlyDisplayedModels, _totalPages) = _manager!.Search(_searchTerm); + (var query, _totalPages) = _manager!.Search(_searchTerm); + _currentlyDisplayedModels = query.ToArray(); } else { OnInitialized(); } } + private void ChangePage(int page) { + if (page < 0 || page > _totalPages - 1) return; + _currentPage = page; + Reload(); + } + + private async Task DeleteEntry(object element) { + 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); + + Reload(); + } + private async Task CreateOrEdit(object? element) { var panel = await Dialogs.ShowPanelAsync(new EditorDialogData(_config!, element), new()); + var result = await panel.Result; + var data = result.Data as EditorDialogData; + + if (result.Cancelled) { + if (data?.CurrentObject is not null) + await _manager!.RevertChanges(data.CurrentObject); + return; + } + + if (element is null) + await _manager!.AddItem(data!.CurrentObject!); + else + await _manager!.EditItem(data!.CurrentObject!); + + Reload(); } } \ No newline at end of file diff --git a/src/HopFrame.Web/Helpers/TypeExtensions.cs b/src/HopFrame.Web/Helpers/TypeExtensions.cs index 9745208..9ad43bf 100644 --- a/src/HopFrame.Web/Helpers/TypeExtensions.cs +++ b/src/HopFrame.Web/Helpers/TypeExtensions.cs @@ -2,6 +2,7 @@ namespace HopFrame.Web.Helpers; internal static class TypeExtensions { public static bool IsNumeric(this Type o) { + if (o.IsEnum) return false; switch (Type.GetTypeCode(o)) { case TypeCode.Byte: case TypeCode.SByte: diff --git a/src/HopFrame.Web/Models/EditorDialogData.cs b/src/HopFrame.Web/Models/EditorDialogData.cs index a055980..a3a8e4b 100644 --- a/src/HopFrame.Web/Models/EditorDialogData.cs +++ b/src/HopFrame.Web/Models/EditorDialogData.cs @@ -3,6 +3,6 @@ using HopFrame.Core.Config; namespace HopFrame.Web.Models; public sealed class EditorDialogData(TableConfig config, object? current = null) { - public object? CurrentObject { get; } = current; + public object? CurrentObject { get; set; } = current; public TableConfig Config { get; } = config; } diff --git a/testing/HopFrame.Testing/Components/Pages/Home.razor b/testing/HopFrame.Testing/Components/Pages/Home.razor index c25ecb7..2e13e12 100644 --- a/testing/HopFrame.Testing/Components/Pages/Home.razor +++ b/testing/HopFrame.Testing/Components/Pages/Home.razor @@ -23,7 +23,8 @@ Welcome to your new Fluent Blazor app. Id = Guid.CreateVersion7(), FirstName = first, LastName = last, - Username = username + Username = username, + Password = GenerateName(Random.Shared.Next(8, 16)) }; Context.Users.Add(user); diff --git a/testing/HopFrame.Testing/Models/Post.cs b/testing/HopFrame.Testing/Models/Post.cs index c2c233f..0f82723 100644 --- a/testing/HopFrame.Testing/Models/Post.cs +++ b/testing/HopFrame.Testing/Models/Post.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace HopFrame.Testing.Models; @@ -15,5 +16,13 @@ public class Post { [ForeignKey("author")] public User? Author { get; set; } - public bool Published { get; set; } + /*public bool Published { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateOnly Created { get; set; } + + public TimeOnly At { get; set; }*/ + + public ListSortDirection Type { get; set; } } \ No newline at end of file diff --git a/testing/HopFrame.Testing/Program.cs b/testing/HopFrame.Testing/Program.cs index 06890fb..5946cef 100644 --- a/testing/HopFrame.Testing/Program.cs +++ b/testing/HopFrame.Testing/Program.cs @@ -24,7 +24,8 @@ builder.Services.AddHopFrame(options => { options.AddDbContext(context => { context.Table(table => { table.Property(u => u.Password) - .List(false); + .List(false) + .DisplayValue(false); table.Property(u => u.FirstName) .SetDisplayName("First Name"); @@ -33,20 +34,17 @@ builder.Services.AddHopFrame(options => { .SetDisplayName("Last Name"); table.Property(u => u.Id) - .Sortable(false); + .Sortable(false) + .ValueTemplate(Guid.CreateVersion7); }); - /* context.Table() - .Property(p => p.Author) - .DisplayedProperty(u => u!.Username); */ context.Table() .Property(p => p.Author) .Format(user => $"{user?.FirstName} {user?.LastName}"); context.Table() .Property(p => p.Id) - .SetDisplayName("ID") - .Editable(true); + .SetDisplayName("ID"); }); });