HardwareOrSystemSelectionModal.razor 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. @using System.ComponentModel.DataAnnotations
  2. @using RackPeek.Domain.Persistence
  3. @using RackPeek.Domain.Resources.Hardware
  4. @using RackPeek.Domain.Resources.SystemResources
  5. @inject IResourceCollection Repo
  6. @if (IsOpen)
  7. {
  8. <div class="fixed inset-0 z-50 flex items-center justify-center">
  9. <!-- Backdrop -->
  10. <div class="absolute inset-0 bg-black/70" @onclick="Cancel"></div>
  11. <!-- Modal -->
  12. <div class="relative bg-zinc-900 border border-zinc-800 rounded w-full max-w-lg p-4">
  13. <!-- Header -->
  14. <div class="flex justify-between items-center mb-4">
  15. <div class="text-zinc-100 text-sm font-medium">
  16. @Title
  17. </div>
  18. <button class="text-zinc-400 hover:text-zinc-200" @onclick="Cancel">
  19. </button>
  20. </div>
  21. <EditForm Model="_model" OnValidSubmit="HandleAccept">
  22. <DataAnnotationsValidator />
  23. <div class="space-y-3 text-sm">
  24. <!-- Search -->
  25. <div>
  26. <InputText class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
  27. placeholder="Search hardware or systems…"
  28. Value="@_search"
  29. ValueChanged="OnSearchChanged"
  30. ValueExpression="() => _search"
  31. @oninput="OnSearchInput" />
  32. </div>
  33. <!-- Selection -->
  34. <div>
  35. <InputSelect class="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-zinc-100"
  36. @bind-Value="_model.Value">
  37. <option value="">Select hardware or system…</option>
  38. @foreach (var group in FilteredItems)
  39. {
  40. <optgroup label="@group.Key">
  41. @foreach (var item in group.Value)
  42. {
  43. <option value="@item.Value">@item.Label</option>
  44. }
  45. </optgroup>
  46. }
  47. </InputSelect>
  48. @* Optional helper text *@
  49. @if (!string.IsNullOrWhiteSpace(_model.Value) && _lookup.TryGetValue(_model.Value!, out var selected))
  50. {
  51. <div class="mt-2 text-xs text-zinc-400">
  52. Selected: <span class="text-zinc-200">@selected.Type</span>
  53. <span class="text-zinc-600">•</span>
  54. <span>@selected.Group</span>
  55. </div>
  56. }
  57. </div>
  58. </div>
  59. <!-- Actions -->
  60. <div class="flex justify-end items-center mt-5 gap-2">
  61. <button type="button"
  62. class="px-3 py-1 rounded border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
  63. @onclick="Cancel">
  64. Cancel
  65. </button>
  66. <button type="submit"
  67. class="px-3 py-1 rounded bg-emerald-600 text-black hover:bg-emerald-500"
  68. disabled="@(!CanAccept)">
  69. Accept
  70. </button>
  71. </div>
  72. </EditForm>
  73. </div>
  74. </div>
  75. }
  76. @code {
  77. /* ---------- Parameters ---------- */
  78. [Parameter] public bool IsOpen { get; set; }
  79. [Parameter] public EventCallback<bool> IsOpenChanged { get; set; }
  80. [Parameter] public string Title { get; set; } = "Select hardware or system";
  81. [Parameter] public string? Value { get; set; }
  82. [Parameter] public EventCallback<string?> OnAccept { get; set; }
  83. private SelectionFormModel _model = new();
  84. private string _search = string.Empty;
  85. private List<SelectionItem> _items = new();
  86. private Dictionary<string, SelectionItem> _lookup = new();
  87. private bool CanAccept => !string.IsNullOrWhiteSpace(_model.Value);
  88. /* ---------- Lifecycle ---------- */
  89. protected override async Task OnParametersSetAsync()
  90. {
  91. if (!IsOpen) return;
  92. // Load both sets
  93. var hardware = (await Repo.GetAllOfTypeAsync<Hardware>())
  94. .Select(h => SelectionItem.Hardware(h.Name, h.Kind))
  95. .ToList();
  96. var systems = (await Repo.GetAllOfTypeAsync<SystemResource>())
  97. .Select(s => SelectionItem.System(s.Name, string.IsNullOrWhiteSpace(s.Type) ? "System" : s.Type!))
  98. .ToList();
  99. _items = hardware
  100. .Concat(systems)
  101. .OrderBy(i => i.TypeSort)
  102. .ThenBy(i => i.Group)
  103. .ThenBy(i => i.Label)
  104. .ToList();
  105. _lookup = _items.ToDictionary(i => i.Value, i => i);
  106. _model = new SelectionFormModel { Value = Value };
  107. _search = string.Empty;
  108. }
  109. private IReadOnlyDictionary<string, List<SelectionItem>> FilteredItems =>
  110. _items
  111. .Where(i =>
  112. string.IsNullOrWhiteSpace(_search) ||
  113. i.Label.Contains(_search.Trim(), StringComparison.OrdinalIgnoreCase) ||
  114. i.Group.Contains(_search.Trim(), StringComparison.OrdinalIgnoreCase))
  115. .GroupBy(i => $"{i.Type} — {i.Group}")
  116. .ToDictionary(
  117. g => g.Key,
  118. g => g.OrderBy(i => i.Label).ToList());
  119. private async Task HandleAccept()
  120. {
  121. await OnAccept.InvokeAsync(_model.Value);
  122. await Close();
  123. }
  124. private async Task Cancel() => await Close();
  125. private async Task Close()
  126. {
  127. _model = new SelectionFormModel();
  128. _search = string.Empty;
  129. await IsOpenChanged.InvokeAsync(false);
  130. }
  131. private void OnSearchChanged(string? value) => _search = value ?? string.Empty;
  132. private void OnSearchInput(ChangeEventArgs e)
  133. {
  134. _search = e.Value?.ToString() ?? string.Empty;
  135. if (FilteredItems.SelectMany(g => g.Value).All(i => i.Value != _model.Value))
  136. _model.Value = null;
  137. }
  138. private sealed class SelectionFormModel
  139. {
  140. [Required] public string? Value { get; set; }
  141. }
  142. private sealed record SelectionItem(
  143. string Type,
  144. string Group,
  145. string Label,
  146. string Value,
  147. int TypeSort)
  148. {
  149. public static SelectionItem Hardware(string name, string kind) =>
  150. new("Hardware", kind, name, $"{name}", 0);
  151. public static SelectionItem System(string name, string category) =>
  152. new("System", category, name, $"{name}", 1);
  153. }
  154. }