From 28e8900fd44eeb0ff534c0f2f6d97b82cb7aef08 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 3 Jul 2024 14:50:42 +0800 Subject: [PATCH] feat(MultiFilter): add OnGetItemsAsync parameter for performance (#3778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * doc: 更新视频配置 * doc: 更新注释 * refactor: 移除 MultiFilterItem 内部类 * doc: 更新示例 * doc: 增加注释 * feat: 增加懒加载动画 * feat: 增加脚本支持数据懒加载 * refactor: 精简代码 * refactor: 重命名回调方法名称 * feat: 增加 Items 保持选中项逻辑 * doc: 更新示例 * chore: bump version 8.7.1-beta05 * test: 增加单元测试 * test: 增加单元测试 * test: 更新单元测试 * test: 更新单元测试 * test: 增加单元测试 * doc: 更新说明文档 --- .../Samples/Table/TablesFilter.razor | 16 ++- .../Samples/Table/TablesFilter.razor.cs | 10 ++ src/BootstrapBlazor.Server/Locales/en-US.json | 3 + src/BootstrapBlazor.Server/Locales/zh-CN.json | 3 + src/BootstrapBlazor.Server/docs.json | 86 +++++++------ src/BootstrapBlazor/BootstrapBlazor.csproj | 2 +- .../Components/Filters/FilterBase.cs | 8 +- .../Components/Filters/MultiFilter.razor | 18 ++- .../Components/Filters/MultiFilter.razor.cs | 118 +++++++++++++----- .../Components/Filters/MultiFilter.razor.js | 31 +++++ .../Components/Filters/MultiFilter.razor.scss | 13 ++ .../Components/Filters/NumberFilter.razor.cs | 12 +- .../Components/Filters/StringFilter.razor.cs | 8 +- test/UnitTest/Components/TableFilterTest.cs | 68 +++++++++- 14 files changed, 295 insertions(+), 101 deletions(-) create mode 100644 src/BootstrapBlazor/Components/Filters/MultiFilter.razor.js diff --git a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor index 032f69f849d..65fee59e62a 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor @@ -113,7 +113,6 @@ - +
+

@((MarkupString)Localizer["MultiFilterTips"].Value)

+
    +
  • @((MarkupString)Localizer["MultiFilterTipsLi1"].Value)
  • +
  • @((MarkupString)Localizer["MultiFilterTipsLi2"].Value)
  • +
+
- + - + - + - + diff --git a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor.cs index 04cc9351d97..801c7df32b7 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesFilter.razor.cs @@ -28,6 +28,8 @@ public partial class TablesFilter [NotNull] private Table? TableSetFilter { get; set; } + private IEnumerable _nameMultiFilterItems = default!; + /// /// OnInitialized 方法 /// @@ -36,6 +38,14 @@ protected override void OnInitialized() base.OnInitialized(); Items = Foo.GenerateFoo(FooLocalizer); + _nameMultiFilterItems = Items.Select(i => new SelectedItem(i.Name!, i.Name!)).DistinctBy(i => i.Value); + } + + private async Task> OnGetAddressItemsAsync() + { + // 模拟数据库延时 + await Task.Delay(500); + return Items.Select(i => new SelectedItem(i.Address!, i.Address!)).DistinctBy(i => i.Value).ToList(); } private Task> OnQueryAsync(QueryPageOptions options) diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index dec5d6b4ac8..2b1e4a5bf70 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -5471,6 +5471,9 @@ "BootstrapBlazor.Server.Components.Samples.Table.TablesFilter": { "MultiFilterTitle": "Multiple selection list filtering", "MultiFilterIntro": "Use the built-in MultiFilter component to provide multi-select filtering via FilterTemplate", + "MultiFilterTips": "The component provides two ways to set the data source: Items and OnGetItemsAsync", + "MultiFilterTipsLi1": "Items is suitable for small amounts of data and statically prepared data sets", + "MultiFilterTipsLi2": "OnGetItemsAsync is suitable for large and dynamic data collections. For performance reasons, the component uses a local lazy record method to fill in data, that is, the callback is called to obtain data when the filter window is opened.", "TablesFilterTitle": "Filter and sort function", "TablesFilterDesc": "Filter to quickly find the data you want to see; sort to quickly find or compare data.", "TablesFilterDescLi1": "Filters a column of data to specify the column to be filtered by specifying the filterable property of the column", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 01dcd330fc4..a225977b063 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -5471,6 +5471,9 @@ "BootstrapBlazor.Server.Components.Samples.Table.TablesFilter": { "MultiFilterTitle": "多选列表筛选", "MultiFilterIntro": "通过 FilterTemplate 使用内置 MultiFilter 组件提供多选筛选功能", + "MultiFilterTips": "组件提供 Items OnGetItemsAsync 两种设置数据源方式", + "MultiFilterTipsLi1": "Items 适合数据量小且静态事先准备好的数据集合", + "MultiFilterTipsLi2": "OnGetItemsAsync 适合数据量大且动态的数据集合,出于性能考虑组件内部采用局域懒记载方式填装数据,即点开过滤窗口时才回调获得数据", "TablesFilterTitle": "筛选和排序功能", "TablesFilterDesc": "筛选可快速查找到自己想看的数据;排序可快速查找或对比数据。", "TablesFilterDescLi1": "对某一列数据进行筛选,通过指定列的 Filterable 属性来指定需要筛选的列", diff --git a/src/BootstrapBlazor.Server/docs.json b/src/BootstrapBlazor.Server/docs.json index 5d130c8f653..23878905cc1 100644 --- a/src/BootstrapBlazor.Server/docs.json +++ b/src/BootstrapBlazor.Server/docs.json @@ -220,48 +220,52 @@ "zip-archive": "ZipArchives" }, "video": { - "autorefresh": "BV1ap4y1x7Qn?p=8", - "column": "BV1ap4y1x7Qn?p=2", - "edit": "BV1ap4y1x7Qn?p=9;BV1ap4y1x7Qn?p=10;BV1ap4y1x7Qn?p=11;BV1ap4y1x7Qn?p=12;BV12P4y137Ar", - "editors": "BV13B4y1y7cS", - "editdialogs": "BV1bT4y1N78e?p=10", - "export": "BV1ap4y1x7Qn?p=6", - "drawers": "BV1bT4y1N78e?p=7", - "detail": "BV1ap4y1x7Qn?p=3", - "dialog": "BV1bT4y1N78e?p=1", - "dialogs": "BV1bT4y1N78e?p=1;BV17v4y1K7Ho", - "header": "BV15o4y1f7eN", - "localizer": "BV1Kz4y1U7FR?p=1", - "filter": "BV1ap4y1x7Qn?p=4", - "fix-column": "BV1ap4y1x7Qn?p=5", - "footer": "BV15o4y1f7eN", - "messages": "BV1bT4y1N78e?p=3", - "modals": "BV1bT4y1N78e?p=4", - "multi-header": "BV15o4y1f7eN", - "pages": "BV1Et4y1r7qr", - "popconfirms": "BV1bT4y1N78e?p=5", - "row": "BV1ap4y1x7Qn?p=3", - "search": "BV1ap4y1x7Qn?p=4;BV1E34y1R7ia", - "searchdialogs": "BV1bT4y1N78e?p=9", - "selection": "BV1jh41127U6", - "swals": "BV1bT4y1N78e?p=8", - "tables": "BV1ap4y1x7Qn?p=1", - "template": "BV1Kp4y1B7pY", - "toolbar": "BV1ap4y1x7Qn?p=6", - "toasts": "BV1bT4y1N78e?p=6", - "tree": "BV1ap4y1x7Qn?p=14;BV1ZW4y1z7bB", - "wrap": "BV1ap4y1x7Qn?p=7", - "uploads": "BV1hK4y157Rj", - "validateforms": "BV1TU4y1Y7CM", - "dynamic": "BV1Eb4y1z7cY", - "excel": "BV1QL411x7v4", - "dynamicexcel": "BV1p3411278A", + "table": "BV1ap4y1x7Qn?p=1", + "table/auto-refresh": "BV1ap4y1x7Qn?p=8", + "table/column": "BV1ap4y1x7Qn?p=2", + "table/edit": "BV1ap4y1x7Qn?p=9;BV1ap4y1x7Qn?p=10;BV1ap4y1x7Qn?p=11;BV1ap4y1x7Qn?p=12;BV12P4y137Ar", + "table/detail": "BV1ap4y1x7Qn?p=3", + "table/dialog": "BV1bT4y1N78e?p=1", + "table/export": "BV1ap4y1x7Qn?p=6;BV1nN411V7W9;BV1Nb4y1L7p9", + "table/header": "BV15o4y1f7eN", + "table/filter": "BV1ap4y1x7Qn?p=4", + "table/fix-column": "BV1ap4y1x7Qn?p=5", + "table/multi-header": "BV15o4y1f7eN", + "table/footer": "BV15o4y1f7eN", + "table/row": "BV1ap4y1x7Qn?p=3", + "table/search": "BV1ap4y1x7Qn?p=4;BV1E34y1R7ia", + "table/selection": "BV1jh41127U6", + "table/toolbar": "BV1ap4y1x7Qn?p=6", + "table/wrap": "BV1ap4y1x7Qn?p=7", + "table/dynamic": "BV1Eb4y1z7cY", + "table/excel": "BV1QL411x7v4", + "tale/dynamic-excel": "BV1p3411278A", + "editor": "BV13B4y1y7cS", + "edit-dialog": "BV1bT4y1N78e?p=10", + "drawer": "BV1bT4y1N78e?p=7", + "dialog": "BV1bT4y1N78e?p=1;BV17v4y1K7Ho", + "message": "BV1bT4y1N78e?p=3", + "modal": "BV1bT4y1N78e?p=4", + "pagination": "BV1Et4y1r7qr", + "pop-confirm": "BV1bT4y1N78e?p=5", + "search-dialog": "BV1bT4y1N78e?p=9", + "sweet-alert": "BV1bT4y1N78e?p=8", + "toast": "BV1bT4y1N78e?p=6", + "tree-view": "BV1ap4y1x7Qn?p=14;BV1ZW4y1z7bB;BV15c411v7w7", + "upload": "BV1hK4y157Rj", + "validate-form": "BV1TU4y1Y7CM", + "speech/speechwave": "BV1Dr4y1J7Z5", + "speech/recognizer": "BV1aR4y1N7UP", + "speech/synthesizer": "BV1aR4y1N7UP", + "reconnector": "BV1Dr4y1J7Z5;BV193411P7Dz", + "topology": "BV1eY4y167jn;BV13Z4y1h7MA", + "select-object": "BV1Zw411j7Ea", + "select-table": "BV1f64y1A7AL;BV1Ye411n7XR", + "step": "BV1oN4y1y75m", + "context-menu": "BV1gk4y1w7Ab", "globalexception": "BV1xq4y1z7K2", - "recognizers": "BV1aR4y1N7UP", - "synthesizers": "BV1aR4y1N7UP", - "reconnectors": "BV1Dr4y1J7Z5;BV193411P7Dz", - "speechwaves": "BV1Dr4y1J7Z5", - "topologies": "BV1eY4y167jn;BV13Z4y1h7MA" + "localizer": "BV1Kz4y1U7FR?p=1", + "template": "BV1Kp4y1B7pY" }, "link": { "AntDesign": "http://www.antblazor.com/", diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 7f4e39a9fe5..dc76c0bbc64 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@ - 8.7.1-beta04 + 8.7.1-beta05 diff --git a/src/BootstrapBlazor/Components/Filters/FilterBase.cs b/src/BootstrapBlazor/Components/Filters/FilterBase.cs index bb55d9e8487..05de848323f 100644 --- a/src/BootstrapBlazor/Components/Filters/FilterBase.cs +++ b/src/BootstrapBlazor/Components/Filters/FilterBase.cs @@ -7,7 +7,7 @@ namespace BootstrapBlazor.Components; /// /// 类型过滤器基类 /// /// -public abstract class FilterBase : ComponentBase, IFilterAction +public abstract class FilterBase : BootstrapModuleComponentBase, IFilterAction { /// /// @@ -43,7 +43,7 @@ public abstract class FilterBase : ComponentBase, IFilterAction public int Count { get; set; } /// - /// 获得/设置 条件候选项 + /// 获得/设置 条件候选项 请尽量使用静态数据 避免组件性能损失 /// [Parameter] public IEnumerable? Items { get; set; } @@ -86,7 +86,7 @@ protected override void OnInitialized() public virtual Task SetFilterConditionsAsync(FilterKeyValueAction filter) => OnFilterValueChanged(); /// - /// + /// 过滤按钮回调方法 /// /// protected async Task OnFilterValueChanged() @@ -99,7 +99,7 @@ protected async Task OnFilterValueChanged() } /// - /// + /// 重置按钮回调方法 /// /// protected async Task OnClearFilter() diff --git a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor index ee465a48dad..96a0e96438a 100644 --- a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor +++ b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor @@ -2,8 +2,9 @@ @namespace BootstrapBlazor.Components @inherits FilterBase @inject IStringLocalizer Localizer +@attribute [BootstrapModuleAutoLoader("Filters/MultiFilter.razor.js", JSObjectReference = true)] -
+
@if (ShowSearch) { - +
}
+ @if (_source == null) + { +
+ @if (LoadingTemplate != null) + { + @LoadingTemplate + } + else + { + + } +
+ } diff --git a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.cs b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.cs index a7e6770f015..fb353a80334 100644 --- a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.cs +++ b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.cs @@ -27,18 +27,31 @@ public partial class MultiFilter [Parameter] public bool ShowSearch { get; set; } = true; + /// + /// 获得 过滤项集合回调方法 适合动态给定数据源 + /// + [Parameter] + public Func>>? OnGetItemsAsync { get; set; } + + /// + /// 获得/设置 Loading 模板 + /// + [Parameter] + public RenderFragment? LoadingTemplate { get; set; } + private string? _searchText; - private List _source = []; + private List? _source; - private List? _items; + private List? _items; /// - /// OnInitialized 方法 + /// /// protected override void OnInitialized() { base.OnInitialized(); + if (TableFilter != null) { TableFilter.ShowMoreButton = false; @@ -52,18 +65,46 @@ protected override void OnParametersSet() { base.OnParametersSet(); + if (Items != null && OnGetItemsAsync != null) + { + throw new InvalidOperationException($"{GetType()} can only accept one item source from its parameters. Do not supply both '{nameof(Items)}' and '{nameof(OnGetItemsAsync)}'."); + } + SearchPlaceHolderText ??= Localizer["MultiFilterSearchPlaceHolderText"]; SelectAllText ??= Localizer["MultiFilterSelectAllText"]; if (Items != null) { - var oldSelectedItems = _source.Where(x => x.Checked).ToList(); - _source = Items.Select(item => new MultiFilterItem() + var selectedItems = _source?.Where(x => x.Active).ToList(); + _source = Items.ToList(); + ResetActiveItems(_source, selectedItems); + } + } + + private static void ResetActiveItems(List source, List? activeItems) + { + if (activeItems != null) + { + foreach (var active in activeItems) { - Checked = oldSelectedItems.Any(x => x.Value == item.Value), - Value = item.Value, - Text = item.Text - }).ToList(); + var item = source.Find(i => i.Value == active.Value); + if (item != null) + { + item.Active = true; + } + } + } + } + + /// + /// + /// + /// + protected override async Task InvokeInitAsync() + { + if (OnGetItemsAsync != null) + { + await InvokeVoidAsync("init", Id, new { Invoker = Interop, Callback = nameof(TriggerGetItemsCallback) }); } } @@ -73,9 +114,12 @@ protected override void OnParametersSet() public override void Reset() { _searchText = string.Empty; - foreach (var item in _source) + if (_source != null) { - item.Checked = false; + foreach (var item in _source) + { + item.Active = false; + } } _items = null; StateHasChanged(); @@ -89,7 +133,7 @@ public override FilterKeyValueAction GetFilterConditions() { var filter = new FilterKeyValueAction() { Filters = [], FilterLogic = FilterLogic.Or }; - foreach (var item in GetItems().Where(i => i.Checked)) + foreach (var item in GetItems().Where(i => i.Active)) { filter.Filters.Add(new FilterKeyValueAction() { @@ -101,6 +145,20 @@ public override FilterKeyValueAction GetFilterConditions() return filter; } + /// + /// Javascript 回调方法 + /// + /// + [JSInvokable] + public async Task TriggerGetItemsCallback() + { + if (OnGetItemsAsync != null) + { + _source = await OnGetItemsAsync(); + StateHasChanged(); + } + } + private CheckboxState _selectAllState = CheckboxState.UnChecked; private CheckboxState GetState() @@ -109,9 +167,9 @@ private CheckboxState GetState() var items = GetItems(); if (items.Count > 0) { - state = items.All(i => i.Checked) + state = items.All(i => i.Active) ? CheckboxState.Checked - : items.Any(i => i.Checked) + : items.Any(i => i.Active) ? CheckboxState.Indeterminate : CheckboxState.UnChecked; } @@ -128,7 +186,7 @@ private Task OnStateChanged(CheckboxState state, bool val) { foreach (var item in GetItems()) { - item.Checked = state == CheckboxState.Checked; + item.Active = state == CheckboxState.Checked; } StateHasChanged(); return Task.CompletedTask; @@ -142,28 +200,20 @@ private Task OnStateChanged(CheckboxState state, bool val) private Task OnSearchValueChanged(string? val) { _searchText = val; - if (!string.IsNullOrEmpty(_searchText)) - { - _items = _source.Where(i => i.Text.Contains(_searchText)).ToList(); - } - else + if (_source != null) { - _items = null; + if (!string.IsNullOrEmpty(_searchText)) + { + _items = _source.Where(i => i.Text.Contains(_searchText)).ToList(); + } + else + { + _items = null; + } + StateHasChanged(); } - StateHasChanged(); return Task.CompletedTask; } - private List GetItems() => _items ?? _source; - - class MultiFilterItem - { - public bool Checked { get; set; } - - [NotNull] - public string? Value { get; init; } - - [NotNull] - public string? Text { get; init; } - } + private List GetItems() => _items ?? _source ?? []; } diff --git a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.js b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.js new file mode 100644 index 00000000000..d859d121d8d --- /dev/null +++ b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.js @@ -0,0 +1,31 @@ +import Data from "../../modules/data.js" +import EventHandler from "../../modules/event-handler.js" + +export function init(id, options) { + const el = document.getElementById(id) + if (el == null) { + return; + } + + const { invoker, callback } = options; + const filterEl = el.closest('.filter-icon'); + if (filterEl) { + const popoverEl = filterEl.querySelector('i[data-bs-toggle="bb.dropdown"]'); + if (popoverEl) { + EventHandler.on(popoverEl, 'show.bs.popover', () => { + EventHandler.off(popoverEl, 'show.bs.popover'); + invoker.invokeMethodAsync(callback); + }); + Data.set(id, popoverEl); + } + } +} + +export function dispose(id) { + const popoverEl = Data.get(id) + Data.remove(id) + + if (popoverEl) { + EventHandler.off(popoverEl, 'show.bs.popover'); + } +} diff --git a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.scss b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.scss index f45f056c898..5b87d19b60a 100644 --- a/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.scss +++ b/src/BootstrapBlazor/Components/Filters/MultiFilter.razor.scss @@ -5,6 +5,7 @@ --bb-multi-filter-body-item-bg: #fff; --bb-multi-filter-body-item-hover-bg: #fff; --bb-multi-filter-body-item-margin: .5rem; + position: relative; .bb-multi-filter-search { margin-bottom: var(--bb-multi-filter-search-margin-bottom); @@ -48,4 +49,16 @@ } } } + + .bb-multi-filter-loading { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: var(--bs-body-bg); + display: flex; + align-items: center; + justify-content: center; + } } diff --git a/src/BootstrapBlazor/Components/Filters/NumberFilter.razor.cs b/src/BootstrapBlazor/Components/Filters/NumberFilter.razor.cs index 084daeb2fbf..18ee784b63b 100644 --- a/src/BootstrapBlazor/Components/Filters/NumberFilter.razor.cs +++ b/src/BootstrapBlazor/Components/Filters/NumberFilter.razor.cs @@ -38,12 +38,12 @@ protected override void OnParametersSet() Items ??= new SelectedItem[] { - new SelectedItem("GreaterThanOrEqual", Localizer["GreaterThanOrEqual"].Value), - new SelectedItem("LessThanOrEqual", Localizer["LessThanOrEqual"].Value), - new SelectedItem("GreaterThan", Localizer["GreaterThan"].Value), - new SelectedItem("LessThan", Localizer["LessThan"].Value), - new SelectedItem("Equal", Localizer["Equal"].Value), - new SelectedItem("NotEqual", Localizer["NotEqual"].Value) + new("GreaterThanOrEqual", Localizer["GreaterThanOrEqual"].Value), + new("LessThanOrEqual", Localizer["LessThanOrEqual"].Value), + new("GreaterThan", Localizer["GreaterThan"].Value), + new("LessThan", Localizer["LessThan"].Value), + new("Equal", Localizer["Equal"].Value), + new("NotEqual", Localizer["NotEqual"].Value) }; } diff --git a/src/BootstrapBlazor/Components/Filters/StringFilter.razor.cs b/src/BootstrapBlazor/Components/Filters/StringFilter.razor.cs index 9e72829550f..6dadbe192c9 100644 --- a/src/BootstrapBlazor/Components/Filters/StringFilter.razor.cs +++ b/src/BootstrapBlazor/Components/Filters/StringFilter.razor.cs @@ -37,10 +37,10 @@ protected override void OnParametersSet() Items ??= new SelectedItem[] { - new SelectedItem("Contains", Localizer["Contains"].Value), - new SelectedItem("Equal", Localizer["Equal"].Value), - new SelectedItem("NotEqual", Localizer["NotEqual"].Value), - new SelectedItem("NotContains", Localizer["NotContains"].Value) + new("Contains", Localizer["Contains"].Value), + new("Equal", Localizer["Equal"].Value), + new("NotEqual", Localizer["NotEqual"].Value), + new("NotContains", Localizer["NotContains"].Value) }; } diff --git a/test/UnitTest/Components/TableFilterTest.cs b/test/UnitTest/Components/TableFilterTest.cs index f3ce8a4caef..7a9d53dc7df 100644 --- a/test/UnitTest/Components/TableFilterTest.cs +++ b/test/UnitTest/Components/TableFilterTest.cs @@ -101,17 +101,68 @@ public async Task MultiFilter_Ok() { b.OpenComponent(0); b.AddAttribute(1, nameof(MultiFilter.ShowSearch), true); - b.AddAttribute(2, nameof(MultiFilter.Items), new SelectedItem[] { - new("test1", "test1"), - new("test2", "test2") - }); + b.AddAttribute(2, nameof(MultiFilter.OnGetItemsAsync), () => Task.FromResult(new List() { new("test1", "test1") })); b.CloseComponent(); })); builder.CloseComponent(); }); }); }); + cut.Contains("bb-multi-filter-loading"); + var filter = cut.FindComponent(); + filter.SetParametersAndRender(pb => + { + pb.Add(a => a.LoadingTemplate, "loading-template-test"); + }); + cut.Contains("loading-template-test"); + await cut.InvokeAsync(() => filter.Instance.TriggerGetItemsCallback()); + } + + [Fact] + public void MultiFilter_Exception() + { + // 测试 Exception + Assert.Throws(() => + { + Context.RenderComponent(pb => + { + pb.Add(a => a.Items, new SelectedItem[] { new("test1", "test1"), new("test2", "test2") }); + pb.Add(a => a.OnGetItemsAsync, () => Task.FromResult(new List() { new("test1", "test1") })); + }); + }); + } + + [Fact] + public async Task MultipleFilter_Items() + { + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.Items, new List + { + new() + }); + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.TableColumns, cat => builder => + { + var index = 0; + builder.OpenComponent>(index++); + builder.AddAttribute(index++, nameof(TableColumn.Field), cat.P8); + builder.AddAttribute(index++, nameof(TableColumn.FieldExpression), Utility.GenerateValueExpression(cat, nameof(Cat.P8), typeof(string))); + builder.AddAttribute(index++, nameof(TableColumn.Filterable), true); + builder.AddAttribute(index++, nameof(TableColumn.FilterTemplate), new RenderFragment(b => + { + b.OpenComponent(0); + b.AddAttribute(1, nameof(MultiFilter.ShowSearch), true); + b.AddAttribute(2, nameof(MultiFilter.Items), new SelectedItem[] { new("test1", "test1"), new("test2", "test2") }); + b.CloseComponent(); + })); + builder.CloseComponent(); + }); + }); + }); cut.DoesNotContain("multi-filter-placeholder"); cut.DoesNotContain("multi-filter-All"); @@ -144,6 +195,15 @@ await cut.InvokeAsync(() => Assert.Equal("P8", action.Filters[0].FieldKey); Assert.Equal(CheckboxState.Indeterminate, checkboxs[0].Instance.State); + // 测试 Items 改变保持选项 + filter.SetParametersAndRender(pb => + { + pb.Add(a => a.Items, new SelectedItem[] { new("test3", "test3"), new("test2", "test2") }); + }); + checkboxs = cut.FindComponents>(); + Assert.Equal(3, checkboxs.Count); + checkboxs[2].Markup.Contains("checked=\"checked\""); + // 测试全选 await cut.InvokeAsync(() => checkboxs[0].Instance.SetState(CheckboxState.Checked)); await cut.InvokeAsync(() => checkboxs[0].Instance.SetState(CheckboxState.UnChecked));