!2791 test(#I581BH): add unit test for table toolbar

* test: 增加 扩展行内按钮 Callback 单元测试
* test: 增加数据导出单元测试
* feat: 增加导出数据相关资源文件
* test: 增加 确认删除 单元测试
* test: 增加删除确认按钮单元测试
* refactor: 移除 Items 双向绑定
* chore: 删除不使用的图片
* style: 微调 logo 大小
* doc: 更新 band logo
* style: 微调后台模板样式
* style: 微调分页组件高度
This commit is contained in:
Argo 2022-05-18 05:41:29 +00:00
parent ed51435e4d
commit 28407c9128
15 changed files with 230 additions and 103 deletions

View File

@ -61,7 +61,7 @@
</Header>
<Side>
<div class="layout-banner">
<img class="layout-logo" src="_content/BootstrapBlazor.Shared/images/brand.png" />
<img class="layout-logo" src="_content/BootstrapBlazor.Shared/images/logo.png" />
<div class="layout-title">
<span>后台管理</span>
</div>

View File

@ -14,7 +14,7 @@
</Header>
<Side>
<div class="layout-banner">
<img class="layout-logo" src="_content/BootstrapBlazor.Shared/images/brand.png" />
<img class="layout-logo" src="_content/BootstrapBlazor.Shared/images/logo.png" />
<div class="layout-title">
<span>后台管理</span>
</div>

View File

@ -5,6 +5,7 @@
--bb-violet-rgb: 112.520718,44.062154,249.437846;
--bb-bg-violet: #7a5cff;
--bb-bg-navbar: #8548ff;
--bb-layout-sidebar-banner-bg: #702cf8;
}
header {
@ -460,6 +461,10 @@ section {
flex-flow: column;
}
.layout-logo {
border: solid 1px #fff;
}
.layout-footer .footer {
background: #7dbcea;
color: #fff;
@ -928,7 +933,7 @@ section {
display: flex;
align-items: center;
padding: 0 .625rem;
background-color: #367fa9;
background-color: var(--bb-layout-sidebar-banner-bg);
height: 50px;
font-size: 1.5rem;
}
@ -1309,7 +1314,7 @@ section {
.layout-item .layout-left .layout-left-header {
height: 16px;
background-color: #367fa9;
background-color: var(--bb-layout-sidebar-banner-bg);
}
.layout-item .layout-left .layout-left-body,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -4,6 +4,7 @@
--bb-layout-footer-height: 90px;
--bb-layout-side-width: 214px;
--bb-layout-sidebar-collapse-width: 70px;
--bb-layout-sidebar-banner-bg: #367fa9;
}
.layout {
@ -123,7 +124,7 @@
display: flex;
align-items: center;
padding: 0 0.625rem;
background-color: #367fa9;
background-color: var(--bb-layout-sidebar-banner-bg);
height: var(--bb-layout-header-height);
font-size: 1.5rem;
}
@ -135,7 +136,7 @@
}
.layout.is-page .layout-side .layout-banner .layout-logo {
width: 44px;
width: 42px;
border-radius: var(--bs-border-radius);
}

View File

@ -47,6 +47,7 @@
.page-link .fa {
width: 11px;
height: 11px;
text-align: center;
}

View File

@ -326,5 +326,8 @@ public partial class Table<TItem>
SortDescText ??= Localizer[nameof(SortDescText)];
EmptyText ??= Localizer[nameof(EmptyText)];
LineNoText ??= Localizer[nameof(LineNoText)];
ExportToastTitle ??= Localizer[nameof(ExportToastTitle)];
ExportToastContent ??= Localizer[nameof(ExportToastContent)];
ExportToastInProgressContent ??= Localizer[nameof(ExportToastInProgressContent)];
}
}

View File

@ -174,6 +174,27 @@ public partial class Table<TItem>
[Parameter]
public string? EditDialogCloseButtonText { get; set; }
/// <summary>
/// 获得/设置 导出数据弹窗 Title 默认为资源文件 导出数据
/// </summary>
[Parameter]
[NotNull]
public string? ExportToastTitle { get; set; }
/// <summary>
/// 获得/设置 导出数据提示内容 默认为资源文件
/// </summary>
[Parameter]
[NotNull]
public string? ExportToastContent { get; set; }
/// <summary>
/// 获得/设置 正在导出数据提示内容 默认为资源文件
/// </summary>
[Parameter]
[NotNull]
public string? ExportToastInProgressContent { get; set; }
/// <summary>
/// ToastService 服务实例
/// </summary>
@ -259,16 +280,14 @@ public partial class Table<TItem>
{
ShowAddForm = true;
ShowEditForm = false;
await UpdateAsync();
StateHasChanged();
}
else if (EditMode == EditMode.InCell)
{
AddInCell = true;
EditInCell = true;
SelectedRows.Add(EditModel);
await UpdateAsync();
StateHasChanged();
}
await OnSelectedRowsChanged();
await ToggleLoading(false);
@ -324,15 +343,14 @@ public partial class Table<TItem>
{
ShowEditForm = true;
ShowAddForm = false;
await UpdateAsync();
StateHasChanged();
}
else if (EditMode == EditMode.InCell)
{
AddInCell = false;
EditInCell = true;
await UpdateAsync();
StateHasChanged();
}
await ToggleLoading(false);
}
@ -382,7 +400,7 @@ public partial class Table<TItem>
/// <returns></returns>
protected async Task<bool> SaveModelAsync(EditContext context, ItemChangedType changedType)
{
bool valid= await InternalOnSaveAsync((TItem)context.Model, changedType);
bool valid = await InternalOnSaveAsync((TItem)context.Model, changedType);
// 回调外部自定义方法
if (OnAfterSaveAsync != null)
@ -498,7 +516,7 @@ public partial class Table<TItem>
await ef.CancelAsync();
await ToggleLoading(false);
}
await UpdateAsync();
StateHasChanged();
},
OnEditAsync = async context =>
{
@ -515,18 +533,6 @@ public partial class Table<TItem>
await DialogService.ShowEditDialog(option);
}
private async Task UpdateAsync()
{
if (ItemsChanged.HasDelegate)
{
await ItemsChanged.InvokeAsync(RowItems);
}
else
{
StateHasChanged();
}
}
/// <summary>
/// 确认删除按钮方法
/// </summary>
@ -575,14 +581,14 @@ public partial class Table<TItem>
{
RowItems.RemoveAll(i => SelectedRows.Contains(i));
SelectedRows.Clear();
await UpdateAsync();
StateHasChanged();
}
else
{
await ToggleLoading(true);
var ret = await DelteItemsAsync();
if (ret && ShowToastAfterSaveOrDeleteModel && !IsTracking)
if (ShowToastAfterSaveOrDeleteModel)
{
var option = new ToastOption()
{
@ -590,7 +596,6 @@ public partial class Table<TItem>
Category = ret ? ToastCategory.Success : ToastCategory.Error
};
option.Content = string.Format(DeleteButtonToastResultContent, ret ? SuccessText : FailText, Math.Ceiling(option.Delay / 1000.0));
await Toast.Show(option);
}
await ToggleLoading(false);
@ -601,13 +606,16 @@ public partial class Table<TItem>
var ret = await InternalOnDeleteAsync();
if (ret)
{
// 删除成功 重新查询
// 由于数据删除导致页码会改变,尤其是最后一页
// 重新计算页码
// https://gitee.com/LongbowEnterprise/BootstrapBlazor/issues/I1UJSL
PageIndex = Math.Max(1, Math.Min(PageIndex, int.Parse(Math.Ceiling((TotalCount - SelectedRows.Count) * 1d / PageItems).ToString())));
var items = PageItemsSource.Where(item => item >= (TotalCount - SelectedRows.Count));
PageItems = Math.Min(PageItems, items.Any() ? items.Min() : PageItems);
if (IsPagination)
{
// 删除成功 重新查询
// 由于数据删除导致页码会改变,尤其是最后一页
// 重新计算页码
// https://gitee.com/LongbowEnterprise/BootstrapBlazor/issues/I1UJSL
PageIndex = Math.Max(1, Math.Min(PageIndex, int.Parse(Math.Ceiling((TotalCount - SelectedRows.Count) * 1d / PageItems).ToString())));
var items = PageItemsSource.Where(item => item >= (TotalCount - SelectedRows.Count));
PageItems = Math.Min(PageItems, items.Any() ? items.Min() : PageItems);
}
SelectedRows.Clear();
await QueryAsync();
}
@ -646,69 +654,39 @@ public partial class Table<TItem>
}
}
/// <summary>
/// 确认导出按钮方法
/// </summary>
protected async Task<bool> ConfirmExport()
{
var ret = false;
if (!RowItems.Any())
{
var option = new ToastOption
{
Category = ToastCategory.Information,
Title = "导出数据"
};
option.Content = $"没有需要导出的数据, {Math.Ceiling(option.Delay / 1000.0)} 秒后自动关闭";
await Toast.Show(option);
}
else
{
ret = true;
}
return ret;
}
/// <summary>
/// 导出数据方法
/// </summary>
protected async Task ExportAsync()
{
var ret = false;
_ = Task.Run(async () =>
var option = new ToastOption
{
if (OnExportAsync != null)
{
ret = await OnExportAsync(RowItems);
}
else
{
// 如果未提供 OnExportAsync 回调委托使用注入服务来尝试解析
// TODO: 这里将本页数据作为参数传递给导出服务,服务本身可以利用自身优势获取全部所需数据,如果获取全部数据呢?
ret = await ExcelExport.ExportAsync(RowItems, Columns, JSRuntime);
}
var option = new ToastOption()
{
Title = "导出数据"
};
option.Category = ret ? ToastCategory.Success : ToastCategory.Error;
option.Content = $"导出数据{(ret ? "" : "")}, {Math.Ceiling(option.Delay / 1000.0)} 秒后自动关闭";
await Toast.Show(option);
});
var option = new ToastOption()
{
Title = "导出数据"
Title = ExportToastTitle,
Category = ToastCategory.Information
};
option.Category = ToastCategory.Information;
option.Content = $"正在导出数据,请稍候, {Math.Ceiling(option.Delay / 1000.0)} 秒后自动关闭";
option.Content = string.Format(ExportToastInProgressContent, Math.Ceiling(option.Delay / 1000.0));
await Toast.Show(option);
await Task.CompletedTask;
var ret = false;
if (OnExportAsync != null)
{
ret = await OnExportAsync(RowItems);
}
else
{
// 如果未提供 OnExportAsync 回调委托使用注入服务来尝试解析
// TODO: 这里将本页数据作为参数传递给导出服务,服务本身可以利用自身优势获取全部所需数据,如果获取全部数据呢?
ret = await ExcelExport.ExportAsync(RowItems, Columns, JSRuntime);
}
option = new ToastOption
{
Title = ExportToastTitle,
Category = ret ? ToastCategory.Success : ToastCategory.Error
};
//$"导出数据{(ret ? "成功" : "失败")}, {Math.Ceiling(option.Delay / 1000.0)} 秒后自动关闭";
option.Content = string.Format(ExportToastContent, ret ? SuccessText : FailText, Math.Ceiling(option.Delay / 1000.0));
await Toast.Show(option);
}
/// <summary>

View File

@ -483,12 +483,6 @@ public partial class Table<TItem> : BootstrapComponentBase, IDisposable, ITable
[Parameter]
public IEnumerable<TItem>? Items { get; set; }
/// <summary>
/// 获得/设置 数据集合回调方法
/// </summary>
[Parameter]
public EventCallback<IEnumerable<TItem>> ItemsChanged { get; set; }
/// <summary>
/// 获得/设置 表格组件大小 默认为 Normal 正常模式
/// </summary>

View File

@ -248,7 +248,10 @@
"UnsetText": "Aufsteigend",
"SortAscText": "Absteigend",
"SortDescText": "Keine",
"EmptyText": "Keine Daten"
"EmptyText": "Keine Daten",
"ExportToastTitle": "Daten exportieren",
"ExportToastContent": "Daten exportieren {0}, Automatisches Schließen in {1} Sekunden",
"ExportToastInProgressContent": "Daten exportieren, Automatisches Schließen in {0} Sekunden"
},
"BootstrapBlazor.Components.EditDialog": {
"CloseButtonText": "Schließen",

View File

@ -248,7 +248,10 @@
"UnsetText": "Asc",
"SortAscText": "Desc",
"SortDescText": "None",
"EmptyText": "No Data"
"EmptyText": "No Data",
"ExportToastTitle": "Export",
"ExportToastContent": "Export data {0}, auto close after {1}s",
"ExportToastInProgressContent": "Exporting data, please waiting a moment, auto close after {0}s"
},
"BootstrapBlazor.Components.EditDialog": {
"CloseButtonText": "Close",

View File

@ -248,7 +248,10 @@
"UnsetText": "Asc",
"SortAscText": "Desc",
"SortDescText": "Nenhum",
"EmptyText": "Vazio"
"EmptyText": "Vazio",
"ExportToastTitle": "Exportar dados",
"ExportToastContent": "Exportar dados {0}, Fechamento automático em {0} segundos",
"ExportToastInProgressContent": "Exportar dados, Fechamento automático em {0} segundos"
},
"BootstrapBlazor.Components.EditDialog": {
"CloseButtonText": "Fechar",

View File

@ -248,7 +248,10 @@
"UnsetText": "点击升序",
"SortAscText": "点击降序",
"SortDescText": "取消排序",
"EmptyText": "无数据"
"EmptyText": "无数据",
"ExportToastTitle": "导出数据",
"ExportToastContent": "导出数据 {0}{1} 秒后自动关闭",
"ExportToastInProgressContent": "正在导出数据,请稍后, {0} 秒后自动关闭"
},
"BootstrapBlazor.Components.EditDialog": {
"CloseButtonText": "关闭",

File diff suppressed because one or more lines are too long

View File

@ -3503,7 +3503,6 @@ public class TableTest : TableTestBase
Assert.DoesNotContain("fa fa-remove", table.Find("tbody").ToMarkup());
}
[Fact]
public void ShowExtendEditButton_Ok()
{
@ -3537,6 +3536,14 @@ public class TableTest : TableTestBase
pb.Add(a => a.ShowExtendEditButton, false);
});
Assert.DoesNotContain("fa fa-edit", table.Find("tbody").ToMarkup());
table.SetParametersAndRender(pb =>
{
pb.Add(a => a.ShowExtendEditButton, true);
pb.Add(a => a.ShowDefaultButtons, false);
pb.Add(a => a.ShowEditButtonCallback, foo => true);
});
Assert.Contains("fa fa-edit", table.Find("tbody").ToMarkup());
}
[Fact]
@ -3571,6 +3578,14 @@ public class TableTest : TableTestBase
pb.Add(a => a.ShowExtendDeleteButton, false);
});
Assert.DoesNotContain("fa fa-remove", table.Find("tbody").ToMarkup());
table.SetParametersAndRender(pb =>
{
pb.Add(a => a.ShowExtendDeleteButton, true);
pb.Add(a => a.ShowDefaultButtons, false);
pb.Add(a => a.ShowDeleteButtonCallback, foo => true);
});
Assert.Contains("fa fa-remove", table.Find("tbody").ToMarkup());
}
[Fact]
@ -3658,6 +3673,124 @@ public class TableTest : TableTestBase
}
}
[Fact]
public async Task ConfirmDelete_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var items = Foo.GenerateFoo(localizer, 2);
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<Foo>>(pb =>
{
pb.Add(a => a.RenderMode, TableRenderMode.Table);
pb.Add(a => a.Items, items);
pb.Add(a => a.IsMultipleSelect, true);
pb.Add(a => a.ShowToolbar, true);
pb.Add(a => a.TableColumns, foo => builder =>
{
builder.OpenComponent<TableColumn<Foo, string>>(0);
builder.AddAttribute(1, "Field", "Name");
builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string)));
builder.CloseComponent();
});
});
});
var table = cut.FindComponent<Table<Foo>>();
var deleteButton = table.FindComponent<TableToolbarPopconfirmButton<Foo>>();
await cut.InvokeAsync(() => deleteButton.Instance.OnBeforeClick());
// 选一个
var input = cut.Find("tbody tr input");
await cut.InvokeAsync(() => input.Click());
await cut.InvokeAsync(() => deleteButton.Instance.OnBeforeClick());
table.SetParametersAndRender(pb =>
{
pb.Add(a => a.ShowDeleteButtonCallback, foo => false);
});
await cut.InvokeAsync(() => deleteButton.Instance.OnBeforeClick());
}
[Fact]
public async Task OnConfirm_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<Foo>>(pb =>
{
pb.Add(a => a.RenderMode, TableRenderMode.Table);
pb.Add(a => a.OnQueryAsync, OnQueryAsync(localizer));
pb.Add(a => a.IsMultipleSelect, true);
pb.Add(a => a.ShowToolbar, true);
pb.Add(a => a.IsPagination, true);
pb.Add(a => a.PageItemsSource, new int[] { 1 });
pb.Add(a => a.TableColumns, foo => builder =>
{
builder.OpenComponent<TableColumn<Foo, string>>(0);
builder.AddAttribute(1, "Field", "Name");
builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string)));
builder.CloseComponent();
});
pb.Add(a => a.OnDeleteAsync, foos => Task.FromResult(true));
});
});
var table = cut.FindComponent<Table<Foo>>();
var deleteButton = table.FindComponent<TableToolbarPopconfirmButton<Foo>>();
// 选一个
var input = cut.Find("tbody tr input");
await cut.InvokeAsync(() => input.Click());
await cut.InvokeAsync(() => deleteButton.Instance.OnConfirm());
table.SetParametersAndRender(pb =>
{
pb.Add(a => a.PageItemsSource, new int[] { 1, 2, 4, 8 });
});
await cut.InvokeAsync(() => deleteButton.Instance.OnConfirm());
table.SetParametersAndRender(pb =>
{
pb.Add(a => a.IsTracking, true);
});
await cut.InvokeAsync(() => deleteButton.Instance.OnConfirm());
}
[Fact]
public async Task ExportAsync_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<Foo>>(pb =>
{
pb.Add(a => a.RenderMode, TableRenderMode.Table);
pb.Add(a => a.OnQueryAsync, OnQueryAsync(localizer));
pb.Add(a => a.ShowToolbar, true);
pb.Add(a => a.ShowExportButton, true);
pb.Add(a => a.TableColumns, foo => builder =>
{
builder.OpenComponent<TableColumn<Foo, string>>(0);
builder.AddAttribute(1, "Field", "Name");
builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string)));
builder.CloseComponent();
});
});
});
var button = cut.Find(".dropdown-menu-right .dropdown-item");
await cut.InvokeAsync(() => button.Click());
//
var table = cut.FindComponent<Table<Foo>>();
table.SetParametersAndRender(pb =>
{
pb.Add(a => a.OnExportAsync, foos => Task.FromResult(true));
});
await cut.InvokeAsync(() => button.Click());
}
private static DataTable CreateDataTable(IStringLocalizer<Foo> localizer)
{
var userData = new DataTable();