Skip to content

Commit 467ccab

Browse files
authored
Merge pull request #144 from Geta/feature/sortable-headers
Add sortable headers feature
2 parents 0f66a17 + 7415b7b commit 467ccab

File tree

14 files changed

+283
-52
lines changed

14 files changed

+283
-52
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
2+
using Microsoft.AspNetCore.Mvc.RazorPages;
3+
4+
namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Base;
5+
6+
public abstract class AbstractSortablePageModel : PageModel
7+
{
8+
public string SortColumn { get; set; }
9+
public SortDirection? SortDirection { get; set; }
10+
11+
public void ApplySort(string sortColumn, SortDirection? sortDirection)
12+
{
13+
SortColumn = sortColumn;
14+
SortDirection = sortDirection;
15+
}
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models
2+
@model Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeaderCell.SortableHeaderCellViewModel
3+
4+
<a href="@Model.GetSortUrl()"
5+
style="white-space: nowrap">
6+
@Model.DisplayName
7+
8+
@if (Model.IsActive() && Model.GetSortDirection() != null)
9+
{
10+
<span data-feather="@(Model.GetSortDirection() == SortDirection.Ascending ? "arrow-up" : "arrow-down")">
11+
</span>
12+
}
13+
</a>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeaderCell;
5+
6+
public class SortableHeaderCellViewComponent : ViewComponent
7+
{
8+
private readonly IHttpContextAccessor _contextAccessor;
9+
10+
public SortableHeaderCellViewComponent(IHttpContextAccessor contextAccessor)
11+
{
12+
_contextAccessor = contextAccessor;
13+
}
14+
15+
public IViewComponentResult Invoke(string key, string displayName)
16+
{
17+
var context = _contextAccessor.HttpContext;
18+
19+
return View(new SortableHeaderCellViewModel
20+
{
21+
QueryString = context?.Request.QueryString.ToString(),
22+
Key = key,
23+
DisplayName = displayName
24+
});
25+
}
26+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System;
2+
using System.Web;
3+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Base;
4+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
5+
6+
namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeaderCell;
7+
8+
public class SortableHeaderCellViewModel
9+
{
10+
public string DisplayName { get; set; }
11+
public string Key { get; set; }
12+
public string QueryString { get; set; }
13+
14+
public SortDirection? GetNextSortDirection()
15+
{
16+
return GetSortDirection() switch
17+
{
18+
null => SortDirection.Ascending,
19+
SortDirection.Ascending => SortDirection.Descending,
20+
SortDirection.Descending => null,
21+
_ => throw new ArgumentOutOfRangeException()
22+
};
23+
}
24+
25+
public string GetSortColumn()
26+
{
27+
if (GetNextSortDirection() == null)
28+
{
29+
return null;
30+
}
31+
32+
return Key;
33+
}
34+
35+
public string GetSortUrl()
36+
{
37+
var qs = HttpUtility
38+
.ParseQueryString(QueryString);
39+
40+
qs[nameof(AbstractSortablePageModel.SortColumn)] = GetSortColumn();
41+
qs[nameof(AbstractSortablePageModel.SortDirection)] = GetNextSortDirection().ToString();
42+
43+
return $"?{qs}";
44+
}
45+
46+
public bool IsActive()
47+
{
48+
var sortColumn = HttpUtility
49+
.ParseQueryString(QueryString)
50+
.Get(nameof(AbstractSortablePageModel.SortColumn));
51+
52+
return !string.IsNullOrEmpty(sortColumn) && sortColumn == Key;
53+
}
54+
55+
public SortDirection? GetSortDirection()
56+
{
57+
var sortDirection = HttpUtility
58+
.ParseQueryString(QueryString)
59+
.Get(nameof(AbstractSortablePageModel.SortDirection));
60+
61+
return Enum.TryParse(sortDirection, out SortDirection sort) ? sort : null;
62+
}
63+
}
Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
@page "{handler?}"
2+
@using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeaderCell
3+
@using Geta.NotFoundHandler.Core.Providers.RegexRedirects
4+
@using Microsoft.AspNetCore.Mvc.TagHelpers
25
@model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.DeletedModel
36

47
@await Component.InvokeAsync("Card", new { message = Model.Message })
@@ -7,42 +10,44 @@
710
<div class="table-responsive mt-3">
811
<table class="table table-hover table-sm" aria-label="Deleted redirects">
912
<thead>
10-
<tr>
11-
<th>URL</th>
12-
<th class="col-1"></th>
13-
</tr>
13+
<tr>
14+
<th>
15+
<vc:sortable-header-cell key="@nameof(RegexRedirect.OldUrlRegex)" display-name="Old URL Regex"/>
16+
</th>
17+
<th class="col-1"></th>
18+
</tr>
1419
</thead>
1520
<tbody>
16-
<tr>
17-
<td>
18-
<input type="text" class="form-control" asp-for="DeletedRedirect.OldUrl">
19-
<span asp-validation-for="DeletedRedirect.OldUrl" class="text-danger"></span>
20-
</td>
21+
<tr>
22+
<td>
23+
<input type="text" class="form-control" asp-for="DeletedRedirect.OldUrl">
24+
<span asp-validation-for="DeletedRedirect.OldUrl" class="text-danger"></span>
25+
</td>
26+
<td>
27+
<div class="d-grid gap-2">
28+
<button type="submit" class="btn btn-primary"
29+
asp-page-handler="create">
30+
<span data-feather="plus"></span> add
31+
</button>
32+
</div>
33+
</td>
34+
</tr>
35+
@foreach (var item in Model.Items)
36+
{
37+
<tr class="align-middle">
38+
<td>@item.OldUrl</td>
2139
<td>
2240
<div class="d-grid gap-2">
23-
<button type="submit" class="btn btn-primary"
24-
asp-page-handler="create">
25-
<span data-feather="plus"></span> add
41+
<button type="submit" class="btn btn-danger"
42+
asp-page-handler="delete" asp-route-oldurl="@item.OldUrl">
43+
<span data-feather="trash-2"></span> delete
2644
</button>
2745
</div>
2846
</td>
2947
</tr>
30-
@foreach (var item in Model.Items)
31-
{
32-
<tr class="align-middle">
33-
<td>@item.OldUrl</td>
34-
<td>
35-
<div class="d-grid gap-2">
36-
<button type="submit" class="btn btn-danger"
37-
asp-page-handler="delete" asp-route-oldurl="@item.OldUrl">
38-
<span data-feather="trash-2"></span> delete
39-
</button>
40-
</div>
41-
</td>
42-
</tr>
43-
}
48+
}
4449
</tbody>
4550
</table>
4651
@await Component.InvokeAsync(typeof(Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewComponent), new { Model.Items })
4752
</div>
48-
</form>
53+
</form>

src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
using System.Collections.Generic;
12
using System.Linq;
3+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Base;
4+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Extensions;
5+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
26
using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models;
37
using Geta.NotFoundHandler.Core.Redirects;
48
using Geta.NotFoundHandler.Infrastructure;
59
using Microsoft.AspNetCore.Authorization;
610
using Microsoft.AspNetCore.Mvc;
7-
using Microsoft.AspNetCore.Mvc.RazorPages;
811
using X.PagedList;
912

1013
namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin;
1114

1215
[Authorize(Constants.PolicyName)]
13-
public class DeletedModel : PageModel
16+
public class DeletedModel : AbstractSortablePageModel
1417
{
1518
private readonly IRedirectsService _redirectsService;
1619

@@ -29,8 +32,10 @@ public DeletedModel(IRedirectsService redirectsService)
2932
[BindProperty(SupportsGet = true)]
3033
public Paging Paging { get; set; }
3134

32-
public void OnGet()
35+
public void OnGet(string sortColumn, SortDirection? sortDirection)
3336
{
37+
ApplySort(sortColumn, sortDirection);
38+
3439
Load();
3540
}
3641

@@ -55,9 +60,16 @@ public IActionResult OnPostDelete(string oldUrl)
5560

5661
private void Load()
5762
{
58-
var items = _redirectsService.GetDeleted().ToPagedList(Paging.PageNumber, Paging.PageSize);
63+
var items = FindRedirects().ToPagedList(Paging.PageNumber, Paging.PageSize);
5964
Message =
6065
$"There are currently {items.TotalItemCount} URLs that return a Deleted response. This tells crawlers to remove these URLs from their index.";
6166
Items = items;
6267
}
68+
69+
private IEnumerable<CustomRedirect> FindRedirects()
70+
{
71+
return _redirectsService
72+
.GetDeleted()
73+
.Sort(SortColumn, SortDirection);
74+
}
6375
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Reflection;
4+
using System.Text.RegularExpressions;
5+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
6+
7+
namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Extensions;
8+
9+
public static class EnumerableExtensions
10+
{
11+
public static IEnumerable<T> Sort<T>(this IEnumerable<T> list, string propertyName, SortDirection? sortDirection)
12+
{
13+
if (!string.IsNullOrEmpty(propertyName))
14+
{
15+
var prop = typeof(T).GetProperty(propertyName);
16+
17+
if (prop != null && sortDirection != null)
18+
{
19+
if (sortDirection == SortDirection.Ascending)
20+
{
21+
list = list.OrderBy(x => GetValue(prop, x));
22+
}
23+
else
24+
{
25+
list = list.OrderByDescending(x => GetValue(prop, x));
26+
}
27+
}
28+
}
29+
30+
return list;
31+
}
32+
33+
private static object GetValue<T>(PropertyInfo prop, T element)
34+
{
35+
var value = prop.GetValue(element);
36+
37+
// Value should be IComparable
38+
if (value is Regex regex)
39+
{
40+
return regex.ToString();
41+
}
42+
43+
return value;
44+
}
45+
}

src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@page "{handler?}"
2+
@using Geta.NotFoundHandler.Core.Redirects
3+
@using Microsoft.AspNetCore.Mvc.TagHelpers
24
@model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.IgnoredModel
35

46
@await Component.InvokeAsync("Card", new { message = Model.Message })
@@ -8,7 +10,9 @@
810
<table class="table table-hover table-sm" aria-label="Ignored redirects">
911
<thead>
1012
<tr>
11-
<th>URL</th>
13+
<th>
14+
<vc:sortable-header-cell key="@nameof(CustomRedirect.OldUrl)" display-name="URL"/>
15+
</th>
1216
<th class="col-1"></th>
1317
</tr>
1418
</thead>

src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
using System.Collections.Generic;
12
using System.Linq;
3+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Base;
4+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Extensions;
5+
using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
26
using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models;
37
using Geta.NotFoundHandler.Core.Redirects;
48
using Geta.NotFoundHandler.Infrastructure;
59
using Microsoft.AspNetCore.Authorization;
610
using Microsoft.AspNetCore.Mvc;
7-
using Microsoft.AspNetCore.Mvc.RazorPages;
811
using X.PagedList;
912

1013
namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin;
1114

1215
[Authorize(Constants.PolicyName)]
13-
public class IgnoredModel : PageModel
16+
public class IgnoredModel : AbstractSortablePageModel
1417
{
1518
private readonly IRedirectsService _redirectsService;
1619

@@ -26,8 +29,10 @@ public IgnoredModel(IRedirectsService redirectsService)
2629
[BindProperty(SupportsGet = true)]
2730
public Paging Paging { get; set; }
2831

29-
public void OnGet()
32+
public void OnGet(string sortColumn, SortDirection? sortDirection)
3033
{
34+
ApplySort(sortColumn, sortDirection);
35+
3136
Load();
3237
}
3338

@@ -40,8 +45,15 @@ public IActionResult OnPostUnignore(string oldUrl)
4045

4146
private void Load()
4247
{
43-
var items = _redirectsService.GetIgnored().ToPagedList(Paging.PageNumber, Paging.PageSize);
48+
var items = FindRedirects().ToPagedList(Paging.PageNumber, Paging.PageSize);
4449
Message = $"There are currently {items.TotalItemCount} ignored suggestions stored.";
4550
Items = items;
4651
}
52+
53+
private IEnumerable<CustomRedirect> FindRedirects()
54+
{
55+
return _redirectsService
56+
.GetIgnored()
57+
.Sort(SortColumn, SortDirection);
58+
}
4759
}

src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Index.cshtml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,18 @@
2727
<table class="table table-hover table-sm" aria-label="Redirects">
2828
<thead>
2929
<tr>
30-
<th>Old URL</th>
31-
<th>New URL</th>
32-
<th class="col-1 text-center">Wildcard</th>
33-
<th class="col-1">Redirect Type</th>
30+
<th>
31+
<vc:sortable-header-cell key="@nameof(CustomRedirect.NewUrl)" display-name="New URL"/>
32+
</th>
33+
<th>
34+
<vc:sortable-header-cell key="@nameof(CustomRedirect.OldUrl)" display-name="Old URL"/>
35+
</th>
36+
<th class="col-1 text-center">
37+
<vc:sortable-header-cell key="@nameof(CustomRedirect.WildCardSkipAppend)" display-name="Wildcard"/>
38+
</th>
39+
<th class="col-1">
40+
<vc:sortable-header-cell key="@nameof(CustomRedirect.RedirectType)" display-name="Redirect Type"/>
41+
</th>
3442
<th class="col-1"></th>
3543
</tr>
3644
</thead>

0 commit comments

Comments
 (0)