@implements IDialogContentComponent @rendermode InteractiveServer @using System.Collections @using HopFrame.Core.Config @using HopFrame.Core.Services @using HopFrame.Web.Models @using HopFrame.Web.Helpers @foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) { if (!_currentlyEditing && !property.Creatable) continue;
@if (property.IsRelation) {
@if (property.IsEnumerable) { @property.Name
@foreach (var item in GetPropertyValue(property) ?? Enumerable.Empty()) { @(GetPropertyValue(property, item)) } } else { }
@if (!property.IsRequired) { }
} else if (property.Info.PropertyType.IsNumeric()) { } else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.Boolean) { } 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 if (property.Info.PropertyType.IsNullableEnum()) {
} else if (property.TextArea) { } else { } @foreach (var error in _validationErrors[property.Info.Name]) { @error } } @inject IContextExplorer Explorer @inject IDialogService Dialogs @inject IHopFrameAuthHandler Handler @inject IToastService Toasts @inject IServiceProvider Provider @code { [Parameter] public required EditorDialogData Content { get; set; } [CascadingParameter] public required FluentDialog Dialog { get; set; } private bool _currentlyEditing; private ITableManager? _manager; private readonly Dictionary> _validationErrors = new(); protected override void OnInitialized() { _currentlyEditing = Content.CurrentObject is not null; Dialog.Instance.Parameters.Title = (_currentlyEditing ? "Edit " : "Add ") + Content.Config.TableType.Name; Dialog.Instance.Parameters.Width = "500px"; Dialog.Instance.Parameters.PrimaryAction = "Save"; Dialog.Instance.Parameters.ValidateDialogAsync = ValidateInputs; _manager = Explorer.GetTableManager(Content.Config.PropertyName); Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType); foreach (var property in Content.Config.Properties) { if (property.IsListingProperty) continue; _validationErrors.Add(property.Info.Name, []); } } private TValue? GetPropertyValue(PropertyConfig config, object? listItem = null) { if (!config.DisplayValue) return default; if (Content.CurrentObject is null) return default; if (listItem is not null) { return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, listItem).Result; } var value = config.Info.GetValue(Content.CurrentObject); if (value is null) return default; if (typeof(TValue).IsAssignableFrom(config.Info.PropertyType)) return (TValue)value; if (typeof(TValue) == typeof(string)) return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config).Result; return (TValue)Convert.ChangeType(value, typeof(TValue)); } 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; var needsOverride = true; if (value is not null && config.Parser is null) { switch (senderType) { case InputType.Number: result = Convert.ChangeType(value, config.Info.PropertyType); break; case InputType.Text: if (config.Info.PropertyType == typeof(Guid)) { var success = Guid.TryParse((string)value, out var guid); if (success) result = guid; else Toasts.ShowError($"'{value}' is not a valid guid"); break; } result = Convert.ToString(value); break; case InputType.Switch: result = Convert.ToBoolean(value); break; case InputType.Enum: 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: if (config.Info.PropertyType == typeof(DateTime)) { var newDate = (DateTime)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.FromDateTime((DateTime)value); break; case InputType.Time: if (config.Info.PropertyType == typeof(DateTime)) { var newTime = (DateTime)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.FromDateTime((DateTime)value); break; case InputType.Relation: if (!config.IsEnumerable) result = ((IEnumerable)value).OfType().FirstOrDefault(); else { needsOverride = false; if (!typeof(IList).IsAssignableFrom(config.Info.PropertyType)) { throw new ArgumentException($"Invalid type of '{config.Name}' property in '{config.Table.DisplayName}' table, only list types are supported on enumerable relations."); } var asList = (IList)config.Info.GetValue(Content.CurrentObject)!; asList.Clear(); foreach (var element in (IEnumerable)value) { asList.Add(element); } } break; default: throw new ArgumentOutOfRangeException(nameof(senderType), senderType, null); } } if (config.Parser is not null && result is not null) { result = await config.Parser(result.ToString()!, Provider); } if (needsOverride) config.Info.SetValue(Content.CurrentObject, result); } private async Task OpenRelationalPicker(PropertyConfig config) { if (!await Handler.IsAuthenticatedAsync(_currentlyEditing ? Content.Config.UpdatePolicy : Content.Config.CreatePolicy)) return; var relationType = config.Info.PropertyType; if (config.IsEnumerable) { relationType = config.Info.PropertyType.GetGenericArguments().First(); } var relationTable = Explorer.GetTable(relationType); if (relationTable is null) return; var currentValues = new List(); if (config.IsEnumerable) { foreach (var o in GetPropertyValue(config) ?? Enumerable.Empty()) { currentValues.Add(o); } } else { var raw = config.Info.GetValue(Content.CurrentObject); if (raw is not null) currentValues.Add(raw); } var dialog = await Dialogs.ShowDialogAsync(new RelationPickerDialogData(relationTable, currentValues, config.IsEnumerable), new DialogParameters()); var result = await dialog.Result; if (result.Cancelled) return; var data = (RelationPickerDialogData)result.Data!; await SetPropertyValue(config, data.SelectedObjects, InputType.Relation); } private async Task 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); if (property.Validator is not null) { errorList.AddRange(await property.Validator.Invoke(value, Provider)); continue; } if (value is null && property.IsRequired) errorList.Add($"{property.Name} is required"); } StateHasChanged(); var valid = _validationErrors .Select(err => err.Value.Count) .All(c => c == 0); if (!valid) return false; var dialog = await Dialogs.ShowConfirmationAsync($"Do you really want to {(_currentlyEditing ? "edit" : "create")} this entry?"); var result = await dialog.Result; return !result.Cancelled; } private enum InputType { Number, Switch, Date, Time, Enum, Text, Relation } }