Added property validation

This commit is contained in:
2025-01-16 16:16:50 +01:00
parent c3c69466d4
commit d8596aa5e1
5 changed files with 101 additions and 24 deletions

View File

@@ -9,14 +9,11 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="Added relation picker dialog"> <list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="Added automatic relation mapping">
<change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/IContextExplorer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/IContextExplorer.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameRelationPicker.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameRelationPicker.razor" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" afterDir="false" />
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
@@ -151,7 +148,7 @@
<workItem from="1736875984621" duration="8464000" /> <workItem from="1736875984621" duration="8464000" />
<workItem from="1736884461354" duration="1075000" /> <workItem from="1736884461354" duration="1075000" />
<workItem from="1736962119221" duration="8119000" /> <workItem from="1736962119221" duration="8119000" />
<workItem from="1737021098746" duration="16566000" /> <workItem from="1737021098746" duration="19408000" />
</task> </task>
<task id="LOCAL-00001" summary="Added basic configuration"> <task id="LOCAL-00001" summary="Added basic configuration">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -209,7 +206,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1737035288104</updated> <updated>1737035288104</updated>
</task> </task>
<option name="localTasksCounter" value="8" /> <task id="LOCAL-00008" summary="Added automatic relation mapping">
<option name="closed" value="true" />
<created>1737037853482</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1737037853482</updated>
</task>
<option name="localTasksCounter" value="9" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -226,6 +231,7 @@
<MESSAGE value="Added entry saving support" /> <MESSAGE value="Added entry saving support" />
<MESSAGE value="Added reload button and animation" /> <MESSAGE value="Added reload button and animation" />
<MESSAGE value="Added relation picker dialog" /> <MESSAGE value="Added relation picker dialog" />
<option name="LAST_COMMIT_MESSAGE" value="Added relation picker dialog" /> <MESSAGE value="Added automatic relation mapping" />
<option name="LAST_COMMIT_MESSAGE" value="Added automatic relation mapping" />
</component> </component>
</project> </project>

View File

@@ -14,11 +14,12 @@ public class PropertyConfig(PropertyInfo info, TableConfig table) {
public Func<object, string>? Formatter { get; set; } public Func<object, string>? Formatter { get; set; }
public Func<string, object>? Parser { get; set; } public Func<string, object>? Parser { get; set; }
public Func<object>? Template { get; set; } public Func<object>? Template { get; set; }
public Func<object?, Task<IEnumerable<string>>>? Validator { get; set; }
public bool Editable { get; set; } = true; public bool Editable { get; set; } = true;
public bool Creatable { get; set; } = true; public bool Creatable { get; set; } = true;
public bool DisplayValue { get; set; } = true; public bool DisplayValue { get; set; } = true;
public bool IsRelation { get; set; } public bool IsRelation { get; set; }
public bool IsPrimaryKey { get; set; } public bool IsRequired { get; set; }
} }
public class PropertyConfig<TProp>(PropertyConfig config) { public class PropertyConfig<TProp>(PropertyConfig config) {
@@ -79,4 +80,14 @@ public class PropertyConfig<TProp>(PropertyConfig config) {
InnerConfig.DisplayValue = display; InnerConfig.DisplayValue = display;
return this; return this;
} }
public PropertyConfig<TProp> Validator(Func<TProp?, IEnumerable<string>> validator) {
InnerConfig.Validator = obj => Task.FromResult(validator.Invoke((TProp?)obj));
return this;
}
public PropertyConfig<TProp> Validator(Func<TProp?, Task<IEnumerable<string>>> validator) {
InnerConfig.Validator = obj => validator.Invoke((TProp?)obj);
return this;
}
} }

View File

@@ -65,6 +65,13 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
propConfig.IsRelation = true; propConfig.IsRelation = true;
} }
foreach (var property in entity.GetProperties()) {
var propConfig = table.Properties
.SingleOrDefault(prop => prop.Info == property.PropertyInfo);
if (propConfig is null) continue;
propConfig.IsRequired = !property.IsNullable;
}
logger.LogInformation("Extracted information for table '" + table.PropertyName + "'"); logger.LogInformation("Extracted information for table '" + table.PropertyName + "'");
table.Seeded = true; table.Seeded = true;
} }

View File

@@ -18,16 +18,16 @@
<FluentTextField <FluentTextField
Label="@property.Name" Label="@property.Name"
Value="@(GetPropertyValue<string>(property))" Value="@(GetPropertyValue<string>(property))"
Disabled="@(!property.Editable)" Required="@property.IsRequired"
ReadOnly="true" ReadOnly="true"
Style="width: 100%" Style="width: 100%"
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Text))" /> ValueChanged="@(v => SetPropertyValue(property, v, InputType.Text))" />
</div> </div>
<div style="display: flex; gap: 5px; margin-bottom: 4px"> <div style="display: flex; gap: 5px; margin-bottom: 4px">
<FluentButton OnClick="() => SetPropertyValue(property, null, InputType.Relation)"> <FluentButton OnClick="() => SetPropertyValue(property, null, InputType.Relation)" Disabled="@(!property.Editable)">
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" /> <FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
</FluentButton> </FluentButton>
<FluentButton OnClick="async () => await OpenRelationalPicker(property)"> <FluentButton OnClick="async () => await OpenRelationalPicker(property)" Disabled="@(!property.Editable)">
<FluentIcon Value="@(new Icons.Regular.Size20.Open())" Color="Color.Neutral" /> <FluentIcon Value="@(new Icons.Regular.Size20.Open())" Color="Color.Neutral" />
</FluentButton> </FluentButton>
</div> </div>
@@ -40,6 +40,7 @@
Value="GetPropertyValue<double>(property)" Value="GetPropertyValue<double>(property)"
Style="width: 100%;" Style="width: 100%;"
Disabled="@(!property.Editable)" Disabled="@(!property.Editable)"
Required="@property.IsRequired"
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Number))" /> ValueChanged="@(v => SetPropertyValue(property, v, InputType.Number))" />
} }
else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.Boolean) { else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.Boolean) {
@@ -47,6 +48,7 @@
Label="@property.Name" Label="@property.Name"
Value="GetPropertyValue<bool>(property)" Value="GetPropertyValue<bool>(property)"
Disabled="@(!property.Editable)" Disabled="@(!property.Editable)"
Required="@property.IsRequired"
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Switch))" /> ValueChanged="@(v => SetPropertyValue(property, v, InputType.Switch))" />
} }
else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.DateTime) { else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.DateTime) {
@@ -56,12 +58,14 @@
Label="@property.Name" Label="@property.Name"
Value="GetPropertyValue<DateTime>(property)" Value="GetPropertyValue<DateTime>(property)"
Disabled="@(!property.Editable)" Disabled="@(!property.Editable)"
Required="@property.IsRequired"
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Date))" /> ValueChanged="@(v => SetPropertyValue(property, v, InputType.Date))" />
</div> </div>
<div style="display: flex; flex-direction: column; justify-content: flex-end"> <div style="display: flex; flex-direction: column; justify-content: flex-end">
<FluentTimePicker <FluentTimePicker
Value="GetPropertyValue<DateTime>(property)" Value="GetPropertyValue<DateTime>(property)"
Disabled="@(!property.Editable)" Disabled="@(!property.Editable)"
Required="@property.IsRequired"
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Time))" /> ValueChanged="@(v => SetPropertyValue(property, v, InputType.Time))" />
</div> </div>
</div> </div>
@@ -72,6 +76,7 @@
Value="GetPropertyValue<DateOnly>(property).ToDateTime(TimeOnly.MinValue)" Value="GetPropertyValue<DateOnly>(property).ToDateTime(TimeOnly.MinValue)"
Style="width: 100%" Style="width: 100%"
Disabled="@(!property.Editable)" Disabled="@(!property.Editable)"
Required="@property.IsRequired"
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Date))" /> ValueChanged="@(v => SetPropertyValue(property, v, InputType.Date))" />
} }
else if (property.Info.PropertyType == typeof(TimeOnly)) { else if (property.Info.PropertyType == typeof(TimeOnly)) {
@@ -80,6 +85,7 @@
Value="GetPropertyValue<TimeOnly>(property).ToDateTime()" Value="GetPropertyValue<TimeOnly>(property).ToDateTime()"
Style="width: 100%" Style="width: 100%"
Disabled="@(!property.Editable)" Disabled="@(!property.Editable)"
Required="@property.IsRequired"
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Time))" /> ValueChanged="@(v => SetPropertyValue(property, v, InputType.Time))" />
} }
else if (property.Info.PropertyType.IsEnum) { else if (property.Info.PropertyType.IsEnum) {
@@ -91,6 +97,7 @@
Style="width: 100%" Style="width: 100%"
Height="250px" Height="250px"
Disabled="@(!property.Editable)" Disabled="@(!property.Editable)"
Required="@property.IsRequired"
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Enum))" /> ValueChanged="@(v => SetPropertyValue(property, v, InputType.Enum))" />
} }
else { else {
@@ -99,8 +106,13 @@
Value="@(GetPropertyValue<string>(property))" Value="@(GetPropertyValue<string>(property))"
Style="width: 100%;" Style="width: 100%;"
Disabled="@(!property.Editable)" Disabled="@(!property.Editable)"
Required="@property.IsRequired"
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Text))" /> ValueChanged="@(v => SetPropertyValue(property, v, InputType.Text))" />
} }
@foreach (var error in _validationErrors[property.Info.Name]) {
<FluentLabel Color="@Color.Error">@error</FluentLabel>
}
</div> </div>
} }
</FluentDialogBody> </FluentDialogBody>
@@ -117,14 +129,20 @@
private bool _currentlyEditing; private bool _currentlyEditing;
private ITableManager? _manager; private ITableManager? _manager;
private Dictionary<string, List<string>> _validationErrors = new();
protected override void OnInitialized() { protected override void OnInitialized() {
_currentlyEditing = Content.CurrentObject is not null; _currentlyEditing = Content.CurrentObject is not null;
Dialog.Instance.Parameters.Title = (_currentlyEditing ? "Edit " : "Add ") + Content.Config.TableType.Name; Dialog.Instance.Parameters.Title = (_currentlyEditing ? "Edit " : "Add ") + Content.Config.TableType.Name;
Dialog.Instance.Parameters.Width = "500px"; Dialog.Instance.Parameters.Width = "500px";
Dialog.Instance.Parameters.PrimaryAction = "Save"; Dialog.Instance.Parameters.PrimaryAction = "Save";
Dialog.Instance.Parameters.ValidateDialogAsync = ValidateInputs;
_manager = Explorer.GetTableManager(Content.Config.PropertyName); _manager = Explorer.GetTableManager(Content.Config.PropertyName);
Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType); Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType);
foreach (var property in Content.Config.Properties) {
_validationErrors.Add(property.Info.Name, []);
}
} }
private TValue? GetPropertyValue<TValue>(PropertyConfig config) { private TValue? GetPropertyValue<TValue>(PropertyConfig config) {
@@ -133,7 +151,7 @@
var value = config.Info.GetValue(Content.CurrentObject); var value = config.Info.GetValue(Content.CurrentObject);
var newlyGenerated = false; var newlyGenerated = false;
if (config.Info.PropertyType.IsDefaultValue(value) && config.Template is not null) { if (config.Info.PropertyType.IsDefaultValue(value) && config.Template is not null && !_currentlyEditing) {
value = config.Template.Invoke(); value = config.Template.Invoke();
newlyGenerated = true; newlyGenerated = true;
} }
@@ -177,20 +195,20 @@
case InputType.Date: case InputType.Date:
if (config.Info.PropertyType == typeof(DateTime)) { if (config.Info.PropertyType == typeof(DateTime)) {
var newDate = (DateOnly)value; var newDate = (DateTime)value;
var dateTime = GetPropertyValue<DateTime>(config); var dateTime = GetPropertyValue<DateTime>(config);
result = new DateTime(newDate.Year, newDate.Month, newDate.Day, dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, dateTime.Microsecond); result = new DateTime(newDate.Year, newDate.Month, newDate.Day, dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, dateTime.Microsecond);
} }
else result = (DateOnly)value; else result = DateOnly.FromDateTime((DateTime)value);
break; break;
case InputType.Time: case InputType.Time:
if (config.Info.PropertyType == typeof(DateTime)) { if (config.Info.PropertyType == typeof(DateTime)) {
var newTime = (TimeOnly)value; var newTime = (DateTime)value;
var dateTime = GetPropertyValue<DateTime>(config); var dateTime = GetPropertyValue<DateTime>(config);
result = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, newTime.Hour, newTime.Minute, newTime.Second, newTime.Millisecond, newTime.Microsecond); result = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, newTime.Hour, newTime.Minute, newTime.Second, newTime.Millisecond, newTime.Microsecond);
} }
else result = (TimeOnly)value; else result = TimeOnly.FromDateTime((DateTime)value);
break; break;
case InputType.Relation: case InputType.Relation:
@@ -222,6 +240,27 @@
SetPropertyValue(config, data.Object, InputType.Relation); SetPropertyValue(config, data.Object, InputType.Relation);
} }
private async Task<bool> ValidateInputs() {
foreach (var property in Content.Config.Properties) {
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));
continue;
}
if (value is null && property.IsRequired)
errorList.Add("Value cannot be null");
}
StateHasChanged();
return _validationErrors
.Select(err => err.Value.Count)
.All(c => c == 0);
}
private enum InputType { private enum InputType {
Number, Number,
Switch, Switch,

View File

@@ -49,6 +49,20 @@ builder.Services.AddHopFrame(options => {
context.Table<Post>() context.Table<Post>()
.Property(p => p.CreatedAt) .Property(p => p.CreatedAt)
.ValueTemplate(() => DateTime.UtcNow); .ValueTemplate(() => DateTime.UtcNow);
context.Table<Post>()
.Property(p => p.Caption)
.Validator(input => {
var errors = new List<string>();
if (input is null)
errors.Add("Value cannot be null");
if (input?.Length > 10)
errors.Add("Value can only be 10 characters long");
return errors;
});
}); });
}); });