LANCommander/LANCommander.Server/UI/Components/DataTable/DataTable.razor
2026-05-29 02:36:47 -05:00

395 lines
No EOL
13 KiB
Text

@using System.Collections.Specialized
@using AntDesign.TableModels
@using LANCommander.Server.Data
@using LANCommander.Server.Models
@using System.IO.Compression
@using System.Linq.Expressions
@using System.Text
@using Microsoft.EntityFrameworkCore
@using Microsoft.AspNetCore.WebUtilities
@using System.Web
@namespace LANCommander.Server.UI.Components
@attribute [CascadingTypeParameter(nameof(TItem))]
@typeparam TItem where TItem : class, IBaseModel
@inherits Table<TItem>
@inject IDbContextFactory<DatabaseContext> DatabaseContextFactory
@inject NavigationManager NavigationManager
@inject DrawerService DrawerService
<CascadingValue Name="DataTable" Value="@this">
@if (Searchable || ColumnPicker || LeftToolbar != null || RightToolbar != null)
{
<Flex Justify="FlexJustify.SpaceBetween" Class="table-toolbar">
<Flex Gap="FlexGap.Small" Justify="FlexJustify.Start">
@if (Searchable)
{
<Search Placeholder="Search" OnChange="SearchChanged" @bind-Value="Search" Style="width: auto;"/>
}
@if (ColumnPicker)
{
<Button Icon="@IconType.Outline.Control" Type="@ButtonType.Text" OnClick="OpenDrawer"/>
}
@LeftToolbar
</Flex>
@if (RightToolbar != null)
{
<Flex Gap="FlexGap.Small" Style="flex-grow: 1" Justify="FlexJustify.End">
@RightToolbar
</Flex>
}
</Flex>
}
<CascadingValue Name="Includes" Value="Includes" IsFixed>
<CascadingValue Value="ColumnVisibility" IsFixed>
<div id="@Id" class="ant-table-visibility-container">
@RenderBase()
</div>
</CascadingValue>
</CascadingValue>
</CascadingValue>
<style>
@foreach (var columnVisibility in ColumnVisibility) {
if (!columnVisibility.Value)
{
@($"#{Id} tbody td:nth-child({columnVisibility.Key + 1}) {{display: none;}}")
@($"#{Id} thead th:nth-child({columnVisibility.Key + 1}) {{display: none;}}")
}
}
</style>
@code {
[Parameter] public RenderFragment? LeftToolbar { get; set; }
[Parameter] public RenderFragment? RightToolbar { get; set; }
[Parameter] public RenderFragment<TItem> Columns { get; set; }
[Parameter] public RenderFragment Actions { get; set; }
[Parameter] public bool Searchable { get; set; }
[Parameter] public bool ColumnPicker { get; set; }
[Parameter] public Expression<Func<TItem, string>> SearchProperty { get; set; }
[Parameter] public Expression<Func<TItem, bool>> Query { get; set; }
[Parameter] public Expression<Func<TItem, TItem, bool>> Children { get; set; }
string Search { get; set; } = "";
string Url;
bool DrawerOpen = false;
internal Dictionary<int, bool> ColumnVisibility = new();
List<string> Includes = new();
Dictionary<Guid, IEnumerable<TItem>> _children = new();
RenderFragment RenderBase() => builder => base.BuildRenderTree(builder);
protected override async Task OnInitializedAsync()
{
NavigationManager.LocationChanged += LocationChanged;
Url = NavigationManager.Uri;
OnChange = EventCallback.Factory.Create<QueryModel<TItem>>(this,
async (queryModel) =>
{
if (queryModel == null)
return;
Loading = true;
await InvokeAsync(StateHasChanged);
await Task.Yield();
using (var context = DatabaseContextFactory.CreateDbContext())
{
var queryable = BuildQueryable(context, queryModel);
Total = await queryable.CountAsync();
if (TotalChanged.HasDelegate)
await TotalChanged.InvokeAsync(Total);
if (HidePagination)
DataSource = await queryable.ToListAsync();
else if (queryModel != null)
DataSource = await queryable.CurrentPagedRecords(queryModel).ToListAsync();
else
DataSource = await queryable.Take(PageSize).ToListAsync();
if (Children != null)
{
_children = new Dictionary<Guid, IEnumerable<TItem>>();
var childrenQueryable = context
.Set<TItem>()
.AsQueryable()
.AsNoTracking()
.AsSplitQuery();
foreach (var include in Includes)
{
childrenQueryable = childrenQueryable.Include(include);
}
foreach (var item in DataSource)
{
var childPredicate = InjectParent(Children, item);
var children = await childrenQueryable.Where(childPredicate).ToListAsync();
if (children.Any())
_children[item.Id] = children;
}
TreeChildren = item =>
{
return _children.ContainsKey(item.Id) ? _children[item.Id] : [];
};
}
await base.OnParametersSetAsync();
base.OnParametersSet();
}
Loading = false;
await InvokeAsync(StateHasChanged);
await Task.Yield();
});
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await OnChange.InvokeAsync();
}
protected override async Task OnParametersSetAsync()
{
if (Columns != null)
ChildContent = Columns;
/*base.OnPageSizeChange = EventCallback.Factory.Create<PaginationEventArgs>(
this,
async (args) =>
{
PageSizeChanged(args);
});
base.OnPageIndexChange = EventCallback.Factory.Create<PaginationEventArgs>(
this,
async (args) =>
{
PageIndexChanged(args);
});*/
await base.OnParametersSetAsync();
}
Expression<Func<TItem, bool>> InjectParent<TItem>(
Expression<Func<TItem, TItem, bool>> expression,
TItem parent)
{
var childParameter = expression.Parameters[1];
var body = new ReplaceParameterVisitor(expression.Parameters[0], Expression.Constant(parent)).Visit(expression.Body);
return Expression.Lambda<Func<TItem, bool>>(body, childParameter);
}
class ReplaceParameterVisitor : ExpressionVisitor
{
private readonly ParameterExpression _parameter;
private readonly Expression _replacement;
public ReplaceParameterVisitor(ParameterExpression parameter, Expression replacement)
{
_parameter = parameter;
_replacement = replacement;
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (node == _parameter)
return _replacement;
return base.VisitParameter(node);
}
}
IQueryable<TItem>? BuildQueryable(DatabaseContext context, QueryModel<TItem> queryModel)
{
var queryable = context
.Set<TItem>()
.AsSplitQuery();
foreach (var include in Includes)
{
queryable = queryable.Include(include);
}
if (Query != null)
{
queryable = queryable.Where(Query);
}
if (!String.IsNullOrWhiteSpace(Search) && SearchProperty != null)
{
var search = Search.StripPunctuation().ToLower().Trim();
var parameter = SearchProperty.Parameters[0];
Expression body = SearchProperty.Body;
// Apply multiple REPLACE(...) calls to strip punctuation
string[] punctuationToStrip = { ".", ",", "!", "?", ";", ":", "'", "\"", "-", "(", ")", "[", "]", "{", "}", "/" };
foreach (var symbol in punctuationToStrip)
{
var replaceMethod = typeof(string).GetMethod(nameof(string.Replace), new[] { typeof(string), typeof(string) });
body = Expression.Call(body, replaceMethod, Expression.Constant(symbol), Expression.Constant(""));
}
// Apply .ToLower()
var toLowerMethod = typeof(string).GetMethod(nameof(string.ToLower), Type.EmptyTypes);
var toLowerExpression = Expression.Call(body, toLowerMethod);
// Apply .Contains(search)
var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) });
var searchExpression = Expression.Constant(search, typeof(string));
var containsExpression = Expression.Call(toLowerExpression, containsMethod, searchExpression);
// Create the final lambda: x => x.Property.ToLower().Contains(search)
var lambda = Expression.Lambda<Func<TItem, bool>>(containsExpression, parameter);
queryable = queryable.Where(lambda);
}
return queryable.ExecuteTableQuery(queryModel);
}
new void PageIndexChanged(PaginationEventArgs args)
{
var uri = NavigationManager.ToAbsoluteUri(Url);
NavigationManager.NavigateTo($"{uri.AbsolutePath}?Page={args.Page}&Size={args.PageSize}{(Search != "" ? "&Search=" + Search : "")}");
}
new void PageSizeChanged(PaginationEventArgs args)
{
var uri = NavigationManager.ToAbsoluteUri(Url);
NavigationManager.NavigateTo($"{uri.AbsolutePath}?Page={args.Page}&Size={args.PageSize}{(Search != "" ? "&Search=" + Search : "")}");
}
async void LocationChanged(object sender, LocationChangedEventArgs e)
{
Url = e.Location;
await LoadTableParameter();
}
async Task LoadTableParameter()
{
await InvokeStateHasChangedAsync();
var uri = NavigationManager.ToAbsoluteUri(Url);
var query = HttpUtility.ParseQueryString(uri.Query);
if (query.AllKeys.Contains("Page"))
PageIndex = int.TryParse(query["Page"], out var index) ? index > 0 ? index : 1 : 1;
if (query.AllKeys.Contains("Size"))
PageSize = int.TryParse(query["Size"], out var size) ? size > 0 ? size : 50 : 50;
if (query["Search"] != null && query["Search"] != Search)
{
Search = query["Search"];
await SearchChanged();
}
await InvokeStateHasChangedAsync();
}
async Task SearchChanged()
{
var uri = NavigationManager.ToAbsoluteUri(Url);
Dictionary<string, string> parameters = new();
PageIndex = 1;
if (PageSize > 1)
parameters.Add("PageSize", PageSize.ToString());
if (!String.IsNullOrWhiteSpace(Search))
parameters.Add("Search", Search);
// NavigationManager.NavigateTo(QueryHelpers.AddQueryString(uri.AbsolutePath, parameters));
await ReloadAsync();
}
public IEnumerable<string> GetIncludes()
{
return Includes;
}
public async Task OpenDrawer()
{
var options = new DrawerOptions
{
Title = "Columns",
Width = "350",
MaskClosable = false,
Placement = DrawerPlacement.Right,
};
var columns = new List<DataTableColumn>();
foreach (var column in ColumnContext.Columns)
{
if (column is not ISelectionColumn && column is not ActionColumn)
{
columns.Add(new DataTableColumn
{
Index = column.ColIndex,
Name = column.Title,
Visible = ColumnVisibility.ContainsKey(column.ColIndex) ? ColumnVisibility[column.ColIndex] : false
});
}
}
columns = (await DrawerService.CreateDialogAsync<DataTableColumnPickerDrawer, IEnumerable<DataTableColumn>, IEnumerable<DataTableColumn>>(options, columns)).ToList();
foreach (var column in columns)
{
ColumnVisibility[column.Index] = column.Visible;
}
ReloadData(PageIndex, PageSize);
InvokeStateHasChanged();
}
public void CloseDrawer()
{
InvokeStateHasChanged();
StateHasChanged();
}
public async Task ReloadAsync()
{
Loading = true;
await InvokeStateHasChangedAsync();
await Task.Yield();
ReloadData(PageIndex, PageSize);
}
public void Dispose()
{
NavigationManager.LocationChanged -= LocationChanged;
}
}