@rendermode InteractiveServer @implements IDialogContentComponent @implements IDisposable @using System.Collections @using HopFrame.Core.Config @using HopFrame.Core.Services @using HopFrame.Web.Models @using HopFrame.Web.Helpers @using HopFrame.Web.Plugins @using HopFrame.Web.Plugins.Events @foreach (var property in GetEditorProperties()) { 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.Name]) { @error } } @inject IContextExplorer Explorer @inject IDialogService Dialogs @inject IHopFrameAuthHandler Handler @inject IToastService Toasts @inject IServiceProvider Provider @inject IPluginOrchestrator PluginOrchestrator @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(); private readonly List _changes = new(); private readonly CancellationTokenSource _tokenSource = 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) { _validationErrors.Add(property.Name, []); } } private IEnumerable GetEditorProperties() { return Content.Config.Properties .Where(prop => prop is not VirtualPropertyConfig { VirtualParser: null }) .OrderBy(prop => prop.Order); } 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, null, listItem).Result; } var value = GetNewestValue(config); 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, value).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; var newItems = ((IEnumerable)value).OfType(); var collection = Activator.CreateInstance(config.Info.PropertyType); var addMethod = config.Info.PropertyType.GetMethod(nameof(ICollection.Add)); if (addMethod is null) throw new ArgumentException($"Cannot modify property '{config.Name}' on table '{config.Table}' because no 'Add' method is implemented"); foreach (var item in newItems) { addMethod.Invoke(collection, [item]); } _changes.Add(new PropertyChange(config.Info, collection)); } 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) _changes.Add(new PropertyChange(config.Info, result)); } private void ApplyChanges(object entry) { foreach (var prop in Content.Config.Properties) { var newValue = GetNewestValue(prop); prop.SetValue(entry, newValue, Provider); } } private object? GetNewestValue(PropertyConfig config) { var value = config.GetValue(Content.CurrentObject, Provider); var change = _changes.LastOrDefault(c => c.Property == config.Info); if (change is not null) value = change.Value; return value; } 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 = relationType.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 = GetNewestValue(config); 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.IsVirtualProperty) continue; var errorList = _validationErrors[property.Name]; errorList.Clear(); var value = GetNewestValue(property); 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"); var eventResult = await PluginOrchestrator.DispatchEvent(new ValidationEvent(this) { Errors = errorList, Property = property, Table = Content.Config }, _tokenSource.Token); if (eventResult.IsCanceled) return false; } 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; if (result.Cancelled) return false; ApplyChanges(Content.CurrentObject!); return true; } public void Dispose() { _tokenSource.Dispose(); } public enum InputType { Number, Switch, Date, Time, Enum, Text, Relation } }