From fbdc2e585951215909761b17ce696e630cad1c11 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 6 Oct 2023 15:46:52 +0800 Subject: [PATCH] feat(Table): order of edit pop-up consisitent with table column (#2223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 增加单元测试 * test: 更新单元测试 * test: 更新单元测试 * test: add test * test: 更新单元测试 * test: 更新单元测试 * refactor: 优化代码 * chore: 更新依赖包 * doc: 增加文档链接 * feat: 增加默认排序回调参数 * doc: 增加忽略章节配置 * test: 增加单元测试 * refactor: 增加 OrderFunc 扩展方法 * feat: 增加 ColumnOrderCallback 回调方法 * doc: 增加 ColumnOrderCallback 回调方法文档说明 * test: 增加 ColumnOrderCallback 单元测试 * refactor: PlaceHolderText 更改为参数 * test: 增加单元测试 --- .../BootstrapBlazor.Shared.csproj | 2 +- src/BootstrapBlazor.Shared/Locales/en.json | 1 + src/BootstrapBlazor.Shared/Locales/zh.json | 1 + .../Samples/Table/Tables.razor.cs | 8 ++ .../Samples/Table/TablesEdit.razor | 54 ++++++----- src/BootstrapBlazor.Shared/docs.json | 1 + .../Components/EditorForm/EditorForm.razor | 2 +- .../Components/EditorForm/EditorForm.razor.cs | 34 ++++--- .../Components/Table/Table.razor.cs | 8 +- src/BootstrapBlazor/Utils/Utility.cs | 14 +-- .../Components/DockView/DockView.razor.js | 2 +- test/UnitTest/Components/EditorFormTest.cs | 22 +++++ test/UnitTest/Components/TableTest.cs | 90 +++++++++++++++++-- test/UnitTest/Utils/GroupTest.cs | 28 +++++- 14 files changed, 211 insertions(+), 56 deletions(-) diff --git a/src/BootstrapBlazor.Shared/BootstrapBlazor.Shared.csproj b/src/BootstrapBlazor.Shared/BootstrapBlazor.Shared.csproj index b8099cf5aae..99921a66870 100644 --- a/src/BootstrapBlazor.Shared/BootstrapBlazor.Shared.csproj +++ b/src/BootstrapBlazor.Shared/BootstrapBlazor.Shared.csproj @@ -31,7 +31,7 @@ - + diff --git a/src/BootstrapBlazor.Shared/Locales/en.json b/src/BootstrapBlazor.Shared/Locales/en.json index 49c70103371..1ddd6d31ef4 100644 --- a/src/BootstrapBlazor.Shared/Locales/en.json +++ b/src/BootstrapBlazor.Shared/Locales/en.json @@ -4665,6 +4665,7 @@ "OnQueryAsyncAttr": "Asynchronous query callback method", "OnAddAsyncAttr": "New Button Callback Method", "OnColumnCreatingAttr": "Callback delegate method on column creation", + "ColumnOrderCallbackAttr": "Callback delegate method on column order", "OnDoubleClickCellCallbackAttr": "Set cell double-click event", "OnDeleteAsyncAttr": "Delete Button Asynchronous Callback Method", "OnEditAsyncAttr": "Edit button asynchronous callback method", diff --git a/src/BootstrapBlazor.Shared/Locales/zh.json b/src/BootstrapBlazor.Shared/Locales/zh.json index f29ceb95e66..94e484a275a 100644 --- a/src/BootstrapBlazor.Shared/Locales/zh.json +++ b/src/BootstrapBlazor.Shared/Locales/zh.json @@ -4665,6 +4665,7 @@ "OnQueryAsyncAttr": "异步查询回调方法", "OnAddAsyncAttr": "新建按钮回调方法", "OnColumnCreatingAttr": "列创建时回调委托方法", + "ColumnOrderCallbackAttr": "列排序回调委托方法", "OnDoubleClickCellCallbackAttr": "设置单元格双击事件", "OnDeleteAsyncAttr": "删除按钮异步回调方法", "OnEditAsyncAttr": "编辑按钮异步回调方法", diff --git a/src/BootstrapBlazor.Shared/Samples/Table/Tables.razor.cs b/src/BootstrapBlazor.Shared/Samples/Table/Tables.razor.cs index d0be80f8fc5..7beb2bd9715 100644 --- a/src/BootstrapBlazor.Shared/Samples/Table/Tables.razor.cs +++ b/src/BootstrapBlazor.Shared/Samples/Table/Tables.razor.cs @@ -949,6 +949,14 @@ private void OnClick() DefaultValue = " — " }, new() + { + Name = nameof(Table.ColumnOrderCallback), + Description = Localizer["ColumnOrderCallbackAttr"], + Type = "Func, IEnumerable>", + ValueList = " — ", + DefaultValue = " — " + }, + new() { Name = nameof(Table.OnDoubleClickCellCallback), Description = Localizer["OnDoubleClickCellCallbackAttr"], diff --git a/src/BootstrapBlazor.Shared/Samples/Table/TablesEdit.razor b/src/BootstrapBlazor.Shared/Samples/Table/TablesEdit.razor index c0aa53120d1..bc0fa846bc9 100644 --- a/src/BootstrapBlazor.Shared/Samples/Table/TablesEdit.razor +++ b/src/BootstrapBlazor.Shared/Samples/Table/TablesEdit.razor @@ -10,7 +10,7 @@ -

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

+
@((MarkupString)Localizer["TablesEditItemsDescription"].Value)
-

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

-

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

-

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

-

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

-

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

-

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

-

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

+
+

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

+

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

+

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

+

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

+

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

+

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

+

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

+
-

+

@((MarkupString)Localizer["TablesColumnEditTemplateDescription1"].Value)
@((MarkupString)Localizer["TablesColumnEditTemplateTips"].Value)
-

+
-

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

-

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

-

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

+
+

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

+

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

+

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

+
-

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

- +
+

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

+ +
-

+

@((MarkupString)Localizer["TablesEditInjectDataServiceDescription"].Value)
  • OnAddAsync
  • @@ -163,8 +169,8 @@
    @Localizer["TablesEditInjectDataServiceTips1"] @Localizer["TablesEditInjectDataServiceTips2"]
    @Localizer["TablesEditInjectDataServiceTips3"]
    @Localizer["TablesEditInjectDataServiceTips4"]
    -

    -
    services.AddTableDemoDataService();
    +
    services.AddTableDemoDataService();
    +
-

+

@Localizer["TablesEditDataServiceDescription"]
@((MarkupString)Localizer["TablesEditDataServiceTips1"].Value)
@((MarkupString)Localizer["TablesEditDataServiceTips2"].Value)
-

-
services.AddTableDemoDataService();
+
services.AddTableDemoDataService();
+
-

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

-
<EditFooterTemplate Context="model">
+    
+

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

+
<EditFooterTemplate Context="model">
     <Button Text="Popup" Color="Color.Danger" Icon="fa-regular fa-comment-dots" OnClick="() => OnClick(model)"></Button>
     <DialogCloseButton />
     <DialogSaveButton Color="Color.Primary" Icon="fa-solid fa-floppy-disk" Text="Save" />
 </EditFooterTemplate>
+
+ @FieldItems?.Invoke(Model) diff --git a/src/BootstrapBlazor/Components/EditorForm/EditorForm.razor.cs b/src/BootstrapBlazor/Components/EditorForm/EditorForm.razor.cs index 62f187f56bd..40f45f89bef 100644 --- a/src/BootstrapBlazor/Components/EditorForm/EditorForm.razor.cs +++ b/src/BootstrapBlazor/Components/EditorForm/EditorForm.razor.cs @@ -129,12 +129,25 @@ public partial class EditorForm : IShowLabel [Parameter] public IEnumerable? Items { get; set; } + /// + /// 获得/设置 自定义列排序规则 默认 null 未设置 使用内部排序机制 1 2 3 0 -3 -2 -1 顺序 + /// + [Parameter] + public Func, IEnumerable>? ColumnOrderCallback { get; set; } + /// /// 获得/设置 未设置 GroupName 编辑项是否放置在顶部 默认 false /// [Parameter] public bool ShowUnsetGroupItemsOnTop { get; set; } + /// + /// 获得/设置 默认占位符文本 默认 null + /// + [Parameter] + [NotNull] + public string? PlaceHolderText { get; set; } + /// /// 获得/设置 级联上下文 EditContext 实例 内置于 EditForm 或者 ValidateForm 时有值 /// @@ -158,23 +171,20 @@ public partial class EditorForm : IShowLabel /// /// 获得/设置 配置编辑项目集合 /// - private List EditorItems { get; } = new(); + private readonly List _editorItems = new(); /// /// 获得/设置 渲染的编辑项集合 /// - private List FormItems { get; } = new(); + private readonly List _formItems = new(); - private IEnumerable UnsetGroupItems => FormItems.Where(i => string.IsNullOrEmpty(i.GroupName)).OrderBy(i => i.Order); + private IEnumerable UnsetGroupItems => _formItems.Where(i => string.IsNullOrEmpty(i.GroupName)); - private IEnumerable>> GroupItems => FormItems + private IEnumerable>> GroupItems => _formItems .Where(i => !string.IsNullOrEmpty(i.GroupName)) .GroupBy(i => i.GroupOrder).OrderBy(i => i.Key) .Select(i => new KeyValuePair>(i.First().GroupName!, i.OrderBy(x => x.Order))); - [NotNull] - private string? PlaceHolderText { get; set; } - /// /// OnInitialized 方法 /// @@ -231,7 +241,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (Items != null) { // 通过级联参数渲染组件 - FormItems.AddRange(Items); + _formItems.AddRange(Items); } else { @@ -239,10 +249,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (AutoGenerateAllItem) { // 获取绑定模型所有属性 - var items = Utility.GetTableColumns().ToList(); + var items = Utility.GetTableColumns(defaultOrderCallback: ColumnOrderCallback).ToList(); // 通过设定的 FieldItems 模板获取项进行渲染 - foreach (var el in EditorItems) + foreach (var el in _editorItems) { var item = items.FirstOrDefault(i => i.GetFieldName() == el.GetFieldName()); if (item != null) @@ -260,11 +270,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender) } } } - FormItems.AddRange(items.Where(i => i.Editable)); + _formItems.AddRange(items.Where(i => i.Editable)); } else { - FormItems.AddRange(EditorItems.Where(i => i.Editable)); + _formItems.AddRange(_editorItems.Where(i => i.Editable)); } } StateHasChanged(); diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.cs b/src/BootstrapBlazor/Components/Table/Table.razor.cs index 550a02374a5..44d91a177bd 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.cs @@ -542,6 +542,12 @@ public void ExpandDetailRow(TItem item) [Parameter] public Func, Task>? OnColumnCreating { get; set; } + /// + /// 获得/设置 自定义列排序规则 默认 null 未设置 使用内部排序机制 1 2 3 0 -3 -2 -1 顺序 + /// + [Parameter] + public Func, IEnumerable>? ColumnOrderCallback { get; set; } + /// /// 获得/设置 OnAfterRenderCallback 是否已经触发 默认 false /// @@ -822,7 +828,7 @@ private async Task ProcessFirstRender() // 初始化列 if (AutoGenerateColumns) { - var cols = Utility.GetTableColumns(Columns); + var cols = Utility.GetTableColumns(Columns, ColumnOrderCallback); Columns.Clear(); Columns.AddRange(cols); } diff --git a/src/BootstrapBlazor/Utils/Utility.cs b/src/BootstrapBlazor/Utils/Utility.cs index 6272808daaf..3ab1dda6434 100644 --- a/src/BootstrapBlazor/Utils/Utility.cs +++ b/src/BootstrapBlazor/Utils/Utility.cs @@ -263,16 +263,18 @@ public static void Copy(TModel source, TModel destination) where TModel /// /// /// + /// 默认排序回调方法 /// - public static IEnumerable GetTableColumns(IEnumerable? source = null) => GetTableColumns(typeof(TModel), source); + public static IEnumerable GetTableColumns(IEnumerable? source = null, Func, IEnumerable>? defaultOrderCallback = null) => GetTableColumns(typeof(TModel), source, defaultOrderCallback); /// /// 通过特定类型模型获取模型属性集合 /// /// 绑定模型类型 /// Razor 文件中列集合 + /// 默认排序回调方法 /// - public static IEnumerable GetTableColumns(Type type, IEnumerable? source = null) + public static IEnumerable GetTableColumns(Type type, IEnumerable? source = null, Func, IEnumerable>? defaultOrderCallback = null) { var cols = new List(50); var classAttribute = type.GetCustomAttribute(true); @@ -320,11 +322,13 @@ public static IEnumerable GetTableColumns(Type type, IEnumerable a.Order > 0).OrderBy(a => a.Order) - .Concat(cols.Where(a => a.Order == 0)) - .Concat(cols.Where(a => a.Order < 0).OrderBy(a => a.Order)); + return defaultOrderCallback?.Invoke(cols) ?? cols.OrderFunc(); } + private static IEnumerable OrderFunc(this List cols) => cols.Where(a => a.Order > 0).OrderBy(a => a.Order) + .Concat(cols.Where(a => a.Order == 0)) + .Concat(cols.Where(a => a.Order < 0).OrderBy(a => a.Order)); + /// /// 通过指定 Model 获得 IEditorItem 集合方法 /// diff --git a/src/Extensions/Components/BootstrapBlazor.Dock/Components/DockView/DockView.razor.js b/src/Extensions/Components/BootstrapBlazor.Dock/Components/DockView/DockView.razor.js index 1bbc3099590..4f52767edcb 100644 --- a/src/Extensions/Components/BootstrapBlazor.Dock/Components/DockView/DockView.razor.js +++ b/src/Extensions/Components/BootstrapBlazor.Dock/Components/DockView/DockView.razor.js @@ -188,7 +188,7 @@ const unLockStack = (stack, dock) => { const resetDockLock = dock => { const unlocks = dock.layout.getAllContentItems().filter(com => com.isComponent && !com.container.initialState.lock) const lock = unlocks.length === 0 - if (dock.lock != lock) { + if (dock.lock !== lock) { dock.lock = lock dock.invokeLockAsync(lock) } diff --git a/test/UnitTest/Components/EditorFormTest.cs b/test/UnitTest/Components/EditorFormTest.cs index e59f58f0c24..51f6e4026f5 100644 --- a/test/UnitTest/Components/EditorFormTest.cs +++ b/test/UnitTest/Components/EditorFormTest.cs @@ -364,6 +364,28 @@ public void Order_Ok() Assert.Equal(1, item.Instance.Order); } + [Fact] + public void ColumnOrderCallback_Ok() + { + var foo = new Foo(); + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Model, foo); + pb.Add(a => a.AutoGenerateAllItem, true); + pb.Add(a => a.ColumnOrderCallback, cols => + { + return cols.OrderByDescending(i => i.Order); + }); + }); + var editor = cut.Instance; + var itemsField = editor.GetType().GetField("_formItems", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetField); + Assert.NotNull(itemsField); + + var v = itemsField.GetValue(editor) as List; + Assert.NotNull(v); + Assert.Equal(new List() { 60, 50, 40, 20, 10, 1 }, v.Select(i => i.Order)); + } + [Fact] public void LookupServiceKey_Ok() { diff --git a/test/UnitTest/Components/TableTest.cs b/test/UnitTest/Components/TableTest.cs index 813e49d65dd..f8477615202 100644 --- a/test/UnitTest/Components/TableTest.cs +++ b/test/UnitTest/Components/TableTest.cs @@ -2509,6 +2509,7 @@ public void ShowEmpty_Ok() [Fact] public void ScrollMode_Query_Ok() { + var isVirtual = false; var localizer = Context.Services.GetRequiredService>(); var cut = Context.RenderComponent(pb => { @@ -2517,7 +2518,17 @@ public void ScrollMode_Query_Ok() pb.Add(a => a.RenderMode, TableRenderMode.Table); pb.Add(a => a.ScrollMode, ScrollMode.Virtual); pb.Add(a => a.ShowLineNo, true); - pb.Add(a => a.OnQueryAsync, OnQueryAsync(localizer)); + pb.Add(a => a.OnQueryAsync, option => + { + isVirtual = option.IsVirtualScroll; + var items = Foo.GenerateFoo(localizer, 5); + var ret = new QueryData() + { + Items = items, + TotalCount = 5 + }; + return Task.FromResult(ret); + }); pb.Add(a => a.TableColumns, foo => builder => { builder.OpenComponent>(0); @@ -2549,6 +2560,48 @@ public void ScrollMode_Query_Ok() var th = ths[1]; th.Click(); }); + Assert.True(isVirtual); + } + + [Fact] + public void RenderPlaceHolderRow_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.ScrollMode, ScrollMode.Virtual); + pb.Add(a => a.ShowLineNo, true); + pb.Add(a => a.OnQueryAsync, option => + { + var items = Foo.GenerateFoo(localizer, 8).Skip(option.StartIndex).Take(option.PageItems); + var ret = new QueryData() + { + Items = items, + TotalCount = option.PageItems + }; + return Task.FromResult(ret); + }); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Name"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string))); + builder.AddAttribute(3, "Sortable", true); + builder.CloseComponent(); + }); + }); + }); + cut.Contains("table-cell is-ph"); + + var table = cut.FindComponent>(); + table.SetParametersAndRender(pb => + { + pb.Add(a => a.ShowExtendButtons, true); + }); + cut.Contains("table-cell is-ph"); } [Theory] @@ -4161,6 +4214,29 @@ public void TableColumn_DefaultSort() Assert.True(column.Instance.DefaultSort); } + [Fact] + public void ColumnOrderCallback_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var items = Foo.GenerateFoo(localizer, 2); + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.RenderMode, TableRenderMode.Table); + pb.Add(a => a.Items, items); + pb.Add(a => a.AutoGenerateColumns, true); + pb.Add(a => a.ColumnOrderCallback, cols => + { + return cols.OrderByDescending(i => i.Order); + }); + }); + }); + var table = cut.FindComponent>(); + var seqs = table.Instance.Columns.Select(i => i.Order); + Assert.Equal(new List() { 70, 60, 50, 40, 20, 10, 1 }, seqs); + } + [Fact] public void TableColumn_TextWrap() { @@ -6494,7 +6570,7 @@ public void CheckShownWithBreakpoint_Ok() } [Fact] - public async Task QueryItems_Null() + public void QueryItems_Null() { var localizer = Context.Services.GetRequiredService>(); var cut = Context.RenderComponent(pb => @@ -6507,14 +6583,12 @@ public async Task QueryItems_Null() }); var table = cut.FindComponent(); - var items = await table.Instance.DataService!.QueryAsync(new QueryPageOptions()); - Assert.Null(items.Items); - - table.SetParametersAndRender(pb => + Assert.NotNull(table.Instance.DataService); + cut.InvokeAsync(async () => { - pb.Add(a => a.ScrollMode, ScrollMode.Virtual); + var items = await table.Instance.DataService.QueryAsync(new QueryPageOptions()); + Assert.Null(items.Items); }); - await cut.InvokeAsync(() => table.Instance.QueryAsync()); } [Fact] diff --git a/test/UnitTest/Utils/GroupTest.cs b/test/UnitTest/Utils/GroupTest.cs index 8fbb6f0a865..dbeca20b71b 100644 --- a/test/UnitTest/Utils/GroupTest.cs +++ b/test/UnitTest/Utils/GroupTest.cs @@ -17,10 +17,10 @@ public void Group_Order_Ok() var items = new List(50) { - new MockTableColumn("Test1", typeof(string)) { GroupName = "Test1", GroupOrder = 2, Order = 2 }, - new MockTableColumn("Test2", typeof(string)) { GroupName = "Test1", GroupOrder = 2, Order = 1 }, - new MockTableColumn("Test3", typeof(string)) { GroupName = "Test2", GroupOrder = 1, Order = 2 }, - new MockTableColumn("Test4", typeof(string)) { GroupName = "Test2", GroupOrder = 1, Order = 1 } + new("Test1", typeof(string)) { GroupName = "Test1", GroupOrder = 2, Order = 2 }, + new("Test2", typeof(string)) { GroupName = "Test1", GroupOrder = 2, Order = 1 }, + new("Test3", typeof(string)) { GroupName = "Test2", GroupOrder = 1, Order = 2 }, + new("Test4", typeof(string)) { GroupName = "Test2", GroupOrder = 1, Order = 1 } }; var groups = items.GroupBy(i => i.GroupOrder).OrderBy(i => i.Key).Select(i => new { i.Key, Items = i.OrderBy(x => x.Order) }); foreach (var g in groups) @@ -36,4 +36,24 @@ public void Group_Order_Ok() var actual = string.Join(",", results); Assert.Equal(expected, actual); } + + [Fact] + public void OrderBy_Ok() + { + var items = new List { 0, -1, 1, -4 }; + var actual = items.OrderByDescending(i => i); + Assert.Equal("1, 0, -1, -4", string.Join(", ", actual)); + + items = new List { 1, 3, 2, 4 }; + actual = items.OrderBy(i => i); + Assert.Equal("1, 2, 3, 4", string.Join(", ", actual)); + + items = new List { -1, -3, -2, -4 }; + actual = items.OrderBy(i => i); + Assert.Equal("-4, -3, -2, -1", string.Join(", ", actual)); + + items = new List { 2, 1, 0, -1, -3, -2, -4 }; + actual = items.OrderBy(i => i); + Assert.Equal("-4, -3, -2, -1, 0, 1, 2", string.Join(", ", actual)); + } }