395 lines
No EOL
13 KiB
Text
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;
|
|
}
|
|
} |