Skip to content

Commit

Permalink
feat(Table): add TableToolbarComponent (#2799)
Browse files Browse the repository at this point in the history
* refactor: 更新 input-group btn-group 样式

* feat: 支持自定义组件

* feat: 增加 OnGetSelectedRows 级联传参

* chore: bump version 8.1.7-beta01

* test: 更新单元测试

* feat: 拆分接口

* test: 增加单元测试

* test: 增加异常单元测试
  • Loading branch information
ArgoZhang authored Jan 14, 2024
1 parent c910914 commit aff2752
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>8.1.6</Version>
<Version>8.1.7-beta01</Version>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
@mixin direction($var, $child) {
> .datetime-picker:not(:#{$child}-child) .form-control,
> .select:not(:#{$child}-child) .form-control,
> .switch:not(:#{$child}-child),
> .multi-select:not(:#{$child}-child) .dropdown-toggle,
> [data-bs-toggle]:not(:#{$child}-child) > .form-control,
> .auto-complete:not(:#{$child}-child) .form-control {
border-top-#{$var}-radius: 0;
border-bottom-#{$var}-radius: 0;
}
}

.input-group {
> .datetime-picker,
> .select,
Expand All @@ -35,7 +23,4 @@
> .segmented {
border: 1px solid var(--bs-border-color);
}

@include direction(right,last);
@include direction(left,first)
}
7 changes: 1 addition & 6 deletions src/BootstrapBlazor/Components/Table/ITableToolbarButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// ITableToolbarButton 接口
/// </summary>
public interface ITableToolbarButton<TItem>
public interface ITableToolbarButton<TItem> : IToolbarComponent<TItem>
{
/// <summary>
/// 获得/设置 选中一行时启用按钮 默认 false 均可用
Expand All @@ -18,9 +18,4 @@ public interface ITableToolbarButton<TItem>
/// 获得/设置 按钮是否被禁用回调方法
/// </summary>
Func<IEnumerable<TItem>, bool>? IsDisabledCallback { get; set; }

/// <summary>
/// 获得/设置 是否显示 默认 true 显示
/// </summary>
bool IsShow { get; set; }
}
16 changes: 16 additions & 0 deletions src/BootstrapBlazor/Components/Table/IToolbarComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

namespace BootstrapBlazor.Components;

/// <summary>
/// 工具栏按钮接口
/// </summary>
public interface IToolbarComponent<TItem>
{
/// <summary>
/// 获得/设置 是否显示 默认 true 显示
/// </summary>
bool IsShow { get; set; }
}
12 changes: 9 additions & 3 deletions src/BootstrapBlazor/Components/Table/TableToolbar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
@ChildContent
</CascadingValue>
<RenderTemplate>
@if (Buttons.Count > 0)
@if (_buttons.Count > 0)
{
<div class="@ToolbarClassString">
@foreach (var button in Buttons)
@foreach (var button in _buttons)
{
@if (button is TableToolbarButton<TItem> b && b.IsShow)
{
Expand All @@ -31,6 +31,12 @@
CloseButtonText="@pb.CloseButtonText" Content="@pb.Content">
</PopConfirmButton>
}
else if (button is TableToolbarComponent<TItem> cb && cb.IsShow)
{
<CascadingValue Value="OnGetSelectedRows" IsFixed="true">
@cb.ChildContent
</CascadingValue>
}
}
</div>
@if (IsAutoCollapsedToolbarButton)
Expand All @@ -40,7 +46,7 @@
<i class="@GearIcon"></i>
</button>
<div class="dropdown-menu">
@foreach (var button in Buttons)
@foreach (var button in _buttons)
{
@if (button is TableToolbarButton<TItem> { IsShow: true } b)
{
Expand Down
6 changes: 3 additions & 3 deletions src/BootstrapBlazor/Components/Table/TableToolbar.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public partial class TableToolbar<TItem> : ComponentBase
/// <summary>
/// 获得 Toolbar 按钮集合
/// </summary>
private List<ButtonBase> Buttons { get; } = [];
private readonly List<IToolbarComponent<TItem>> _buttons = [];

private readonly ConcurrentDictionary<ButtonBase, bool> _asyncButtonStateCache = new();

Expand Down Expand Up @@ -109,10 +109,10 @@ private bool GetDisabled(ButtonBase button)
/// <summary>
/// 添加按钮到工具栏方法
/// </summary>
public void AddButton(ButtonBase button) => Buttons.Add(button);
public void AddButton(IToolbarComponent<TItem> button) => _buttons.Add(button);

/// <summary>
/// 移除按钮到工具栏方法
/// </summary>
public void RemoveButton(ButtonBase button) => Buttons.Remove(button);
public void RemoveButton(IToolbarComponent<TItem> button) => _buttons.Remove(button);
}
60 changes: 60 additions & 0 deletions src/BootstrapBlazor/Components/Table/TableToolbarComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

namespace BootstrapBlazor.Components;

/// <summary>
/// Table 工具栏自定义组件
/// </summary>
public class TableToolbarComponent<TItem> : ComponentBase, IToolbarComponent<TItem>, IDisposable
{
/// <summary>
/// <inheritdoc/>
/// </summary>
[Parameter]
public bool IsShow { get; set; } = true;

/// <summary>
/// 获得/设置 子组件
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }

/// <summary>
/// 获得/设置 Table Toolbar 实例
/// </summary>
[CascadingParameter]
protected TableToolbar<TItem>? Toolbar { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();

Toolbar?.AddButton(this);
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Toolbar?.RemoveButton(this);
}
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
2 changes: 0 additions & 2 deletions src/BootstrapBlazor/Utils/JSModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ public virtual async ValueTask InvokeVoidAsync(string identifier, CancellationTo
}
await InvokeVoidAsync();

[ExcludeFromCodeCoverage]
async ValueTask InvokeVoidAsync()
{
try
Expand Down Expand Up @@ -110,7 +109,6 @@ public virtual async ValueTask<TValue> InvokeAsync<TValue>(string identifier, Ca
}
return await InvokeAsync();

[ExcludeFromCodeCoverage]
async ValueTask<TValue> InvokeAsync()
{
TValue ret = default!;
Expand Down
22 changes: 22 additions & 0 deletions src/BootstrapBlazor/wwwroot/scss/root.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,25 @@ a, a:hover, a:focus {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}

@mixin direction($var, $child) {
> .datetime-picker:not(:#{$child}-child) .form-control,
> .select:not(:#{$child}-child) .form-control,
> .switch:not(:#{$child}-child),
> .multi-select:not(:#{$child}-child) .dropdown-toggle,
> [data-bs-toggle]:not(:#{$child}-child) > .form-control,
> .auto-complete:not(:#{$child}-child) .form-control {
border-top-#{$var}-radius: 0;
border-bottom-#{$var}-radius: 0;
}
}

.input-group {
@include direction(right,last);
@include direction(left,first)
}

.btn-group {
@include direction(right,last);
@include direction(left,first)
}
21 changes: 20 additions & 1 deletion test/UnitTest/Components/TableTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2265,6 +2265,16 @@ public async Task CustomerToolbarButton_Ok()
builder.OpenComponent<MockToolbarButton<Foo>>(0);
builder.AddAttribute(12, nameof(MockToolbarButton<Foo>.Text), "test-confirm-mock");
builder.CloseComponent();

builder.OpenComponent<TableToolbarComponent<Foo>>(0);
builder.AddAttribute(14, nameof(TableToolbarComponent<Foo>.IsShow), true);
builder.AddAttribute(13, nameof(TableToolbarComponent<Foo>.ChildContent), new RenderFragment(b =>
{
b.OpenComponent<Button>(0);
b.AddAttribute(1, "Text", "test");
b.CloseComponent();
}));
builder.CloseComponent();
});
});
});
Expand Down Expand Up @@ -2472,6 +2482,9 @@ public void TableToolbar_Null()

var cut1 = Context.RenderComponent<TableToolbarPopConfirmButton<Foo>>();
Assert.Equal("", cut1.Markup);

var cut2 = Context.RenderComponent<TableToolbarComponent<Foo>>();
Assert.Equal("", cut2.Markup);
}

[Fact]
Expand Down Expand Up @@ -7623,8 +7636,14 @@ protected override ValueTask DisposeAsync(bool disposing)
}
}

private class MockToolbarButton<TItem> : ButtonBase
private class MockToolbarButton<TItem> : ButtonBase, ITableToolbarButton<TItem>
{
public bool IsEnableWhenSelectedOneRow { get; set; }

public Func<IEnumerable<TItem>, bool>? IsDisabledCallback { get; set; }

public bool IsShow { get; set; } = true;

[CascadingParameter]
[NotNull]
protected TableToolbar<TItem>? Buttons { get; set; }
Expand Down
54 changes: 54 additions & 0 deletions test/UnitTest/Utils/JSModuleTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,33 @@ public async Task JSModule_JSDisconnectedException()
await module.InvokeAsync<int>("test");
}

[Fact]
public async Task JSModule_JSException()
{
var js = new MockJSExceptionObjectReference();
var module = new JSModule(js);
await module.InvokeVoidAsync("test");
await module.InvokeAsync<int>("test");
}

[Fact]
public async Task JSModule_AggregateException()
{
var js = new MockAggregateExceptionObjectReference();
var module = new JSModule(js);
await module.InvokeVoidAsync("test");
await module.InvokeAsync<int>("test");
}

[Fact]
public async Task JSModule_InvalidOperationException()
{
var js = new MockInvalidOperationExceptionObjectReference();
var module = new JSModule(js);
await module.InvokeVoidAsync("test");
await module.InvokeAsync<int>("test");
}

[Fact]
public async Task JSModule_TaskCanceledException()
{
Expand Down Expand Up @@ -123,4 +150,31 @@ private class MockTaskCanceledObjectReference : IJSObjectReference

public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => throw new TaskCanceledException("Test");
}

private class MockJSExceptionObjectReference : IJSObjectReference
{
public ValueTask DisposeAsync() => throw new TaskCanceledException("Test");

public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, object?[]? args) => throw new JSException("Test");

public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => throw new JSException("Test");
}

private class MockAggregateExceptionObjectReference : IJSObjectReference
{
public ValueTask DisposeAsync() => throw new TaskCanceledException("Test");

public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, object?[]? args) => throw new AggregateException("Test");

public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => throw new AggregateException("Test");
}

private class MockInvalidOperationExceptionObjectReference : IJSObjectReference
{
public ValueTask DisposeAsync() => throw new TaskCanceledException("Test");

public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, object?[]? args) => throw new InvalidOperationException("Test");

public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args) => throw new InvalidOperationException("Test");
}
}

0 comments on commit aff2752

Please sign in to comment.