Added policy validation, ordering and virtual listing properties
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
@using Microsoft.EntityFrameworkCore.Internal
|
||||
|
||||
<FluentDialogBody>
|
||||
@foreach (var property in Content.Config.Properties) {
|
||||
@foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) {
|
||||
if (!_currentlyEditing && !property.Creatable) continue;
|
||||
|
||||
<div style="margin-bottom: 20px">
|
||||
@@ -21,10 +21,10 @@
|
||||
Required="@property.IsRequired"
|
||||
ReadOnly="true"
|
||||
Style="width: 100%"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Text))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px; margin-bottom: 4px">
|
||||
<FluentButton OnClick="() => SetPropertyValue(property, null, InputType.Relation)" Disabled="@(!property.Editable)">
|
||||
<FluentButton OnClick="async () => await SetPropertyValue(property, null, InputType.Relation)" Disabled="@(!property.Editable)">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
||||
</FluentButton>
|
||||
<FluentButton OnClick="async () => await OpenRelationalPicker(property)" Disabled="@(!property.Editable)">
|
||||
@@ -41,7 +41,7 @@
|
||||
Style="width: 100%;"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Number))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Number))" />
|
||||
}
|
||||
else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.Boolean) {
|
||||
<FluentSwitch
|
||||
@@ -49,24 +49,24 @@
|
||||
Value="GetPropertyValue<bool>(property)"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Switch))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Switch))" />
|
||||
}
|
||||
else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.DateTime) {
|
||||
<div style="display: flex; gap: 10px">
|
||||
<div style="display: flex; gap: 5px">
|
||||
<div style="display: flex; flex-direction: column; width: 100%">
|
||||
<FluentDatePicker
|
||||
Label="@property.Name"
|
||||
Value="GetPropertyValue<DateTime>(property)"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Date))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Date))" />
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; justify-content: flex-end">
|
||||
<FluentTimePicker
|
||||
Value="GetPropertyValue<DateTime>(property)"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Time))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Time))" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -77,7 +77,7 @@
|
||||
Style="width: 100%"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Date))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Date))" />
|
||||
}
|
||||
else if (property.Info.PropertyType == typeof(TimeOnly)) {
|
||||
<FluentTimePicker
|
||||
@@ -86,7 +86,7 @@
|
||||
Style="width: 100%"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Time))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Time))" />
|
||||
}
|
||||
else if (property.Info.PropertyType.IsEnum) {
|
||||
<FluentSelect
|
||||
@@ -98,7 +98,28 @@
|
||||
Height="250px"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Enum))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Enum))" />
|
||||
}
|
||||
else if (property.Info.PropertyType.IsNullableEnum()) {
|
||||
<div style="display: flex; gap: 5px; align-items: flex-end">
|
||||
<div style="flex-grow: 1">
|
||||
<FluentSelect
|
||||
TOption="string"
|
||||
Label="@property.Name"
|
||||
Items="@(["", ..Enum.GetNames(Nullable.GetUnderlyingType(property.Info.PropertyType)!)])"
|
||||
Value="@(GetPropertyValue<string>(property))"
|
||||
Style="width: 100%"
|
||||
Height="250px"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Enum))" />
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px">
|
||||
<FluentButton OnClick="async () => await SetPropertyValue(property, null, InputType.Enum)" Disabled="@(!property.Editable)">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
||||
</FluentButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
<FluentTextField
|
||||
@@ -107,7 +128,7 @@
|
||||
Style="width: 100%;"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Text))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
|
||||
}
|
||||
|
||||
@foreach (var error in _validationErrors[property.Info.Name]) {
|
||||
@@ -119,6 +140,7 @@
|
||||
|
||||
@inject IContextExplorer Explorer
|
||||
@inject IDialogService Dialogs
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -141,6 +163,7 @@
|
||||
Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType);
|
||||
|
||||
foreach (var property in Content.Config.Properties) {
|
||||
if (property.IsListingProperty) continue;
|
||||
_validationErrors.Add(property.Info.Name, []);
|
||||
}
|
||||
}
|
||||
@@ -157,15 +180,18 @@
|
||||
return (TValue)value;
|
||||
|
||||
if (typeof(TValue) == typeof(string))
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config.Info, Content.Config);
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, Content.Config);
|
||||
|
||||
return (TValue)Convert.ChangeType(value, typeof(TValue));
|
||||
}
|
||||
|
||||
private void SetPropertyValue(PropertyConfig config, object? value, InputType senderType) {
|
||||
private async Task SetPropertyValue(PropertyConfig config, object? value, InputType senderType) {
|
||||
if (!await Handler.IsAuthenticatedAsync(_currentlyEditing ? Content.Config.UpdatePolicy : Content.Config.CreatePolicy))
|
||||
return;
|
||||
|
||||
object? result = null;
|
||||
|
||||
if (value is not null) {
|
||||
if (value is not null && config.Parser is null) {
|
||||
switch (senderType) {
|
||||
case InputType.Number:
|
||||
result = Convert.ChangeType(value, config.Info.PropertyType);
|
||||
@@ -180,7 +206,10 @@
|
||||
break;
|
||||
|
||||
case InputType.Enum:
|
||||
result = Enum.Parse(config.Info.PropertyType, (string)value);
|
||||
var type = Nullable.GetUnderlyingType(config.Info.PropertyType);
|
||||
if (type is not null && string.IsNullOrEmpty((string)value)) break;
|
||||
type ??= config.Info.PropertyType;
|
||||
result = Enum.Parse(type, (string)value);
|
||||
break;
|
||||
|
||||
case InputType.Date:
|
||||
@@ -218,6 +247,9 @@
|
||||
}
|
||||
|
||||
private async Task OpenRelationalPicker(PropertyConfig config) {
|
||||
if (!await Handler.IsAuthenticatedAsync(_currentlyEditing ? Content.Config.UpdatePolicy : Content.Config.CreatePolicy))
|
||||
return;
|
||||
|
||||
var relationTable = Explorer.GetTable(config.Info.PropertyType);
|
||||
if (relationTable is null) return;
|
||||
|
||||
@@ -227,11 +259,16 @@
|
||||
if (result.Cancelled) return;
|
||||
|
||||
var data = (RelationPickerDialogData)result.Data!;
|
||||
SetPropertyValue(config, data.Object, InputType.Relation);
|
||||
await SetPropertyValue(config, data.Object, InputType.Relation);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateInputs() {
|
||||
if (!await Handler.IsAuthenticatedAsync(_currentlyEditing ? Content.Config.UpdatePolicy : Content.Config.CreatePolicy))
|
||||
return false;
|
||||
|
||||
foreach (var property in Content.Config.Properties) {
|
||||
if (property.IsListingProperty) continue;
|
||||
|
||||
var errorList = _validationErrors[property.Info.Name];
|
||||
errorList.Clear();
|
||||
var value = property.Info.GetValue(Content.CurrentObject);
|
||||
@@ -242,7 +279,7 @@
|
||||
}
|
||||
|
||||
if (value is null && property.IsRequired)
|
||||
errorList.Add("Value cannot be null");
|
||||
errorList.Add($"{property.Name} is required");
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@using HopFrame.Core.Services
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
|
||||
<FluentAppBar Orientation="Orientation.Vertical" Style="background-color: var(--neutral-layer-2); height: auto">
|
||||
<FluentAppBarItem Href="/admin"
|
||||
Match="NavLinkMatch.All"
|
||||
@@ -9,7 +11,7 @@
|
||||
|
||||
<br>
|
||||
|
||||
@foreach (var table in Explorer.GetTableNames()) {
|
||||
@foreach (var table in _tables.OrderBy(t => t.Order).Select(t => t.DisplayName)) {
|
||||
<FluentAppBarItem Href="@("/admin/" + table.ToLower())"
|
||||
Match="NavLinkMatch.All"
|
||||
IconActive="new Icons.Filled.Size24.Database()"
|
||||
@@ -20,3 +22,18 @@
|
||||
</FluentAppBar>
|
||||
|
||||
@inject IContextExplorer Explorer
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
|
||||
@code {
|
||||
|
||||
private readonly List<TableConfig> _tables = [];
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
foreach (var table in Explorer.GetTables()) {
|
||||
if (table.Ignored) continue;
|
||||
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
||||
_tables.Add(table);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,45 @@
|
||||
@page "/admin"
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
@layout HopFrameLayout
|
||||
|
||||
<h3>HopFrameHome</h3>
|
||||
<PageTitle>HopFrame</PageTitle>
|
||||
|
||||
<div style="padding: 1.5rem 1.5rem;">
|
||||
<h2>Tables</h2>
|
||||
|
||||
<FluentStack Orientation="Orientation.Horizontal" Wrap="true" Style="margin-top: 1.5rem">
|
||||
@foreach (var table in _tables.OrderBy(t => t.Order)) {
|
||||
<FluentCard Width="350px" Height="200px" Style="display: flex; flex-direction: column; background-color: var(--neutral-layer-1)">
|
||||
<h3 style="margin-bottom: 0;">@table.DisplayName</h3>
|
||||
<FluentLabel Typo="Typography.Body" Color="Color.Info" Style="margin-bottom: 0.5rem">@table.ViewPolicy</FluentLabel>
|
||||
<span>@table.Description</span>
|
||||
<FluentSpacer />
|
||||
<div style="display: flex">
|
||||
<FluentSpacer/>
|
||||
|
||||
<a href="@("/admin/" + table.DisplayName.ToLower())" style="display: inline-block">
|
||||
<FluentButton>Open</FluentButton>
|
||||
</a>
|
||||
</div>
|
||||
</FluentCard>
|
||||
}
|
||||
</FluentStack>
|
||||
</div>
|
||||
|
||||
@inject IContextExplorer Explorer
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
|
||||
@code {
|
||||
|
||||
private readonly List<TableConfig> _tables = [];
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
foreach (var table in Explorer.GetTables()) {
|
||||
if (table.Ignored) continue;
|
||||
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
||||
_tables.Add(table);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,23 +9,29 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@if (!DisplaySelection) {
|
||||
<PageTitle>@_config?.DisplayName</PageTitle>
|
||||
}
|
||||
|
||||
<FluentDialogProvider />
|
||||
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
<FluentToolbar Class="hopframe-toolbar">
|
||||
<h3>@_config?.DisplayName</h3>
|
||||
<FluentButton
|
||||
IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
|
||||
OnClick="Reload"
|
||||
Loading="_loading"
|
||||
Style="margin-left: 10px">
|
||||
Refresh
|
||||
</FluentButton>
|
||||
@if (!DisplaySelection) {
|
||||
<FluentButton
|
||||
IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
|
||||
OnClick="Reload"
|
||||
Loading="_loading"
|
||||
Style="margin-left: 10px">
|
||||
Refresh
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
<FluentSpacer />
|
||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" />
|
||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
||||
|
||||
@if (!DisplaySelection) {
|
||||
@if (!DisplaySelection && _hasCreatePolicy) {
|
||||
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entry</FluentButton>
|
||||
}
|
||||
</FluentToolbar>
|
||||
@@ -47,25 +53,30 @@
|
||||
|
||||
<div style="flex-grow: 1">
|
||||
<FluentDataGrid Items="_currentlyDisplayedModels.AsQueryable()">
|
||||
@foreach (var property in _config!.Properties.Where(prop => prop.List)) {
|
||||
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
|
||||
<PropertyColumn
|
||||
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property.Info, _config)"
|
||||
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property, _config)"
|
||||
Style="min-width: max-content; height: 44px;"
|
||||
Sortable="@property.Sortable"/>
|
||||
}
|
||||
|
||||
@if (DisplayActions) {
|
||||
@if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy)) {
|
||||
var dataIndex = 0;
|
||||
|
||||
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content">
|
||||
@{ var currentElement = _currentlyDisplayedModels.ElementAtOrDefault(dataIndex); }
|
||||
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(currentElement); }">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
||||
</FluentButton>
|
||||
|
||||
@if (_hasUpdatePolicy) {
|
||||
<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!); }">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
||||
</FluentButton>
|
||||
@if (_hasDeletePolicy) {
|
||||
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(currentElement!); }">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning" />
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
@{
|
||||
dataIndex++;
|
||||
@@ -116,6 +127,7 @@
|
||||
@inject NavigationManager Navigator
|
||||
@inject IJSRuntime Js
|
||||
@inject IDialogService Dialogs
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
|
||||
@code {
|
||||
|
||||
@@ -142,7 +154,10 @@
|
||||
private int _totalPages;
|
||||
private string? _searchTerm;
|
||||
private bool _loading;
|
||||
private int _selectedIndex = -1;
|
||||
|
||||
private bool _hasUpdatePolicy;
|
||||
private bool _hasDeletePolicy;
|
||||
private bool _hasCreatePolicy;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
_config ??= Explorer.GetTable(TableDisplayName);
|
||||
@@ -153,6 +168,15 @@
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
if (!await Handler.IsAuthenticatedAsync(_config?.ViewPolicy)) {
|
||||
Navigator.NavigateTo("/admin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
_hasUpdatePolicy = await Handler.IsAuthenticatedAsync(_config?.UpdatePolicy);
|
||||
_hasDeletePolicy = await Handler.IsAuthenticatedAsync(_config?.DeletePolicy);
|
||||
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
||||
|
||||
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
||||
_currentlyDisplayedModels = await _manager!.LoadPage(_currentPage, PerPage).ToArrayAsync();
|
||||
_totalPages = await _manager.TotalPages(PerPage);
|
||||
@@ -201,6 +225,11 @@
|
||||
}
|
||||
|
||||
private async Task DeleteEntry(object element) {
|
||||
if (!await Handler.IsAuthenticatedAsync(_config?.DeletePolicy)) {
|
||||
Navigator.NavigateTo("/admin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = await Dialogs.ShowConfirmationAsync("Do you really want to delete this entry?");
|
||||
var result = await dialog.Result;
|
||||
if (result.Cancelled) return;
|
||||
@@ -210,6 +239,11 @@
|
||||
}
|
||||
|
||||
private async Task CreateOrEdit(object? element) {
|
||||
if (!await Handler.IsAuthenticatedAsync(element is null ? _config?.CreatePolicy : _config?.UpdatePolicy)) {
|
||||
Navigator.NavigateTo("/admin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
var panel = await Dialogs.ShowPanelAsync<HopFrameEditor>(new EditorDialogData(_config!, element), new DialogParameters {
|
||||
TrapFocus = false
|
||||
});
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
using HopFrame.Core;
|
||||
using HopFrame.Core.Config;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
public static class ServiceCollectionExtensions {
|
||||
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator) {
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator, LibraryConfiguration? fluentUiLibraryConfiguration = null) {
|
||||
var config = new HopFrameConfig();
|
||||
configurator.Invoke(new HopFrameConfigurator(config));
|
||||
return AddHopFrame(services, config);
|
||||
return AddHopFrame(services, config, fluentUiLibraryConfiguration);
|
||||
}
|
||||
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, HopFrameConfig config) {
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, HopFrameConfig config, LibraryConfiguration? fluentUiLibraryConfiguration = null) {
|
||||
services.AddSingleton(config);
|
||||
services.AddHopFrameServices();
|
||||
services.AddFluentUIComponents(fluentUiLibraryConfiguration);
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user