Added n -> m relation support
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
@implements IDialogContentComponent<EditorDialogData>
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using System.Collections
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
@using HopFrame.Web.Models
|
||||
@using HopFrame.Web.Helpers
|
||||
@using Microsoft.EntityFrameworkCore.Internal
|
||||
|
||||
<FluentDialogBody>
|
||||
@foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) {
|
||||
@@ -15,18 +15,31 @@
|
||||
@if (property.IsRelation) {
|
||||
<div style="display: flex; gap: 5px; align-items: flex-end">
|
||||
<div style="flex-grow: 1">
|
||||
<FluentTextField
|
||||
Label="@property.Name"
|
||||
Value="@(GetPropertyValue<string>(property))"
|
||||
Required="@property.IsRequired"
|
||||
ReadOnly="true"
|
||||
Style="width: 100%"
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
|
||||
@if (property.IsEnumerable) {
|
||||
<FluentLabel Style="margin-bottom: calc(var(--design-unit) * 1px)">@property.Name</FluentLabel>
|
||||
<div class="hopframe-listview">
|
||||
<FluentOverflow Style="width: 100%">
|
||||
@foreach (var item in GetPropertyValue<IEnumerable>(property) ?? Enumerable.Empty<object>()) {
|
||||
<FluentOverflowItem><FluentBadge>@(GetPropertyValue<string>(property, item))</FluentBadge></FluentOverflowItem>
|
||||
}
|
||||
</FluentOverflow>
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
<FluentTextField
|
||||
Label="@property.Name"
|
||||
Value="@(GetPropertyValue<string>(property))"
|
||||
Required="@property.IsRequired"
|
||||
ReadOnly="true"
|
||||
Style="width: 100%" />
|
||||
}
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px; margin-bottom: 4px">
|
||||
<FluentButton OnClick="async () => await SetPropertyValue(property, null, InputType.Relation)" Disabled="@(!property.Editable)">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
||||
</FluentButton>
|
||||
@if (!property.IsRequired) {
|
||||
<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)">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size20.Open())" Color="Color.Neutral" />
|
||||
</FluentButton>
|
||||
@@ -168,19 +181,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
private TValue? GetPropertyValue<TValue>(PropertyConfig config) {
|
||||
private TValue? GetPropertyValue<TValue>(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);
|
||||
}
|
||||
|
||||
var value = config.Info.GetValue(Content.CurrentObject);
|
||||
|
||||
if (value is null)
|
||||
return default;
|
||||
|
||||
if (config.Info.PropertyType == typeof(TValue))
|
||||
if (typeof(TValue).IsAssignableFrom(config.Info.PropertyType))
|
||||
return (TValue)value;
|
||||
|
||||
if (typeof(TValue) == typeof(string))
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, Content.Config);
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config);
|
||||
|
||||
return (TValue)Convert.ChangeType(value, typeof(TValue));
|
||||
}
|
||||
@@ -190,6 +208,7 @@
|
||||
return;
|
||||
|
||||
object? result = null;
|
||||
var needsOverride = true;
|
||||
|
||||
if (value is not null && config.Parser is null) {
|
||||
switch (senderType) {
|
||||
@@ -231,7 +250,16 @@
|
||||
break;
|
||||
|
||||
case InputType.Relation:
|
||||
result = value;
|
||||
if (!config.IsEnumerable)
|
||||
result = ((IEnumerable)value).OfType<object>().FirstOrDefault();
|
||||
else {
|
||||
needsOverride = false;
|
||||
var asList = (IList)config.Info.GetValue(Content.CurrentObject)!;
|
||||
asList.Clear();
|
||||
foreach (var element in (IEnumerable)value) {
|
||||
asList.Add(element);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -243,23 +271,40 @@
|
||||
result = config.Parser(result.ToString()!);
|
||||
}
|
||||
|
||||
config.Info.SetValue(Content.CurrentObject, result);
|
||||
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(config.Info.PropertyType);
|
||||
var relationTable = Explorer.GetTable(relationType);
|
||||
if (relationTable is null) return;
|
||||
|
||||
var currentValue = config.Info.GetValue(Content.CurrentObject);
|
||||
var dialog = await Dialogs.ShowDialogAsync<HopFrameRelationPicker>(new RelationPickerDialogData(relationTable, currentValue), new DialogParameters());
|
||||
var currentValues = new List<object>();
|
||||
if (config.IsEnumerable) {
|
||||
foreach (var o in GetPropertyValue<IEnumerable>(config) ?? Enumerable.Empty<object>()) {
|
||||
currentValues.Add(o);
|
||||
}
|
||||
}
|
||||
else {
|
||||
var raw = config.Info.GetValue(Content.CurrentObject);
|
||||
if (raw is not null)
|
||||
currentValues.Add(raw);
|
||||
}
|
||||
|
||||
var dialog = await Dialogs.ShowDialogAsync<HopFrameRelationPicker>(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.Object, InputType.Relation);
|
||||
await SetPropertyValue(config, data.SelectedObjects, InputType.Relation);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateInputs() {
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
@using HopFrame.Web.Components.Pages
|
||||
|
||||
<FluentDialogBody Style="overflow-x: auto">
|
||||
<HopFrameTablePage DisplayActions="false" DisplaySelection="true" TableDisplayName="@Content.SourceTable.DisplayName" PerPage="15" DialogData="Content" />
|
||||
<HopFrameTablePage
|
||||
DisplayActions="false"
|
||||
DisplaySelection="true"
|
||||
TableDisplayName="@Content.SourceTable.DisplayName"
|
||||
PerPage="15"
|
||||
DialogData="Content"
|
||||
SelectionMode="@(Content.AllowMultiple ? DataGridSelectMode.Multiple : DataGridSelectMode.Single)"/>
|
||||
</FluentDialogBody>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
|
||||
<FluentAppBar Orientation="Orientation.Vertical" Style="background-color: var(--neutral-layer-2); height: auto">
|
||||
<FluentAppBar Orientation="Orientation.Vertical" PopoverShowSearch="false" Style="background-color: var(--neutral-layer-2); height: auto">
|
||||
<FluentAppBarItem Href="/admin"
|
||||
Match="NavLinkMatch.All"
|
||||
IconActive="new Icons.Filled.Size24.Grid()"
|
||||
|
||||
@@ -31,31 +31,29 @@
|
||||
<FluentSpacer />
|
||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
||||
|
||||
@if (!DisplaySelection && _hasCreatePolicy) {
|
||||
@if (_hasCreatePolicy && DisplayActions) {
|
||||
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entry</FluentButton>
|
||||
}
|
||||
</FluentToolbar>
|
||||
<FluentProgress Visible="_loading" Width="100%" />
|
||||
|
||||
<div style="display: flex; overflow-y: auto; flex-grow: 1">
|
||||
@if (DisplaySelection) {
|
||||
<div style="margin-top: calc(44px - (var(--stroke-width) * 1px)); border-top: calc(var(--stroke-width)* 1px) solid var(--neutral-stroke-divider-rest);">
|
||||
@foreach (var model in _currentlyDisplayedModels) {
|
||||
<div class="hopframe-radio">
|
||||
<FluentRadioGroup TValue="int" Value="@(DialogData!.Object == model ? 1 : 0)">
|
||||
<FluentRadio Value="0" Style="display: none" />
|
||||
<FluentRadio Value="1" @onclick="() => DialogData!.Object = model" Style="width: 20px"/>
|
||||
</FluentRadioGroup>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style="flex-grow: 1">
|
||||
<FluentDataGrid Items="_currentlyDisplayedModels.AsQueryable()">
|
||||
@if (DisplaySelection) {
|
||||
<SelectColumn
|
||||
TGridItem="object"
|
||||
SelectMode="SelectionMode"
|
||||
SelectFromEntireRow="true"
|
||||
SelectedItems="DialogData?.SelectedObjects.ToArray()"
|
||||
OnSelect="data => SelectItem(data.Item, data.Selected)"
|
||||
SelectAllChanged="SelectAll"
|
||||
Style="min-width: max-content; height: 44px; display: grid; align-items: center" @ref="_selectColumn" />
|
||||
}
|
||||
|
||||
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
|
||||
<PropertyColumn
|
||||
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property, _config)"
|
||||
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property, null)"
|
||||
Style="min-width: max-content; height: 44px;"
|
||||
Sortable="@property.Sortable"/>
|
||||
}
|
||||
@@ -65,16 +63,16 @@
|
||||
|
||||
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content">
|
||||
@{ var currentElement = _currentlyDisplayedModels.ElementAtOrDefault(dataIndex); }
|
||||
|
||||
|
||||
@if (_hasUpdatePolicy) {
|
||||
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(currentElement); }">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())" />
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
@if (_hasDeletePolicy) {
|
||||
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(currentElement!); }">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning" />
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
@@ -143,13 +141,16 @@
|
||||
[Parameter]
|
||||
public RelationPickerDialogData? DialogData { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public DataGridSelectMode SelectionMode { get; set; } = DataGridSelectMode.Single;
|
||||
|
||||
[Parameter]
|
||||
public int PerPage { get; set; } = 20;
|
||||
|
||||
private TableConfig? _config;
|
||||
private ITableManager? _manager;
|
||||
|
||||
private IEnumerable<object> _currentlyDisplayedModels = [];
|
||||
private object[] _currentlyDisplayedModels = [];
|
||||
private int _currentPage;
|
||||
private int _totalPages;
|
||||
private string? _searchTerm;
|
||||
@@ -159,6 +160,8 @@
|
||||
private bool _hasDeletePolicy;
|
||||
private bool _hasCreatePolicy;
|
||||
|
||||
private SelectColumn<object>? _selectColumn;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
_config ??= Explorer.GetTable(TableDisplayName);
|
||||
|
||||
@@ -263,4 +266,19 @@
|
||||
|
||||
await Reload();
|
||||
}
|
||||
|
||||
private void SelectItem(object item, bool selected) {
|
||||
if (!selected)
|
||||
DialogData?.SelectedObjects.Remove(item);
|
||||
else DialogData?.SelectedObjects.Add(item);
|
||||
}
|
||||
|
||||
private void SelectAll(bool? selected) {
|
||||
selected = _currentlyDisplayedModels.Any(obj => DialogData?.SelectedObjects.Contains(obj) != true);
|
||||
foreach (var displayedModel in _currentlyDisplayedModels) {
|
||||
SelectItem(displayedModel, selected == true);
|
||||
}
|
||||
|
||||
_selectColumn!.SelectAll = selected;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace HopFrame.Web.Models;
|
||||
|
||||
public sealed class RelationPickerDialogData(TableConfig sourceTable, object? current) {
|
||||
public object? Object { get; set; } = current;
|
||||
public sealed class RelationPickerDialogData(TableConfig sourceTable, List<object> current, bool multiple) {
|
||||
public List<object> SelectedObjects { get; set; } = current;
|
||||
public TableConfig SourceTable { get; init; } = sourceTable;
|
||||
public bool AllowMultiple { get; set; } = multiple;
|
||||
}
|
||||
@@ -21,6 +21,18 @@
|
||||
padding: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.hopframe-listview {
|
||||
background: padding-box linear-gradient(var(--neutral-fill-input-rest), var(--neutral-fill-input-rest)), border-box var(--neutral-stroke-input-rest);
|
||||
border: calc(var(--stroke-width) * 1px) solid transparent;
|
||||
border-radius: calc(var(--control-corner-radius) * 1px);
|
||||
padding: 0 calc(var(--design-unit) * 2px + 1px);
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.hopframe-content .empty-content-row.empty-content-cell {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user