|
50 | 50 | DateTo="@_dateTo" DateToChanged="OnDateToChanged" Statuses="@_statuses" Categories="@_categories" |
51 | 51 | TotalResults="@((int?)_totalItems)" OnFiltersApplied="OnFiltersApplied" OnFiltersCleared="OnFiltersCleared" /> |
52 | 52 |
|
| 53 | + <!-- Active Label Filters --> |
| 54 | + @if (_labelFilter?.Count > 0) |
| 55 | + { |
| 56 | + <div class="flex flex-wrap items-center gap-2"> |
| 57 | + <span class="text-sm text-gray-700 dark:text-gray-300">Active label filters:</span> |
| 58 | + @foreach (var label in _labelFilter) |
| 59 | + { |
| 60 | + <button type="button" @onclick="() => RemoveLabelFilter(label)" |
| 61 | + class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"> |
| 62 | + @label |
| 63 | + <svg class="h-3 w-3" fill="currentColor" viewBox="0 0 20 20"> |
| 64 | + <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" /> |
| 65 | + </svg> |
| 66 | + </button> |
| 67 | + } |
| 68 | + <button type="button" @onclick="ClearAllLabelFilters" |
| 69 | + class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 underline"> |
| 70 | + Clear all |
| 71 | + </button> |
| 72 | + </div> |
| 73 | + } |
| 74 | + |
53 | 75 | <!-- Quick sort: Top Voted --> |
54 | 76 | <div class="flex items-center gap-2"> |
55 | 77 | <button type="button" @onclick="ToggleTopVoted" |
|
185 | 207 | </time> |
186 | 208 | </div> |
187 | 209 | </div> |
| 210 | + @if (issue.Labels.Count > 0) |
| 211 | + { |
| 212 | + <div class="mt-2 flex flex-wrap gap-1.5"> |
| 213 | + @foreach (var label in issue.Labels) |
| 214 | + { |
| 215 | + <button type="button" @onclick="() => AddLabelFilter(label)" @onclick:stopPropagation="true" |
| 216 | + class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors cursor-pointer" |
| 217 | + title="Filter by this label"> |
| 218 | + @label |
| 219 | + </button> |
| 220 | + } |
| 221 | + </div> |
| 222 | + } |
188 | 223 | </div> |
189 | 224 | </a> |
190 | 225 | </div> |
|
231 | 266 | private string? _categoryFilter; |
232 | 267 | private DateOnly? _dateFrom; |
233 | 268 | private DateOnly? _dateTo; |
| 269 | + private List<string>? _labelFilter; |
234 | 270 |
|
235 | 271 | // Pagination state |
236 | 272 | private int _currentPage = 1; |
|
266 | 302 | !string.IsNullOrWhiteSpace(_statusFilter) || |
267 | 303 | !string.IsNullOrWhiteSpace(_categoryFilter) || |
268 | 304 | _dateFrom.HasValue || |
269 | | - _dateTo.HasValue; |
| 305 | + _dateTo.HasValue || |
| 306 | + (_labelFilter?.Count > 0); |
270 | 307 |
|
271 | 308 | private IEnumerable<IssueDto> DisplayedIssues => |
272 | 309 | _sortByVotes |
|
356 | 393 | { |
357 | 394 | _currentPage = Math.Max(1, pageNumber); |
358 | 395 | } |
| 396 | + |
| 397 | + if (query.TryGetValue("label", out var labels)) |
| 398 | + { |
| 399 | + _labelFilter = labels.ToString() |
| 400 | + .Split(',', StringSplitOptions.RemoveEmptyEntries) |
| 401 | + .Select(l => l.Trim()) |
| 402 | + .Where(l => !string.IsNullOrWhiteSpace(l)) |
| 403 | + .Distinct(StringComparer.OrdinalIgnoreCase) |
| 404 | + .ToList(); |
| 405 | + |
| 406 | + if (_labelFilter.Count == 0) |
| 407 | + { |
| 408 | + _labelFilter = null; |
| 409 | + } |
| 410 | + } |
359 | 411 | } |
360 | 412 |
|
361 | 413 | private void UpdateQueryParameters() |
|
392 | 444 | queryParams["page"] = _currentPage.ToString(); |
393 | 445 | } |
394 | 446 |
|
| 447 | + if (_labelFilter?.Count > 0) |
| 448 | + { |
| 449 | + queryParams["label"] = string.Join(",", _labelFilter); |
| 450 | + } |
| 451 | + |
395 | 452 | var newUrl = QueryHelpers.AddQueryString("/issues", queryParams!); |
396 | 453 | NavigationManager.NavigateTo(newUrl, forceLoad: false, replace: true); |
397 | 454 | } |
|
431 | 488 | CategoryFilter = _categoryFilter, |
432 | 489 | DateFrom = _dateFrom, |
433 | 490 | DateTo = _dateTo, |
| 491 | + LabelFilter = _labelFilter, |
434 | 492 | Page = _currentPage, |
435 | 493 | PageSize = PAGE_SIZE, |
436 | 494 | IncludeArchived = false |
|
503 | 561 | _categoryFilter = null; |
504 | 562 | _dateFrom = null; |
505 | 563 | _dateTo = null; |
| 564 | + _labelFilter = null; |
| 565 | + _currentPage = 1; |
| 566 | + UpdateQueryParameters(); |
| 567 | + await LoadIssuesAsync(); |
| 568 | + } |
| 569 | + |
| 570 | + private async Task AddLabelFilter(string label) |
| 571 | + { |
| 572 | + if (string.IsNullOrWhiteSpace(label)) |
| 573 | + { |
| 574 | + return; |
| 575 | + } |
| 576 | + |
| 577 | + _labelFilter ??= []; |
| 578 | + |
| 579 | + if (!_labelFilter.Contains(label, StringComparer.OrdinalIgnoreCase)) |
| 580 | + { |
| 581 | + _labelFilter.Add(label); |
| 582 | + _currentPage = 1; |
| 583 | + UpdateQueryParameters(); |
| 584 | + await LoadIssuesAsync(); |
| 585 | + } |
| 586 | + } |
| 587 | + |
| 588 | + private async Task RemoveLabelFilter(string label) |
| 589 | + { |
| 590 | + if (_labelFilter is null || string.IsNullOrWhiteSpace(label)) |
| 591 | + { |
| 592 | + return; |
| 593 | + } |
| 594 | + |
| 595 | + _labelFilter.RemoveAll(l => l.Equals(label, StringComparison.OrdinalIgnoreCase)); |
| 596 | + |
| 597 | + if (_labelFilter.Count == 0) |
| 598 | + { |
| 599 | + _labelFilter = null; |
| 600 | + } |
| 601 | + |
| 602 | + _currentPage = 1; |
| 603 | + UpdateQueryParameters(); |
| 604 | + await LoadIssuesAsync(); |
| 605 | + } |
| 606 | + |
| 607 | + private async Task ClearAllLabelFilters() |
| 608 | + { |
| 609 | + _labelFilter = null; |
506 | 610 | _currentPage = 1; |
507 | 611 | UpdateQueryParameters(); |
508 | 612 | await LoadIssuesAsync(); |
|
0 commit comments