From e36f0bf9c8c0a5524857b5145b5fa92de2f442e0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 21 May 2026 09:36:36 -0400 Subject: [PATCH] feat(centralui): compact multi-select dropdowns for the audit filter bar 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. --- .../Components/Audit/AuditFilterBar.razor | 119 +++++++----------- .../Components/Audit/AuditFilterBar.razor.cs | 81 ++++++------ .../Shared/MultiSelectDropdown.razor | 40 ++++++ .../Shared/MultiSelectDropdown.razor.cs | 95 ++++++++++++++ .../Shared/MultiSelectDropdown.razor.css | 32 +++++ .../Audit/AuditLogPageTests.cs | 11 +- .../Components/Audit/AuditFilterBarTests.cs | 48 +++---- 7 files changed, 282 insertions(+), 144 deletions(-) create mode 100644 src/ScadaLink.CentralUI/Components/Shared/MultiSelectDropdown.razor create mode 100644 src/ScadaLink.CentralUI/Components/Shared/MultiSelectDropdown.razor.cs create mode 100644 src/ScadaLink.CentralUI/Components/Shared/MultiSelectDropdown.razor.css diff --git a/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor b/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor index 8601e5f..22195f4 100644 --- a/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor +++ b/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor @@ -6,78 +6,55 @@
- @* Channel chip multi-select. *@ -
- -
- @foreach (var channel in Enum.GetValues()) - { - var selected = _model.Channels.Contains(channel); - - } -
-
- - @* Kind chip multi-select — narrowed by Channel selection. *@ -
- -
- @foreach (var kind in _model.VisibleKinds()) - { - var selected = _model.Kinds.Contains(kind); - - } -
-
- - @* Status chip multi-select. *@ -
- -
- @foreach (var status in Enum.GetValues()) - { - var selected = _model.Statuses.Contains(status); - - } -
-
- - @* Site chip multi-select — populated from ISiteRepository. *@ -
- -
- @if (_sites.Count == 0) - { - No sites available. - } - else - { - @foreach (var site in _sites) - { - var selected = _model.SiteIdentifiers.Contains(site.SiteIdentifier); - - } - } -
-
- + @* All filters sit in one wrapped row. The four multi-value dimensions + (Channel / Kind / Status / Site) use compact MultiSelectDropdown + controls so the bar stays a row or two tall instead of four stacked + blocks of chip buttons. *@
+
+ +
+ +
+
+ + @* Kind options are narrowed by the Channel selection (VisibleKinds). *@ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ @Display(item) + + + } + } + +
diff --git a/src/ScadaLink.CentralUI/Components/Shared/MultiSelectDropdown.razor.cs b/src/ScadaLink.CentralUI/Components/Shared/MultiSelectDropdown.razor.cs new file mode 100644 index 0000000..18471c2 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Shared/MultiSelectDropdown.razor.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Components; + +namespace ScadaLink.CentralUI.Components.Shared; + +/// +/// 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. +/// +/// +/// 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 +/// collection in place and raises +/// after every toggle so the parent can react +/// (re-render, prune dependent selections, …). +/// +/// +/// +/// Requires the Bootstrap JS bundle (loaded in App.razor) for the +/// dropdown toggle; data-bs-auto-close="outside" keeps the menu open +/// while the operator ticks several boxes. +/// +/// +/// The option value type (an enum or string). +public partial class MultiSelectDropdown where TValue : notnull +{ + /// The options shown in the menu, in display order. + [Parameter, EditorRequired] + public IReadOnlyList Items { get; set; } = Array.Empty(); + + /// + /// The caller-owned selection set. Mutated in place by . + /// + [Parameter, EditorRequired] + public ICollection Selected { get; set; } = default!; + + /// Maps an option to its display label. Defaults to ToString(). + [Parameter] + public Func Display { get; set; } = static v => v.ToString() ?? string.Empty; + + /// Raised after each toggle, once has been updated. + [Parameter] + public EventCallback SelectionChanged { get; set; } + + /// Summary text shown on the toggle button when nothing is selected. + [Parameter] + public string AllLabel { get; set; } = "All"; + + /// Text shown in the menu when there are no options. + [Parameter] + public string EmptyText { get; set; } = "None available"; + + /// data-test root for this control, its toggle and its options. + [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"; + } +} diff --git a/src/ScadaLink.CentralUI/Components/Shared/MultiSelectDropdown.razor.css b/src/ScadaLink.CentralUI/Components/Shared/MultiSelectDropdown.razor.css new file mode 100644 index 0000000..1a0e4a7 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Shared/MultiSelectDropdown.razor.css @@ -0,0 +1,32 @@ +/* Compact multi-select dropdown. Tuned to sit inline with form-select-sm / + form-control-sm controls in a filter row. */ + +.msd-toggle { + min-width: 9rem; + max-width: 15rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Keep a long option list from running off-screen — scroll within the menu. */ +.msd-menu { + max-height: 16rem; + overflow-y: auto; +} + +/* The whole row is a