feat(centralui): DateTimeRangeFilter reusable from/to input component (T35c)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
@* Reusable from/to datetime-local input pair for filter bars.
|
||||
INPUT-ONLY: emits raw DateTime? (Unspecified kind) via FromChanged / ToChanged.
|
||||
UTC conversion is the caller's responsibility — no Apply button, no service injection. *@
|
||||
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label small mb-1" for="@(IdPrefix)-from">@FromLabel</label>
|
||||
<input id="@(IdPrefix)-from"
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
data-test="@(IdPrefix)-from"
|
||||
value="@From?.ToString("yyyy-MM-ddTHH:mm")"
|
||||
@onchange="OnFromChanged" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label small mb-1" for="@(IdPrefix)-to">@ToLabel</label>
|
||||
<input id="@(IdPrefix)-to"
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
data-test="@(IdPrefix)-to"
|
||||
value="@To?.ToString("yyyy-MM-ddTHH:mm")"
|
||||
@onchange="OnToChanged" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public DateTime? From { get; set; }
|
||||
[Parameter] public EventCallback<DateTime?> FromChanged { get; set; }
|
||||
[Parameter] public DateTime? To { get; set; }
|
||||
[Parameter] public EventCallback<DateTime?> ToChanged { get; set; }
|
||||
|
||||
/// <summary>Prefix applied to all id/data-test attributes. Pages pass short codes like "no", "sc", "audit-filter".</summary>
|
||||
[Parameter] public string IdPrefix { get; set; } = "dtr";
|
||||
|
||||
[Parameter] public string FromLabel { get; set; } = "From";
|
||||
[Parameter] public string ToLabel { get; set; } = "To";
|
||||
|
||||
private async Task OnFromChanged(ChangeEventArgs e)
|
||||
{
|
||||
var parsed = ParseInput(e.Value?.ToString());
|
||||
await FromChanged.InvokeAsync(parsed);
|
||||
}
|
||||
|
||||
private async Task OnToChanged(ChangeEventArgs e)
|
||||
{
|
||||
var parsed = ParseInput(e.Value?.ToString());
|
||||
await ToChanged.InvokeAsync(parsed);
|
||||
}
|
||||
|
||||
private static DateTime? ParseInput(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return null;
|
||||
|
||||
return DateTime.TryParse(raw, out var dt) ? dt : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Component tests for <see cref="DateTimeRangeFilter"/> — the reusable from/to
|
||||
/// datetime-local input pair. The component is INPUT-ONLY: it emits raw
|
||||
/// DateTime? (Unspecified kind); UTC conversion is the page's responsibility.
|
||||
/// </summary>
|
||||
public class DateTimeRangeFilterTests : BunitContext
|
||||
{
|
||||
[Fact]
|
||||
public void Renders_TwoInputs_WithPrefixedIdsAndTestHooks()
|
||||
{
|
||||
var cut = Render<DateTimeRangeFilter>(ps => ps
|
||||
.Add(p => p.IdPrefix, "sc"));
|
||||
|
||||
var fromInput = cut.Find("[data-test='sc-from']");
|
||||
var toInput = cut.Find("[data-test='sc-to']");
|
||||
|
||||
Assert.Equal("datetime-local", fromInput.GetAttribute("type"));
|
||||
Assert.Equal("datetime-local", toInput.GetAttribute("type"));
|
||||
Assert.Equal("sc-from", fromInput.GetAttribute("id"));
|
||||
Assert.Equal("sc-to", toInput.GetAttribute("id"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersLabels()
|
||||
{
|
||||
var cut = Render<DateTimeRangeFilter>(ps => ps
|
||||
.Add(p => p.FromLabel, "Start")
|
||||
.Add(p => p.ToLabel, "End"));
|
||||
|
||||
cut.Find("label[for='dtr-from']"); // default IdPrefix = "dtr"
|
||||
cut.Find("label[for='dtr-to']");
|
||||
|
||||
Assert.Contains("Start", cut.Markup);
|
||||
Assert.Contains("End", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingFromInput_InvokesFromChanged_WithParsedValue()
|
||||
{
|
||||
DateTime? captured = default;
|
||||
var cut = Render<DateTimeRangeFilter>(ps => ps
|
||||
.Add(p => p.IdPrefix, "sc")
|
||||
.Add(p => p.FromChanged,
|
||||
EventCallback.Factory.Create<DateTime?>(this, v => captured = v)));
|
||||
|
||||
cut.Find("[data-test='sc-from']").Change("2026-06-01T08:30");
|
||||
|
||||
Assert.NotNull(captured);
|
||||
Assert.Equal(2026, captured!.Value.Year);
|
||||
Assert.Equal(6, captured.Value.Month);
|
||||
Assert.Equal(1, captured.Value.Day);
|
||||
Assert.Equal(8, captured.Value.Hour);
|
||||
Assert.Equal(30, captured.Value.Minute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearingFromInput_InvokesFromChanged_WithNull()
|
||||
{
|
||||
DateTime? captured = new DateTime(2026, 1, 1);
|
||||
var cut = Render<DateTimeRangeFilter>(ps => ps
|
||||
.Add(p => p.IdPrefix, "sc")
|
||||
.Add(p => p.From, new DateTime(2026, 1, 1))
|
||||
.Add(p => p.FromChanged,
|
||||
EventCallback.Factory.Create<DateTime?>(this, v => captured = v)));
|
||||
|
||||
cut.Find("[data-test='sc-from']").Change("");
|
||||
|
||||
Assert.Null(captured);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitialValue_RendersInInput()
|
||||
{
|
||||
var initial = new DateTime(2025, 12, 31, 23, 59, 0);
|
||||
var cut = Render<DateTimeRangeFilter>(ps => ps
|
||||
.Add(p => p.From, initial));
|
||||
|
||||
var value = cut.Find("[data-test='dtr-from']").GetAttribute("value");
|
||||
Assert.Equal("2025-12-31T23:59", value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user