Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/Components/Filters/MultiFilter.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ else
@if (ShowSearch)
{
<BootstrapInput UseInputEvent="true" class="bb-multi-filter-search"
Value="@_searchText" bind-value:event="oninput" IsAutoFocus="true"
Value="@_searchText" IsAutoFocus="true"
PlaceHolder="@SearchPlaceHolderText" IsSelectAllTextOnFocus="true"
OnValueChanged="OnSearchValueChanged" ShowLabel="false" SkipValidate="true"></BootstrapInput>
}
Expand Down
16 changes: 16 additions & 0 deletions src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@namespace BootstrapBlazor.Components
@inherits FilterBase
@typeparam TType

@if (IsHeaderRow)
{
<div class="@FilterRowClassString">
<MultiSelect class="is-filter" @bind-Value="_value1" OnValueChanged="_ => OnFilterAsync()"

Check warning on line 8 in src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor

View check run for this annotation

Codecov / codecov/patch

src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor#L8

Added line #L8 was not covered by tests
ShowLabel="false" SkipValidate="true" IsPopover="true"></MultiSelect>
<FilterButton Items="Items" @bind-Value="_action1" OnSelectedItemChanged="_ => OnFilterAsync()" OnClearFilter="OnClearFilter"></FilterButton>

Check warning on line 10 in src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor

View check run for this annotation

Codecov / codecov/patch

src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor#L10

Added line #L10 was not covered by tests
</div>
}
else
{
<MultiSelect Items="@Items" @bind-Value="@_value1" IsPopover="true" ShowLabel="false" SkipValidate="true"></MultiSelect>
}
91 changes: 91 additions & 0 deletions src/BootstrapBlazor/Components/Filters/MultiSelectFilter.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone

namespace BootstrapBlazor.Components;

/// <summary>
/// 多项选择下拉框过滤组件
/// </summary>
public partial class MultiSelectFilter<TType>
{
private string? FilterRowClassString => CssBuilder.Default("filter-row")
.AddClass("active", TableColumnFilter.HasFilter())
.Build();

private TType? _value1;
private FilterAction _action1 = FilterAction.Equal;

/// <summary>
/// Gets or sets the filter items.
/// </summary>
[Parameter]
public List<SelectedItem>? Items { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
public override void Reset()
{
_value1 = default;
_action1 = FilterAction.GreaterThanOrEqual;
Count = 0;
Logic = FilterLogic.And;
StateHasChanged();
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
public override FilterKeyValueAction GetFilterConditions()
{
var filter = new FilterKeyValueAction() { FilterLogic = FilterLogic.Or };
if (_value1 is string v)
{
var items = v.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var item in items)
{
filter.Filters.Add(new FilterKeyValueAction
{
FieldKey = FieldKey,
FieldValue = item,
FilterAction = _action1
});
}
}
else if (_value1 is IEnumerable<string> values)
{
foreach (var item in values)
{
filter.Filters.Add(new FilterKeyValueAction
{
FieldKey = FieldKey,
FieldValue = item,
FilterAction = _action1
});
}
}
return filter;
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public override async Task SetFilterConditionsAsync(FilterKeyValueAction filter)
{
var first = filter.Filters.FirstOrDefault() ?? filter;
if (first.FieldValue is TType value)
{
_value1 = value;
}
else
{
_value1 = default;
}
_action1 = first.FilterAction;

await base.SetFilterConditionsAsync(filter);
}
}
140 changes: 140 additions & 0 deletions test/UnitTest/Components/TableMultiSelectFilterTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone

namespace UnitTest.Components;

public class TableMultiSelectFilterTest : BootstrapBlazorTestBase
{
[Fact]
public void IsHeaderRow_Ok()
{
var cut = Context.RenderComponent<TableColumnFilter>(pb =>
{
pb.Add(a => a.Table, new MockTable());
pb.Add(a => a.Column, new MockColumn());
pb.Add(a => a.IsHeaderRow, true);
});
cut.Contains("filter-row");
}
[Fact]
public async Task OnFilterAsync_Ok()
{
var cut = Context.RenderComponent<TableColumnFilter>(pb =>
{
pb.Add(a => a.Table, new MockTable());
pb.Add(a => a.Column, new MockColumn());
pb.Add(a => a.IsHeaderRow, false);
});
var item = cut.Find(".dropdown-menu .dropdown-item");
await cut.InvokeAsync(() => item.Click());

var filter = cut.FindComponent<MultiSelectFilter<string>>();
var conditions = filter.Instance.GetFilterConditions();

Assert.Single(conditions.Filters);

await cut.InvokeAsync(() => filter.Instance.Reset());
conditions = filter.Instance.GetFilterConditions();
Assert.Empty(conditions.Filters);

await cut.InvokeAsync(() => filter.Instance.SetFilterConditionsAsync(new FilterKeyValueAction()
{
FieldValue = "v1,v2",
}));
conditions = filter.Instance.GetFilterConditions();
Assert.Equal(2, conditions.Filters.Count);

await cut.InvokeAsync(() => filter.Instance.SetFilterConditionsAsync(new FilterKeyValueAction()
{
FieldValue = true,
}));
conditions = filter.Instance.GetFilterConditions();
Assert.Empty(conditions.Filters);
}

[Fact]
public async Task OnFilterAsync_List()
{
var cut = Context.RenderComponent<TableColumnFilter>(pb =>
{
pb.Add(a => a.Table, new MockTable());
pb.Add(a => a.Column, new MockColumnList());
pb.Add(a => a.IsHeaderRow, false);
});
var item = cut.Find(".dropdown-menu .dropdown-item");
await cut.InvokeAsync(() => item.Click());

var filter = cut.FindComponent<MultiSelectFilter<List<string>>>();
var conditions = filter.Instance.GetFilterConditions();

Assert.Single(conditions.Filters);

await cut.InvokeAsync(() => filter.Instance.Reset());
conditions = filter.Instance.GetFilterConditions();
Assert.Empty(conditions.Filters);
}

class MockTable : ITable
{
public Dictionary<string, IFilterAction> Filters { get; set; } = [];

public Func<Task>? OnFilterAsync { get; set; }

public List<ITableColumn> Columns => [];

public IEnumerable<ITableColumn> GetVisibleColumns() => Columns;
}

class MockColumn : TableColumn<Foo, string>
{
public MockColumn()
{
PropertyType = typeof(string);
FieldName = "MultiSelect";

FilterTemplate = new RenderFragment(builder =>
{
builder.OpenComponent<FilterProvider>(0);
builder.AddAttribute(1, nameof(FilterProvider.ChildContent), new RenderFragment(builder =>
{
builder.OpenComponent<MultiSelectFilter<string>>(2);
builder.AddAttribute(3, nameof(MultiSelectFilter<string>.Items), new List<SelectedItem>
{
new("v1", "Test-1"),
new("v2", "Test-2")
});
builder.CloseComponent();
}));
builder.CloseComponent();
});
}
}

class MockColumnList : TableColumn<Foo, List<string>>
{
public MockColumnList()
{
PropertyType = typeof(List<string>);
FieldName = "MultiSelect";

FilterTemplate = new RenderFragment(builder =>
{
builder.OpenComponent<FilterProvider>(0);
builder.AddAttribute(1, nameof(FilterProvider.ChildContent), new RenderFragment(builder =>
{
builder.OpenComponent<MultiSelectFilter<List<string>>>(2);
builder.AddAttribute(3, nameof(MultiSelectFilter<List<string>>.Items), new List<SelectedItem>
{
new("v1", "Test-1"),
new("v2", "Test-2")
});
builder.CloseComponent();
}));
builder.CloseComponent();
});
}
}

}