Replace the four stacked chip-button groups (Channel, Kind, Status, Site) on the Audit Log filter bar with a reusable MultiSelectDropdown component, so the bar collapses from four full-width chip blocks to four inline dropdowns sharing one wrapped filter row. Bootstrap dropdown + checkbox menu (data-bs-auto-close =outside); no third-party UI libraries.
96 lines
3.3 KiB
C#
96 lines
3.3 KiB
C#
using Microsoft.AspNetCore.Components;
|
|
|
|
namespace ScadaLink.CentralUI.Components.Shared;
|
|
|
|
/// <summary>
|
|
/// A compact multi-select control: a Bootstrap dropdown whose toggle button
|
|
/// summarises the current selection ("All" when empty, the single item's label
|
|
/// when one is picked, or "N selected" otherwise) over a checkbox menu.
|
|
///
|
|
/// <para>
|
|
/// It exists to keep multi-value filter controls one row tall instead of a
|
|
/// wrapped block of chip buttons. The component mutates the caller-owned
|
|
/// <see cref="Selected"/> collection in place and raises
|
|
/// <see cref="SelectionChanged"/> after every toggle so the parent can react
|
|
/// (re-render, prune dependent selections, …).
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Requires the Bootstrap JS bundle (loaded in <c>App.razor</c>) for the
|
|
/// dropdown toggle; <c>data-bs-auto-close="outside"</c> keeps the menu open
|
|
/// while the operator ticks several boxes.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <typeparam name="TValue">The option value type (an enum or string).</typeparam>
|
|
public partial class MultiSelectDropdown<TValue> where TValue : notnull
|
|
{
|
|
/// <summary>The options shown in the menu, in display order.</summary>
|
|
[Parameter, EditorRequired]
|
|
public IReadOnlyList<TValue> Items { get; set; } = Array.Empty<TValue>();
|
|
|
|
/// <summary>
|
|
/// The caller-owned selection set. Mutated in place by <see cref="Toggle"/>.
|
|
/// </summary>
|
|
[Parameter, EditorRequired]
|
|
public ICollection<TValue> Selected { get; set; } = default!;
|
|
|
|
/// <summary>Maps an option to its display label. Defaults to <c>ToString()</c>.</summary>
|
|
[Parameter]
|
|
public Func<TValue, string> Display { get; set; } = static v => v.ToString() ?? string.Empty;
|
|
|
|
/// <summary>Raised after each toggle, once <see cref="Selected"/> has been updated.</summary>
|
|
[Parameter]
|
|
public EventCallback SelectionChanged { get; set; }
|
|
|
|
/// <summary>Summary text shown on the toggle button when nothing is selected.</summary>
|
|
[Parameter]
|
|
public string AllLabel { get; set; } = "All";
|
|
|
|
/// <summary>Text shown in the menu when there are no options.</summary>
|
|
[Parameter]
|
|
public string EmptyText { get; set; } = "None available";
|
|
|
|
/// <summary><c>data-test</c> root for this control, its toggle and its options.</summary>
|
|
[Parameter]
|
|
public string DataTest { get; set; } = "multi-select";
|
|
|
|
private async Task Toggle(TValue item)
|
|
{
|
|
// ICollection.Remove returns false when the item was absent — that is the
|
|
// "not currently selected" case, so add it. This is a plain toggle.
|
|
if (!Selected.Remove(item))
|
|
{
|
|
Selected.Add(item);
|
|
}
|
|
|
|
await SelectionChanged.InvokeAsync();
|
|
}
|
|
|
|
private string Summary()
|
|
{
|
|
var count = Selected.Count;
|
|
if (count == 0)
|
|
{
|
|
return AllLabel;
|
|
}
|
|
|
|
if (count == 1)
|
|
{
|
|
// Prefer the single selection's label over a bare "1 selected".
|
|
foreach (var item in Items)
|
|
{
|
|
if (Selected.Contains(item))
|
|
{
|
|
return Display(item);
|
|
}
|
|
}
|
|
|
|
// The one selected value is not in the current Items list (e.g. a Kind
|
|
// narrowed out by a Channel change before the parent pruned it).
|
|
return "1 selected";
|
|
}
|
|
|
|
return $"{count} selected";
|
|
}
|
|
}
|