diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Administer.cshtml.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Administer.cshtml.cs index 9cacff9..29750e8 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Administer.cshtml.cs +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Administer.cshtml.cs @@ -49,7 +49,8 @@ public IActionResult OnPostDeleteAllIgnoredSuggestions() Message = $"All {count} ignored suggestions permanently removed"; CardType = CardType.Success; - return RedirectToPage(new { + return RedirectToPage(new + { Message, CardType }); @@ -168,7 +169,7 @@ public IActionResult OnPostImportDeletedRedirects() public IActionResult OnPostExportRedirects() { - var redirects = _redirectsService.GetSaved().ToList(); + var redirects = _redirectsService.GetRedirects(new Data.QueryParams() { QueryState = RedirectState.Saved }).Redirects.ToList(); var document = _redirectsXmlParser.Export(redirects); var memoryStream = new MemoryStream(); @@ -208,5 +209,6 @@ private CustomRedirectCollection ReadDeletedRedirectsFromImportFile() public class DeleteSuggestionsModel { public int MaxErrors { get; set; } = 5; + public int MinimumDays { get; set; } = 30; } diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/Default.cshtml b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/Default.cshtml index d6f6348..75e6d0f 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/Default.cshtml +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/Default.cshtml @@ -3,19 +3,23 @@ \ No newline at end of file diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewComponent.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewComponent.cs index eb3dd9c..cea3235 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewComponent.cs +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewComponent.cs @@ -1,6 +1,6 @@ +using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using X.PagedList; namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager { @@ -13,15 +13,16 @@ public PagerViewComponent(IHttpContextAccessor contextAccessor) _contextAccessor = contextAccessor; } - public IViewComponentResult Invoke(IPagedList items) + public IViewComponentResult Invoke(int page, int pageSize, int totalCount) { var context = _contextAccessor.HttpContext; + var pageCount = pageSize is int ps && ps > 0 ? (int)Math.Ceiling((decimal)totalCount / ps) : 1; return View(new PagerViewModel { - HasPreviousPage = items.HasPreviousPage, - HasNextPage = items.HasNextPage, - PageNumber = items.PageNumber, - PageCount = items.PageCount, + HasPreviousPage = page > 1, + HasNextPage = pageCount > page, + PageNumber = page, + PageCount = pageCount, QueryString = context.Request.QueryString.ToString() }); } diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewModel.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewModel.cs index 1ec85ce..cff0d84 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewModel.cs +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewModel.cs @@ -1,20 +1,15 @@ -using System.Web; - namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager { public class PagerViewModel { public bool HasPreviousPage { get; set; } + public bool HasNextPage { get; set; } + public int PageNumber { get; set; } + public int PageCount { get; set; } - public string QueryString { get; set; } - public string PageUrl(int page) - { - var qs = HttpUtility.ParseQueryString(QueryString); - qs["page"] = page.ToString(); - return $"?{qs}"; - } + public string QueryString { get; set; } } } diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeader/Default.cshtml b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeader/Default.cshtml new file mode 100644 index 0000000..4e4138b --- /dev/null +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeader/Default.cshtml @@ -0,0 +1,24 @@ +@using System.Data.SqlClient; +@model Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeader.SortableHeaderViewModel + +@{ + var sortBy = Model.InternalName; + var isCurrent = Model.Params.SortBy == Model.InternalName; + var nextSortDirection = isCurrent && Model.Params.SortDirection == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending; +} + + + + \ No newline at end of file diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeader/SortableHeaderViewComponent.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeader/SortableHeaderViewComponent.cs new file mode 100644 index 0000000..af2a6c8 --- /dev/null +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeader/SortableHeaderViewComponent.cs @@ -0,0 +1,13 @@ +using Geta.NotFoundHandler.Data; +using Microsoft.AspNetCore.Mvc; + +namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeader +{ + public class SortableHeaderViewComponent : ViewComponent + { + public IViewComponentResult Invoke(string displayName, string internalName, QueryParams @params, string additionalClass = null) + { + return View(new SortableHeaderViewModel(displayName, internalName, @params, additionalClass)); + } + } +} diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeader/SortableHeaderViewModel.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeader/SortableHeaderViewModel.cs new file mode 100644 index 0000000..8a29e07 --- /dev/null +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeader/SortableHeaderViewModel.cs @@ -0,0 +1,23 @@ +using Geta.NotFoundHandler.Data; + +namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeader +{ + public class SortableHeaderViewModel + { + public SortableHeaderViewModel(string displayName, string internalName, QueryParams @params, string additionalClass = null) + { + DisplayName = displayName; + InternalName = internalName; + Params = @params; + AdditionalClass = additionalClass; + } + + public string DisplayName { get; init; } + + public string InternalName { get; init; } + + public QueryParams Params { get; init; } + + public string AdditionalClass { get; } + } +} diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml index c247027..667ff2e 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml @@ -1,48 +1,52 @@ @page "{handler?}" -@model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.DeletedModel +@model DeletedModel @await Component.InvokeAsync("Card", new { message = Model.Message }) +@await Component.InvokeAsync("Card", new { message = Model.OperationMessage, cardType = CardType.Success }) -
-
- - - - - - - - - - + + @Html.HiddenForQueryParams() + + +
+
URL
- - -
+ + + @await Component.InvokeAsync("SortableHeader", new { DisplayName = "URL", InternalName = nameof(CustomRedirect.OldUrl), Model.Params }) + + + + + @Html.HiddenForQueryParams() + + + + + + @foreach (var item in Model.Results.Redirects) + { + + - @foreach (var item in Model.Items) - { - - - - - } - -
+ + + +
+ +
+
@item.OldUrl
-
@item.OldUrl -
- -
-
- @await Component.InvokeAsync(typeof(Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewComponent), new { Model.Items }) -
- + } + + + + @await Component.InvokeAsync(typeof(Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewComponent), new { Model.Params.Page, Model.Params.PageSize, Model.Results.TotalCount }) + \ No newline at end of file diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml.cs index 6cd5cbe..9eb79d6 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml.cs +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml.cs @@ -1,63 +1,39 @@ -using System.Linq; using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models; using Geta.NotFoundHandler.Core.Redirects; using Geta.NotFoundHandler.Infrastructure; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using X.PagedList; namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin; [Authorize(Constants.PolicyName)] -public class DeletedModel : PageModel +public class DeletedModel : BaseCustomRedirectPageModel { - private readonly IRedirectsService _redirectsService; - - public DeletedModel(IRedirectsService redirectsService) + public DeletedModel(IRedirectsService redirectsService) : base(redirectsService, RedirectState.Deleted, + "There are currently {0} URLs that return a Deleted response.This tells crawlers to remove these URLs from their index. ") { - _redirectsService = redirectsService; } - public string Message { get; set; } - - public IPagedList Items { get; set; } = Enumerable.Empty().ToPagedList(); - [BindProperty] public DeletedRedirectModel DeletedRedirect { get; set; } - [BindProperty(SupportsGet = true)] - public Paging Paging { get; set; } - - public void OnGet() - { - Load(); - } - public IActionResult OnPostCreate() { - if (!ModelState.IsValid) + if (ModelState.IsValid) { - Load(); - return Page(); + RedirectsService.AddDeletedRedirect(DeletedRedirect.OldUrl); + OperationMessage = $"Added delete redirect for {DeletedRedirect.OldUrl}"; + DeletedRedirect = new DeletedRedirectModel(); + return LoadPage(true); } - _redirectsService.AddDeletedRedirect(DeletedRedirect.OldUrl); - - return RedirectToPage(); + return LoadPage(); } public IActionResult OnPostDelete(string oldUrl) { - _redirectsService.DeleteByOldUrl(oldUrl); - return RedirectToPage(); - } - - private void Load() - { - var items = _redirectsService.GetDeleted().ToPagedList(Paging.PageNumber, Paging.PageSize); - Message = - $"There are currently {items.TotalItemCount} URLs that return a Deleted response. This tells crawlers to remove these URLs from their index."; - Items = items; + RedirectsService.DeleteByOldUrl(oldUrl); + OperationMessage = $"Removed {oldUrl}"; + return LoadPage(true); } } diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml index 7bdd6aa..2ff0c78 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml @@ -1,34 +1,40 @@ @page "{handler?}" -@model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.IgnoredModel +@model IgnoredModel @await Component.InvokeAsync("Card", new { message = Model.Message }) +@await Component.InvokeAsync("Card", new { message = Model.OperationMessage, cardType = CardType.Success }) -
-
- - + + + @Html.HiddenForQueryParams() + + +
+
+ - + @await Component.InvokeAsync("SortableHeader", new { DisplayName = "URL", InternalName = nameof(CustomRedirect.OldUrl), Model.Params }) - - - @foreach (var item in Model.Items) - { - - - - - } - -
URL
@item.OldUrl -
- -
-
- @await Component.InvokeAsync(typeof(Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewComponent), new { Model.Items }) -
- \ No newline at end of file + +
+ @Html.HiddenForQueryParams() + + @foreach (var item in Model.Results.Redirects) + { + + @item.OldUrl + +
+ +
+ + + } + +
+ + @await Component.InvokeAsync(typeof(Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewComponent), new { Model.Params.Page, Model.Params.PageSize, Model.Results.TotalCount }) + \ No newline at end of file diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml.cs index f7a6cf4..3981504 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml.cs +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml.cs @@ -1,47 +1,22 @@ -using System.Linq; -using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models; using Geta.NotFoundHandler.Core.Redirects; using Geta.NotFoundHandler.Infrastructure; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using X.PagedList; namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin; [Authorize(Constants.PolicyName)] -public class IgnoredModel : PageModel +public class IgnoredModel : BaseCustomRedirectPageModel { - private readonly IRedirectsService _redirectsService; - - public IgnoredModel(IRedirectsService redirectsService) + public IgnoredModel(IRedirectsService redirectsService) : base(redirectsService, RedirectState.Ignored, + "There are currently {0} ignored suggestions stored. ") { - _redirectsService = redirectsService; - } - - public string Message { get; set; } - - public IPagedList Items { get; set; } = Enumerable.Empty().ToPagedList(); - - [BindProperty(SupportsGet = true)] - public Paging Paging { get; set; } - - public void OnGet() - { - Load(); } public IActionResult OnPostUnignore(string oldUrl) { - _redirectsService.DeleteByOldUrl(oldUrl); - - return RedirectToPage(); - } - - private void Load() - { - var items = _redirectsService.GetIgnored().ToPagedList(Paging.PageNumber, Paging.PageSize); - Message = $"There are currently {items.TotalItemCount} ignored suggestions stored."; - Items = items; + RedirectsService.DeleteByOldUrl(oldUrl); + OperationMessage = $"Removed {oldUrl} from the ignore list"; + return LoadPage(true); } } diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Index.cshtml b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Index.cshtml index 9f5ac4b..eb21232 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Index.cshtml +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Index.cshtml @@ -1,12 +1,12 @@ @page "{handler?}" -@using Geta.NotFoundHandler.Core.Redirects -@model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.IndexModel +@model IndexModel @await Component.InvokeAsync("Card", new { message = Model.Message }) +@await Component.InvokeAsync("Card", new { message = Model.OperationMessage, cardType = CardType.Success }) -
+
- +
+ @Html.HiddenForQueryParams()
-
-
- - +
+
+ - - - - + @await Component.InvokeAsync("SortableHeader", new { DisplayName = "Old URL", InternalName = nameof(CustomRedirect.OldUrl), Model.Params }) + @await Component.InvokeAsync("SortableHeader", new { DisplayName = "New URL", InternalName = nameof(CustomRedirect.NewUrl), Model.Params }) + @await Component.InvokeAsync("SortableHeader", new { DisplayName = "Wildcard", InternalName = nameof(CustomRedirect.WildCardSkipAppend), Model.Params, AdditionalClass = "col-1 text-center" }) + @await Component.InvokeAsync("SortableHeader", new { DisplayName = "Redirect type", InternalName = nameof(CustomRedirect.RedirectType), Model.Params, AdditionalClass = "col-1" }) - - + + + @Html.HiddenForQueryParams() + - @foreach (var item in Model.Items) + @foreach (var item in Model.Results.Redirects) { @@ -69,16 +70,15 @@ } - -
Old URLNew URLWildcardRedirect Type
@@ -51,14 +53,13 @@
-
@item.OldUrl@item.RedirectType
-
- @await Component.InvokeAsync(typeof(Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewComponent), new { Model.Items }) -
-
\ No newline at end of file + + + + @await Component.InvokeAsync(typeof(Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewComponent), new { Model.Params.Page, Model.Params.PageSize, Model.Results.TotalCount }) + \ No newline at end of file diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Index.cshtml.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Index.cshtml.cs index c4aa129..ca7db9d 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Index.cshtml.cs +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Index.cshtml.cs @@ -1,78 +1,44 @@ -using System.Collections.Generic; -using System.Linq; using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models; using Geta.NotFoundHandler.Core.Redirects; using Geta.NotFoundHandler.Infrastructure; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using X.PagedList; namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin; [Authorize(Constants.PolicyName)] -public class IndexModel : PageModel +public class IndexModel : BaseCustomRedirectPageModel { - private readonly IRedirectsService _redirectsService; - - public IndexModel(IRedirectsService redirectsService) + public IndexModel(IRedirectsService redirectsService) : base(redirectsService, RedirectState.Saved, + "There are currently stored {0} custom redirects. ") { - _redirectsService = redirectsService; } - public string Message { get; set; } - - public IPagedList Items { get; set; } = Enumerable.Empty().ToPagedList(); - [BindProperty] public RedirectModel CustomRedirect { get; set; } - [BindProperty(SupportsGet = true)] - public Paging Paging { get; set; } - - [BindProperty(SupportsGet = true, Name = "q")] - public string Query { get; set; } - - public bool HasQuery => !string.IsNullOrEmpty(Query); - - public void OnGet() - { - Load(); - } - public IActionResult OnPostCreate() { - if (!ModelState.IsValid) + if (ModelState.IsValid) { - Load(); - return Page(); + var customRedirect = new CustomRedirect(CustomRedirect.OldUrl, + CustomRedirect.NewUrl, + CustomRedirect.WildCardSkipAppend, + CustomRedirect.RedirectType); + + RedirectsService.AddOrUpdate(customRedirect); + OperationMessage = $"Added/updated redirect from {CustomRedirect.OldUrl} to {CustomRedirect.NewUrl}"; + CustomRedirect = new RedirectModel(); + return LoadPage(true); } - var customRedirect = new CustomRedirect(CustomRedirect.OldUrl, - CustomRedirect.NewUrl, - CustomRedirect.WildCardSkipAppend, - CustomRedirect.RedirectType); - - _redirectsService.AddOrUpdate(customRedirect); - - return RedirectToPage(); + return LoadPage(); } public IActionResult OnPostDelete(string oldUrl) { - _redirectsService.DeleteByOldUrl(oldUrl); - return RedirectToPage(); - } - - private void Load() - { - var items = FindRedirects().ToPagedList(Paging.PageNumber, Paging.PageSize); - Message = $"There are currently stored {items.TotalItemCount} custom redirects."; - Items = items; - } - - private IEnumerable FindRedirects() - { - return HasQuery ? _redirectsService.Search(Query) : _redirectsService.GetSaved(); + RedirectsService.DeleteByOldUrl(oldUrl); + OperationMessage = $"Removed redirect for {oldUrl}"; + return LoadPage(true); } } diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/BaseCustomRedirectPageModel.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/BaseCustomRedirectPageModel.cs new file mode 100644 index 0000000..c9c9b42 --- /dev/null +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/BaseCustomRedirectPageModel.cs @@ -0,0 +1,35 @@ +using Geta.NotFoundHandler.Core; +using Geta.NotFoundHandler.Core.Redirects; + +namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin; + +public abstract class BaseCustomRedirectPageModel : BaseRedirectPageModel +{ + private readonly RedirectState _redirectState; + private readonly string _messageFormat; + + protected BaseCustomRedirectPageModel(IRedirectsService redirectsService, RedirectState redirectState, string messageFormat) + { + RedirectsService = redirectsService; + _redirectState = redirectState; + _messageFormat = messageFormat; + } + + protected IRedirectsService RedirectsService { get; } + + public CustomRedirectsResult Results { get; set; } + + protected override void Load() + { + Params.SortBy ??= nameof(CustomRedirect.OldUrl); + Params.QueryState = _redirectState; + Params.PageSize ??= 50; + var results = RedirectsService.GetRedirects(Params); + Message = string.Format(_messageFormat, results.UnfilteredCount); + if (results.TotalCount < results.UnfilteredCount) + { + Message += $"Current filter gives {results.TotalCount}."; + } + Results = results; + } +} diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/BaseRedirectPageModel.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/BaseRedirectPageModel.cs new file mode 100644 index 0000000..4967c8e --- /dev/null +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/BaseRedirectPageModel.cs @@ -0,0 +1,33 @@ +using Geta.NotFoundHandler.Data; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin; + +public abstract class BaseRedirectPageModel : PageModel +{ + public string Message { get; set; } + + public string OperationMessage { get; set; } + + [BindProperty(SupportsGet = true)] + public QueryParams Params { get; set; } + + public void OnGet() + { + Load(); + } + + protected virtual IActionResult LoadPage(bool clearModelState = false) + { + if (clearModelState) + { + ModelState.Clear(); + } + + Load(); + return Page(); + } + + protected abstract void Load(); +} diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/HtmlHelperExtensions.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/HtmlHelperExtensions.cs new file mode 100644 index 0000000..eae9ee0 --- /dev/null +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/HtmlHelperExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Linq.Expressions; +using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; + +namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Infrastructure +{ + public static class HtmlHelperExtensions + { + private static readonly ConcurrentDictionary<(Type, string), string> QueryNameDictionary = new(); + + public static IHtmlContent HiddenForQueryParams(this IHtmlHelper htmlHelper) + where T : BaseRedirectPageModel + { + var htmlContentBuilder = new HtmlContentBuilder(); + htmlContentBuilder.AppendHiddenInputFor(htmlHelper, x => x.Params.QueryText); + htmlContentBuilder.AppendHiddenInputFor(htmlHelper, x => x.Params.Page); + htmlContentBuilder.AppendHiddenInputFor(htmlHelper, x => x.Params.SortBy); + htmlContentBuilder.AppendHiddenInputFor(htmlHelper, x => x.Params.SortDirection); + return htmlContentBuilder; + } + + public static string NameQueryFor(this IHtmlHelper html, Expression> expression) + { + ArgumentNullException.ThrowIfNull(html); + ArgumentNullException.ThrowIfNull(expression); + + return QueryNameDictionary.GetOrAdd((typeof(TModel), expression.ToString()), _ => + { + var modelExpression = html.ViewContext.HttpContext.RequestServices.GetRequiredService().CreateModelExpression(html.ViewData, expression); + + if (modelExpression.Metadata is DefaultModelMetadata defaultMetadata && defaultMetadata.Attributes.Attributes.OfType().FirstOrDefault() is IModelNameProvider attribute + && attribute.Name is string queryName && !string.IsNullOrEmpty(queryName)) + { + return queryName; + } + + return html.NameFor(expression); + }); + } + + private static void AppendHiddenInputFor(this IHtmlContentBuilder htmlContentBuilder, IHtmlHelper htmlHelper, Expression> expression) + { + htmlContentBuilder.AppendHtml(htmlHelper.Hidden(htmlHelper.NameQueryFor(expression), htmlHelper.DisplayTextFor(expression))); + } + } +} diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/Paging.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/Paging.cs deleted file mode 100644 index 3635713..0000000 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/Paging.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models -{ - public class Paging - { - public const int DefaultPageSize = 50; - - [FromQuery(Name = "page")] - public int PageNumber { get; set; } = 1; - - [FromQuery(Name = "page-size")] - public int PageSize { get; set; } = DefaultPageSize; - } -} diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Suggestions.cshtml b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Suggestions.cshtml index 900686a..5419073 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Suggestions.cshtml +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Suggestions.cshtml @@ -1,107 +1,105 @@ @page "{handler?}" -@model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.SuggestionsModel +@model SuggestionsModel @await Component.InvokeAsync("Card", new { message = Model.Message }) +@await Component.InvokeAsync("Card", new { message = Model.OperationMessage, cardType = CardType.Success }) + +
+ @Html.HiddenForQueryParams() +
- - - - - - - + + @await Component.InvokeAsync("SortableHeader", new { DisplayName = "Old URL", InternalName = nameof(SuggestionRedirectModel.OldUrl), Model.Params }) + + + + + + + @Html.HiddenForQueryParams() - @for (var i = 0; i < Model.Items.Count; i++) - { - var item = Model.Items[i]; - var formId = $"redirect{i}"; - - + - - - + + + - + - - } + + + + } +
Old URLNew URL
New URL
- + @for (var i = 0; i < Model.Items.Count; i++) + { + var item = Model.Items[i]; +
- - @item.OldUrl (@item.Count errors) - - - - -
- -
-
- @{ - var referersModalId = $"referers{i}"; - var modalTitleId = $"modalTitle{i}"; - } - + + + +
+ +
+
+ @{ + var referersModalId = $"referers{i}"; + var modalTitleId = $"modalTitle{i}"; + } + - -
- -
-
- @{ - var ignoreFormId = $"ignore{i}"; - } -
-
- +
+
+
+ - -
-
- @await Component.InvokeAsync(typeof(Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewComponent), new { Model.Items }) + @await Component.InvokeAsync(typeof(Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewComponent), new { Model.Params.Page, Model.Params.PageSize, Model.Results.TotalCount })
\ No newline at end of file diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Suggestions.cshtml.cs b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Suggestions.cshtml.cs index 3282edf..a96ac9c 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Suggestions.cshtml.cs +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Suggestions.cshtml.cs @@ -1,17 +1,16 @@ using System.Collections.Generic; using System.Linq; using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models; +using Geta.NotFoundHandler.Core; using Geta.NotFoundHandler.Core.Suggestions; using Geta.NotFoundHandler.Infrastructure; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using X.PagedList; namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin; [Authorize(Constants.PolicyName)] -public class SuggestionsModel : PageModel +public class SuggestionsModel : BaseRedirectPageModel { private readonly ISuggestionService _suggestionService; @@ -20,51 +19,47 @@ public SuggestionsModel(ISuggestionService suggestionService) _suggestionService = suggestionService; } - public string Message { get; set; } + public SuggestionRedirectsResult Results { get; set; } - public IPagedList Items { get; set; } = Enumerable.Empty().ToPagedList(); + public IList Items { get; set; } = Enumerable.Empty().ToList(); - [BindProperty(SupportsGet = true)] - public Paging Paging { get; set; } - - public void OnGet() - { - Load(); - } - - public IActionResult OnPostCreate(Dictionary items) + public IActionResult OnPostCreate(Dictionary items, int index) { - if (!ModelState.IsValid) + ModelState.Clear(); + if (items.ContainsKey(index)) { - Load(); - return Page(); + var item = items[index]; + if (TryValidateModel(item, $"{nameof(items)}[{index}]")) + { + _suggestionService.AddRedirect(new SuggestionRedirect(item.OldUrl, item.NewUrl)); + OperationMessage = $"Added redirect from {item.OldUrl} to {item.NewUrl}"; + } } - var item = items.First().Value; - - _suggestionService.AddRedirect(new SuggestionRedirect(item.OldUrl, item.NewUrl)); - - return RedirectToPage(); + return LoadPage(); } public IActionResult OnPostIgnore(string oldUrl) { _suggestionService.IgnoreSuggestion(oldUrl); - - return RedirectToPage(); + OperationMessage = $"Added {oldUrl} to ignore list"; + return LoadPage(true); } - private void Load() + protected override void Load() { - var summaries = _suggestionService.GetSummaries(Paging.PageNumber, Paging.PageSize); - var redirectModels = summaries.Select(x => new SuggestionRedirectModel + Params.SortBy ??= nameof(SuggestionRedirectModel.OldUrl); + Params.PageSize ??= 50; + var results = _suggestionService.GetSummaries(Params); + var redirectModels = results.Suggestions.Select(x => new SuggestionRedirectModel { OldUrl = x.OldUrl, Count = x.Count, Referers = x.Referers }); - Message = $"Based on the logged 404 errors, there are {summaries.TotalItemCount} custom redirect suggestions."; - Items = new StaticPagedList(redirectModels, summaries); + Message = $"Based on the logged 404 errors, there are {results.TotalCount} custom redirect suggestions."; + Results = results; + Items = redirectModels.ToList(); } } diff --git a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/_ViewImports.cshtml b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/_ViewImports.cshtml index e60fc9a..7b204be 100644 --- a/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/_ViewImports.cshtml +++ b/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/_ViewImports.cshtml @@ -1,2 +1,7 @@ -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@addTagHelper *, Geta.NotFoundHandler.Admin \ No newline at end of file +@using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Infrastructure; +@using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Card; +@using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models; +@using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin; +@using Geta.NotFoundHandler.Core.Redirects +@addTagHelper *, Geta.NotFoundHandler.Admin +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/src/Geta.NotFoundHandler.Admin/Geta.NotFoundHandler.Admin.csproj b/src/Geta.NotFoundHandler.Admin/Geta.NotFoundHandler.Admin.csproj index 5daceb3..6372092 100644 --- a/src/Geta.NotFoundHandler.Admin/Geta.NotFoundHandler.Admin.csproj +++ b/src/Geta.NotFoundHandler.Admin/Geta.NotFoundHandler.Admin.csproj @@ -23,10 +23,6 @@ - - - - diff --git a/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/css/dashboard.css b/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/css/dashboard.css index 7a46c83..07dda8b 100644 --- a/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/css/dashboard.css +++ b/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/css/dashboard.css @@ -13,56 +13,56 @@ */ .sidebar { - position: fixed; - top: 0; - /* rtl:raw: + position: fixed; + top: 0; + /* rtl:raw: right: 0; */ - bottom: 0; - /* rtl:remove */ - left: 0; - z-index: 100; /* Behind the navbar */ - padding: 48px 0 0; /* Height of navbar */ - box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); + bottom: 0; + /* rtl:remove */ + left: 0; + z-index: 100; /* Behind the navbar */ + padding: 48px 0 0; /* Height of navbar */ + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); } @media (max-width: 767.98px) { - .sidebar { - top: 5rem; - } + .sidebar { + top: 5rem; + } } .sidebar-sticky { - position: relative; - top: 0; - height: calc(100vh - 48px); - padding-top: .5rem; - overflow-x: hidden; - overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ + position: relative; + top: 0; + height: calc(100vh - 48px); + padding-top: .5rem; + overflow-x: hidden; + overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ } .sidebar .nav-link { - font-weight: 500; - color: #333; + font-weight: 500; + color: #333; } .sidebar .nav-link .feather { - margin-right: 4px; - color: #727272; + margin-right: 4px; + color: #727272; } .sidebar .nav-link.active { - color: #007bff; + color: #007bff; } .sidebar .nav-link:hover .feather, .sidebar .nav-link.active .feather { - color: inherit; + color: inherit; } .sidebar-heading { - font-size: .75rem; - text-transform: uppercase; + font-size: .75rem; + text-transform: uppercase; } /* @@ -121,4 +121,28 @@ td { word-break: break-all; -} \ No newline at end of file +} + +.sortable-header { + white-space: nowrap; +} + +.sortable-header button { + padding: 0; + margin: 0; + border: none; + background-color: transparent; + font-weight: bold; +} + +.sortable-header svg { + color: lightgray; +} + +.sortable-header.current button, .sortable-header:hover button { + text-decoration: underline; +} + +.sortable-header.current svg, .sortable-header:hover svg { + color: black; +} diff --git a/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/js/dashboard.js b/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/js/dashboard.js index 8b02046..3a5c090 100644 --- a/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/js/dashboard.js +++ b/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/js/dashboard.js @@ -1,16 +1,15 @@ /* globals feather:false */ -(function() { +(function () { 'use strict'; feather.replace(); - function clearInput() { var initiators = document.querySelectorAll('[data-clear]'); - initiators.forEach(function(initiator) { + initiators.forEach(function (initiator) { initiator.addEventListener('click', - function(e) { + function (e) { var target = e.currentTarget; var selector = target.getAttribute('data-clear'); var input = document.querySelector(selector); @@ -21,7 +20,7 @@ function confirmSubmit() { var initiators = document.querySelectorAll('[data-confirm]'); - initiators.forEach(function(initiator) { + initiators.forEach(function (initiator) { var form = initiator.form; form.addEventListener('submit', function (e) { @@ -36,6 +35,43 @@ }); } + function addFormTriggers() { + var form = document.getElementById("tableQueryState"); + if (!form) { return; } + + var buttons = form.parentElement.querySelectorAll('.sortable-header button'); + buttons.forEach(function (button) { + button.addEventListener('click', function (e) { + e.preventDefault(); + var header = button.closest(".sortable-header"); + var sortBy = form.querySelector("input[name='sort-by']"); + var sortDirection = form.querySelector("input[name='sort-direction']"); + if (sortBy) sortBy.value = header.dataset.sortBy; + if (sortDirection) sortDirection.value = header.dataset.sortDirection; + form.submit(); + }); + }); + + var pageLinks = form.parentElement.querySelectorAll('.page-link[name="page"]'); + pageLinks.forEach(function (pageLink) { + pageLink.addEventListener('click', function (e) { + e.preventDefault(); + var page = form.querySelector("input[name='page']"); + if (page) page.value = pageLink.value; + form.submit(); + }); + }); + + var searchButton = form.querySelector(".search-button"); + if (searchButton) { + var page = form.querySelector("input[name='page']"); + if (page) { + page.value = 1; + } + } + } + clearInput(); confirmSubmit(); -})() \ No newline at end of file + addFormTriggers(); +})() diff --git a/src/Geta.NotFoundHandler/Core/CustomRedirectsResult.cs b/src/Geta.NotFoundHandler/Core/CustomRedirectsResult.cs new file mode 100644 index 0000000..76512ab --- /dev/null +++ b/src/Geta.NotFoundHandler/Core/CustomRedirectsResult.cs @@ -0,0 +1,23 @@ +// Copyright (c) Geta Digital. All rights reserved. +// Licensed under Apache-2.0. See the LICENSE file in the project root for more information + +using System.Collections.Generic; +using Geta.NotFoundHandler.Core.Redirects; + +namespace Geta.NotFoundHandler.Core; + +public class CustomRedirectsResult +{ + public CustomRedirectsResult(IList redirects, int unfilteredCount, int totalCount) + { + Redirects = redirects; + UnfilteredCount = unfilteredCount; + TotalCount = totalCount; + } + + public IList Redirects { get; init; } + + public int UnfilteredCount { get; init; } + + public int TotalCount { get; init; } +} diff --git a/src/Geta.NotFoundHandler/Core/Redirects/DefaultRedirectsService.cs b/src/Geta.NotFoundHandler/Core/Redirects/DefaultRedirectsService.cs index 40f9703..25b2cbe 100644 --- a/src/Geta.NotFoundHandler/Core/Redirects/DefaultRedirectsService.cs +++ b/src/Geta.NotFoundHandler/Core/Redirects/DefaultRedirectsService.cs @@ -31,22 +31,27 @@ public IEnumerable GetAll() public IEnumerable GetSaved() { - return _redirectLoader.GetByState(RedirectState.Saved); + return GetRedirects(new QueryParams() { QueryState = RedirectState.Saved }).Redirects; } public IEnumerable GetIgnored() { - return _redirectLoader.GetByState(RedirectState.Ignored); + return GetRedirects(new QueryParams() { QueryState = RedirectState.Ignored }).Redirects; } public IEnumerable GetDeleted() { - return _redirectLoader.GetByState(RedirectState.Deleted); + return GetRedirects(new QueryParams() { QueryState = RedirectState.Deleted }).Redirects; } public IEnumerable Search(string searchText) { - return _redirectLoader.Find(searchText); + return GetRedirects(new QueryParams() { QueryText = searchText }).Redirects; + } + + public CustomRedirectsResult GetRedirects(QueryParams query) + { + return _redirectLoader.GetRedirects(query); } public void AddOrUpdate(CustomRedirect redirect) @@ -91,7 +96,9 @@ public void AddDeletedRedirect(string oldUrl) { var redirect = new CustomRedirect { - OldUrl = oldUrl, NewUrl = string.Empty, State = Convert.ToInt32(RedirectState.Deleted) + OldUrl = oldUrl, + NewUrl = string.Empty, + State = Convert.ToInt32(RedirectState.Deleted) }; AddOrUpdate(redirect, notifyUpdated: true); } @@ -141,7 +148,7 @@ public int DeleteAll() public int DeleteAllIgnored() { // In order to avoid a database timeout, we delete the items one by one. - var ignoredRedirects = GetIgnored().ToList(); + var ignoredRedirects = GetRedirects(new QueryParams() { QueryState = RedirectState.Ignored }).Redirects.ToList(); foreach (var redirect in ignoredRedirects) { _repository.Delete(redirect); diff --git a/src/Geta.NotFoundHandler/Core/Redirects/IRedirectsService.cs b/src/Geta.NotFoundHandler/Core/Redirects/IRedirectsService.cs index 09c8598..c949587 100644 --- a/src/Geta.NotFoundHandler/Core/Redirects/IRedirectsService.cs +++ b/src/Geta.NotFoundHandler/Core/Redirects/IRedirectsService.cs @@ -1,23 +1,42 @@ // Copyright (c) Geta Digital. All rights reserved. // Licensed under Apache-2.0. See the LICENSE file in the project root for more information +using System; using System.Collections.Generic; +using Geta.NotFoundHandler.Data; namespace Geta.NotFoundHandler.Core.Redirects { public interface IRedirectsService { IEnumerable GetAll(); + + [Obsolete($"Use {nameof(GetRedirects)} instead")] IEnumerable GetSaved(); + + [Obsolete($"Use {nameof(GetRedirects)} instead")] IEnumerable GetIgnored(); + + [Obsolete($"Use {nameof(GetRedirects)} instead")] IEnumerable GetDeleted(); + + [Obsolete($"Use {nameof(GetRedirects)} instead")] IEnumerable Search(string searchText); + + CustomRedirectsResult GetRedirects(QueryParams query); + void AddOrUpdate(CustomRedirect redirect); + void AddOrUpdate(IEnumerable redirects); + void AddDeletedRedirect(string oldUrl); + void DeleteByOldUrl(string oldUrl); + void DeleteByOldUrl(IEnumerable oldUrls); + int DeleteAll(); + int DeleteAllIgnored(); } } diff --git a/src/Geta.NotFoundHandler/Core/SuggestionRedirectsResult.cs b/src/Geta.NotFoundHandler/Core/SuggestionRedirectsResult.cs new file mode 100644 index 0000000..315b341 --- /dev/null +++ b/src/Geta.NotFoundHandler/Core/SuggestionRedirectsResult.cs @@ -0,0 +1,23 @@ +// Copyright (c) Geta Digital. All rights reserved. +// Licensed under Apache-2.0. See the LICENSE file in the project root for more information + +using System.Collections.Generic; +using Geta.NotFoundHandler.Core.Suggestions; + +namespace Geta.NotFoundHandler.Core; + +public class SuggestionRedirectsResult +{ + public SuggestionRedirectsResult(IList redirects, int unfilteredCount, int totalCount) + { + Suggestions = redirects; + UnfilteredCount = unfilteredCount; + TotalCount = totalCount; + } + + public IList Suggestions { get; init; } + + public int UnfilteredCount { get; init; } + + public int TotalCount { get; init; } +} diff --git a/src/Geta.NotFoundHandler/Core/Suggestions/DefaultSuggestionService.cs b/src/Geta.NotFoundHandler/Core/Suggestions/DefaultSuggestionService.cs index 47e60ca..1fc25b0 100644 --- a/src/Geta.NotFoundHandler/Core/Suggestions/DefaultSuggestionService.cs +++ b/src/Geta.NotFoundHandler/Core/Suggestions/DefaultSuggestionService.cs @@ -28,6 +28,11 @@ public IPagedList GetSummaries(int page, int pageSize) return _suggestionLoader.GetSummaries(page, pageSize); } + public SuggestionRedirectsResult GetSummaries(QueryParams query) + { + return _suggestionLoader.GetSummaries(query); + } + public void AddRedirect(SuggestionRedirect suggestionRedirect) { SaveRedirect(suggestionRedirect); diff --git a/src/Geta.NotFoundHandler/Core/Suggestions/ISuggestionService.cs b/src/Geta.NotFoundHandler/Core/Suggestions/ISuggestionService.cs index 6b73586..1e68b61 100644 --- a/src/Geta.NotFoundHandler/Core/Suggestions/ISuggestionService.cs +++ b/src/Geta.NotFoundHandler/Core/Suggestions/ISuggestionService.cs @@ -1,16 +1,18 @@ // Copyright (c) Geta Digital. All rights reserved. // Licensed under Apache-2.0. See the LICENSE file in the project root for more information -using X.PagedList; +using Geta.NotFoundHandler.Data; namespace Geta.NotFoundHandler.Core.Suggestions { - public interface ISuggestionService + public interface ISuggestionService : ISuggestionLoader { - IPagedList GetSummaries(int page, int pageSize); void AddRedirect(SuggestionRedirect suggestionRedirect); + void IgnoreSuggestion(string oldUrl); + void DeleteAll(); + void Delete(int maxErrors, int minimumDays); } } diff --git a/src/Geta.NotFoundHandler/Data/IDataExecutor.cs b/src/Geta.NotFoundHandler/Data/IDataExecutor.cs index b2bb19d..4d076fe 100644 --- a/src/Geta.NotFoundHandler/Data/IDataExecutor.cs +++ b/src/Geta.NotFoundHandler/Data/IDataExecutor.cs @@ -10,15 +10,25 @@ namespace Geta.NotFoundHandler.Data public interface IDataExecutor { DataTable ExecuteQuery(string sqlCommand, params IDbDataParameter[] parameters); + bool ExecuteNonQuery(string sqlCommand, params IDbDataParameter[] parameters); - int ExecuteScalar(string sqlCommand); + + int ExecuteScalar(string sqlCommand, params IDbDataParameter[] parameters); + int ExecuteStoredProcedure(string sqlCommand, int defaultReturnValue = -1); + DbParameter CreateParameter(string parameterName, DbType dbType); + DbParameter CreateParameter(string parameterName, DbType dbType, int size); + DbParameter CreateGuidParameter(string name, Guid value); + DbParameter CreateStringParameter(string name, string value, int size = 2000); + DbParameter CreateIntParameter(string name, int value); + DbParameter CreateBoolParameter(string name, bool value); + DbParameter CreateDateTimeParameter(string name, DateTime value); } } diff --git a/src/Geta.NotFoundHandler/Data/IRedirectLoader.cs b/src/Geta.NotFoundHandler/Data/IRedirectLoader.cs index 0c95b98..a954344 100644 --- a/src/Geta.NotFoundHandler/Data/IRedirectLoader.cs +++ b/src/Geta.NotFoundHandler/Data/IRedirectLoader.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Geta.NotFoundHandler.Core; using Geta.NotFoundHandler.Core.Redirects; namespace Geta.NotFoundHandler.Data @@ -10,9 +11,17 @@ namespace Geta.NotFoundHandler.Data public interface IRedirectLoader { CustomRedirect GetByOldUrl(string oldUrl); + IEnumerable GetAll(); + + [Obsolete($"Use {nameof(GetRedirects)} instead")] IEnumerable GetByState(RedirectState state); + + [Obsolete($"Use {nameof(GetRedirects)} instead")] IEnumerable Find(string searchText); + + CustomRedirectsResult GetRedirects(QueryParams query); + CustomRedirect Get(Guid id); } } diff --git a/src/Geta.NotFoundHandler/Data/ISuggestionLoader.cs b/src/Geta.NotFoundHandler/Data/ISuggestionLoader.cs index 2ec9ae0..62fdc30 100644 --- a/src/Geta.NotFoundHandler/Data/ISuggestionLoader.cs +++ b/src/Geta.NotFoundHandler/Data/ISuggestionLoader.cs @@ -1,6 +1,8 @@ // Copyright (c) Geta Digital. All rights reserved. // Licensed under Apache-2.0. See the LICENSE file in the project root for more information +using System; +using Geta.NotFoundHandler.Core; using Geta.NotFoundHandler.Core.Suggestions; using X.PagedList; @@ -8,6 +10,9 @@ namespace Geta.NotFoundHandler.Data { public interface ISuggestionLoader { + [Obsolete($"Please use other overload for {nameof(GetSummaries)}")] IPagedList GetSummaries(int page, int pageSize); + + SuggestionRedirectsResult GetSummaries(QueryParams query); } } diff --git a/src/Geta.NotFoundHandler/Data/QueryParams.cs b/src/Geta.NotFoundHandler/Data/QueryParams.cs new file mode 100644 index 0000000..47c0548 --- /dev/null +++ b/src/Geta.NotFoundHandler/Data/QueryParams.cs @@ -0,0 +1,29 @@ +// Copyright (c) Geta Digital. All rights reserved. +// Licensed under Apache-2.0. See the LICENSE file in the project root for more information + +using System.Data.SqlClient; +using Geta.NotFoundHandler.Core.Redirects; +using Geta.NotFoundHandler.Infrastructure.Web; + +namespace Geta.NotFoundHandler.Data +{ + public class QueryParams + { + [FromFormOrQuery(Name = "q")] + public string QueryText { get; set; } = string.Empty; + + public RedirectState? QueryState { get; set; } + + [FromFormOrQuery(Name = "page")] + public int Page { get; set; } = 1; + + [FromFormOrQuery(Name = "page-size")] + public int? PageSize { get; set; } + + [FromFormOrQuery(Name = "sort-by")] + public string SortBy { get; set; } + + [FromFormOrQuery(Name = "sort-direction")] + public SortOrder SortDirection { get; set; } = SortOrder.Ascending; + } +} diff --git a/src/Geta.NotFoundHandler/Data/SqlDataExecutor.cs b/src/Geta.NotFoundHandler/Data/SqlDataExecutor.cs index 66a22fe..072a543 100644 --- a/src/Geta.NotFoundHandler/Data/SqlDataExecutor.cs +++ b/src/Geta.NotFoundHandler/Data/SqlDataExecutor.cs @@ -4,9 +4,9 @@ using System; using System.Data; using System.Data.Common; +using System.Data.SqlClient; using System.Globalization; using Geta.NotFoundHandler.Infrastructure.Configuration; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -70,7 +70,7 @@ public bool ExecuteNonQuery(string sqlCommand, params IDbDataParameter[] paramet return success; } - public int ExecuteScalar(string sqlCommand) + public int ExecuteScalar(string sqlCommand, params IDbDataParameter[] parameters) { int result; try @@ -78,7 +78,7 @@ public int ExecuteScalar(string sqlCommand) using var connection = new SqlConnection(_connectionString); connection.Open(); - using var command = CreateCommand(connection, sqlCommand); + using var command = CreateCommand(connection, sqlCommand, parameters); var queryResult = command.ExecuteScalar(); if (queryResult == null) return 0; result = (int)queryResult; @@ -125,7 +125,9 @@ public DbParameter CreateParameter(string parameterName, DbType dbType) { var parameter = new SqlParameter { - ParameterName = parameterName, DbType = dbType, Direction = ParameterDirection.Input + ParameterName = parameterName, + DbType = dbType, + Direction = ParameterDirection.Input }; return parameter; } @@ -187,7 +189,9 @@ private static SqlParameter CreateReturnParameter() { var parameter = new SqlParameter { - ParameterName = "@ReturnValue", DbType = DbType.Int32, Direction = ParameterDirection.ReturnValue, + ParameterName = "@ReturnValue", + DbType = DbType.Int32, + Direction = ParameterDirection.ReturnValue, }; return parameter; } @@ -202,7 +206,7 @@ private static SqlCommand CreateCommand(SqlConnection connection, string sqlComm foreach (var dbDataParameter in parameters) { var parameter = (SqlParameter)dbDataParameter; - command.Parameters.Add(parameter); + command.Parameters.Add((parameter as ICloneable).Clone()); } } diff --git a/src/Geta.NotFoundHandler/Data/SqlRedirectRepository.cs b/src/Geta.NotFoundHandler/Data/SqlRedirectRepository.cs index 67c660a..507c90a 100644 --- a/src/Geta.NotFoundHandler/Data/SqlRedirectRepository.cs +++ b/src/Geta.NotFoundHandler/Data/SqlRedirectRepository.cs @@ -4,7 +4,10 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.SqlClient; using System.Linq; +using System.Text.RegularExpressions; +using Geta.NotFoundHandler.Core; using Geta.NotFoundHandler.Core.Redirects; namespace Geta.NotFoundHandler.Data @@ -104,7 +107,7 @@ public CustomRedirect GetByOldUrl(string oldUrl) public IEnumerable GetAll() { - var sqlCommand = $@"SELECT {AllFields} FROM {RedirectsTable}"; + var sqlCommand = $"SELECT {AllFields} FROM {RedirectsTable}"; var dataTable = _dataExecutor.ExecuteQuery(sqlCommand); @@ -113,27 +116,42 @@ public IEnumerable GetAll() public IEnumerable GetByState(RedirectState state) { - var sqlCommand = $@"SELECT {AllFields} FROM {RedirectsTable} - WHERE State = @state"; - - var dataTable = _dataExecutor.ExecuteQuery( - sqlCommand, - _dataExecutor.CreateIntParameter("state", (int)state)); - - return ToCustomRedirects(dataTable); + return GetRedirects(new QueryParams() { QueryState = state }).Redirects; } public IEnumerable Find(string searchText) { - var sqlCommand = $@"SELECT {AllFields} FROM {RedirectsTable} - WHERE OldUrl like '%' + @searchText + '%' - OR NewUrl like '%' + @searchText + '%'"; + return GetRedirects(new QueryParams() { QueryText = searchText }).Redirects; + } + + public CustomRedirectsResult GetRedirects(QueryParams query) + { + var parameters = new List(); + + var whereString = GetWhereString(query, parameters); + + var suffixString = GetSuffixString(query, parameters, out var isPaginated); var dataTable = _dataExecutor.ExecuteQuery( - sqlCommand, - _dataExecutor.CreateStringParameter("searchText", searchText)); + $"SELECT {AllFields} FROM {RedirectsTable}{whereString}{suffixString}", + parameters.ToArray()); - return ToCustomRedirects(dataTable); + var items = ToCustomRedirects(dataTable); + var totalCount = items.Count; + if (isPaginated) + { + var whereState = query.QueryState != null ? @" + WHERE state = @state" : ""; + totalCount = _dataExecutor.ExecuteScalar($"SELECT COUNT(*) FROM {RedirectsTable}{whereState}", + _dataExecutor.CreateIntParameter("state", (int)query.QueryState)); + } + var filteredCount = totalCount; + if (!string.IsNullOrWhiteSpace(whereString)) + { + filteredCount = _dataExecutor.ExecuteScalar($"SELECT COUNT(*) FROM {RedirectsTable}{whereString}", parameters.ToArray()); + } + + return new CustomRedirectsResult(items, totalCount, filteredCount); } public CustomRedirect Get(Guid id) @@ -148,9 +166,9 @@ public CustomRedirect Get(Guid id) return ToCustomRedirects(dataTable).FirstOrDefault(); } - private static IEnumerable ToCustomRedirects(DataTable table) + private static IList ToCustomRedirects(DataTable table) { - return table.AsEnumerable().Select(ToCustomRedirect); + return table.AsEnumerable().Select(ToCustomRedirect).ToList(); } private static CustomRedirect ToCustomRedirect(DataRow x) @@ -159,7 +177,65 @@ private static CustomRedirect ToCustomRedirect(DataRow x) x.Field("OldUrl"), x.Field("NewUrl"), x.Field("WildCardSkipAppend"), - x.Field("RedirectType")) { Id = x.Field("Id"), State = x.Field("State") }; + x.Field("RedirectType")) + { Id = x.Field("Id"), State = x.Field("State") }; + } + + private string GetWhereString(QueryParams query, IList parameters) + { + var conditions = new List(); + if (!string.IsNullOrEmpty(query?.QueryText)) + { + parameters.Add(_dataExecutor.CreateStringParameter("searchText", query.QueryText)); + conditions.Add(@"(OldUrl like '%' + @searchText + '%' + OR NewUrl like '%' + @searchText + '%')"); + } + + if (query.QueryState != null) + { + parameters.Add(_dataExecutor.CreateIntParameter("state", (int)query.QueryState)); + conditions.Add("State = @state"); + } + + var hasFilter = conditions.Any(); + if (hasFilter) + { + return $@" + WHERE {string.Join(" AND ", conditions)}"; + } + + return ""; + } + + private string GetSuffixString(QueryParams query, IList parameters, out bool isPaginated) + { + var suffixString = ""; + var safeSortBy = Regex.Replace(query.SortBy ?? string.Empty, "[^A-Za-z]", "", RegexOptions.IgnoreCase); + var hasSortBy = !string.IsNullOrWhiteSpace(safeSortBy); + if (hasSortBy) + { + suffixString += $@" + ORDER BY [{safeSortBy}] {(query.SortDirection == SortOrder.Ascending ? "ASC" : "DESC")}"; + } + + isPaginated = false; + if (query.PageSize is int ps && ps > 0) + { + isPaginated = true; + if (!hasSortBy) + { + // Adds dummy ORDER BY for pagination + suffixString += @" + ORDER BY(SELECT NULL)"; + } + parameters.Add(_dataExecutor.CreateIntParameter("pageSize", ps)); + parameters.Add(_dataExecutor.CreateIntParameter("skip", (query.Page - 1) * ps)); + suffixString += $@" + OFFSET {(query.Page - 1) * ps} ROWS + FETCH NEXT @pageSize ROWS ONLY"; + } + + return suffixString; } } } diff --git a/src/Geta.NotFoundHandler/Data/SqlSuggestionRepository.cs b/src/Geta.NotFoundHandler/Data/SqlSuggestionRepository.cs index 120bebe..783495a 100644 --- a/src/Geta.NotFoundHandler/Data/SqlSuggestionRepository.cs +++ b/src/Geta.NotFoundHandler/Data/SqlSuggestionRepository.cs @@ -4,7 +4,10 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.SqlClient; using System.Linq; +using System.Text.RegularExpressions; +using Geta.NotFoundHandler.Core; using Geta.NotFoundHandler.Core.Suggestions; using X.PagedList; @@ -23,14 +26,41 @@ public SqlSuggestionRepository(IDataExecutor dataExecutor) public IPagedList GetSummaries(int page, int pageSize) { - var table = GetSuggestionsPaged(page, pageSize); - var summaries = CreateSuggestionSummaries(table); - var count = CountSummaries(); + var result = GetSummaries(new QueryParams() { Page = page, PageSize = pageSize }); + return new StaticPagedList(result.Suggestions, page, pageSize, result.TotalCount); + } + + public SuggestionRedirectsResult GetSummaries(QueryParams query) + { + var parameters = new List(); + + var whereString = GetWhereString(query, parameters); + + var suffixString = GetSuffixString(query, parameters, out var isPaginated); - return new StaticPagedList(summaries, page, pageSize, count); + const string GroupByOldUrl = @" + GROUP BY [OldUrl]"; + + var dataTable = _dataExecutor.ExecuteQuery( + $"SELECT [OldUrl], COUNT(*) as Requests FROM {SuggestionsTable}{whereString}{GroupByOldUrl}{suffixString}", + parameters.ToArray()); + + var items = CreateSuggestionSummaries(dataTable); + var totalCount = items.Count; + if (isPaginated) + { + totalCount = _dataExecutor.ExecuteScalar($"SELECT DISTINCT COUNT(*) OVER () FROM {SuggestionsTable}{GroupByOldUrl}"); + } + var filteredCount = totalCount; + if (!string.IsNullOrWhiteSpace(whereString)) + { + filteredCount = _dataExecutor.ExecuteScalar($"SELECT DISTINCT COUNT(*) OVER () FROM {SuggestionsTable}{whereString}{GroupByOldUrl}", parameters.ToArray()); + } + + return new SuggestionRedirectsResult(items, totalCount, filteredCount); } - private IEnumerable CreateSuggestionSummaries(DataTable table) + private IList CreateSuggestionSummaries(DataTable table) { var summaries = new List(); @@ -83,7 +113,7 @@ private IEnumerable GetReferers(string url) public void DeleteAll() { - var sqlCommand = $@"delete from {SuggestionsTable}"; + var sqlCommand = $"delete from {SuggestionsTable}"; _dataExecutor.ExecuteNonQuery(sqlCommand); } @@ -131,38 +161,58 @@ public void Save(string oldUrl, string referer, DateTime requestedOn) _dataExecutor.ExecuteNonQuery(sqlCommand, requestedParam, refererParam, oldUrlParam); } - private DataTable GetSuggestionsPaged(int? page, int? pageSize) + public DataTable GetSuggestionReferers(string url) { var sqlCommand = - $"SELECT [OldUrl], COUNT(*) as Requests FROM {SuggestionsTable} GROUP BY [OldUrl] order by Requests desc"; - - if (page.HasValue && pageSize.HasValue) - { - page = Math.Max(1, page.Value); - var skip = (page.Value - 1) * pageSize.Value; + $"SELECT [Referer], COUNT(*) as Requests FROM {SuggestionsTable} where [OldUrl] = @oldUrl GROUP BY [Referer] order by Requests desc"; - sqlCommand += $" OFFSET {skip} ROWS FETCH NEXT {pageSize.Value} ROWS ONLY"; - } + var oldUrlParam = _dataExecutor.CreateParameter("oldUrl", DbType.String, 2000); + oldUrlParam.Value = url; - return _dataExecutor.ExecuteQuery(sqlCommand); + return _dataExecutor.ExecuteQuery(sqlCommand, oldUrlParam); } - private int CountSummaries() + private string GetWhereString(QueryParams query, IList parameters) { - var sqlCommand = $"SELECT COUNT([ID]) FROM {SuggestionsTable}"; + if (!string.IsNullOrEmpty(query?.QueryText)) + { + parameters.Add(_dataExecutor.CreateStringParameter("searchText", query.QueryText)); + return @" + WHERE OldUrl like '%' + @searchText + '%'"; + } - return _dataExecutor.ExecuteScalar(sqlCommand); + return ""; } - public DataTable GetSuggestionReferers(string url) + private string GetSuffixString(QueryParams query, IList parameters, out bool isPaginated) { - var sqlCommand = - $"SELECT [Referer], COUNT(*) as Requests FROM {SuggestionsTable} where [OldUrl] = @oldUrl GROUP BY [Referer] order by Requests desc"; + var suffixString = ""; + var safeSortBy = Regex.Replace(query.SortBy ?? string.Empty, "[^A-Za-z]", "", RegexOptions.IgnoreCase); + var hasSortBy = !string.IsNullOrWhiteSpace(safeSortBy); + if (hasSortBy) + { + suffixString += $@" + ORDER BY [{safeSortBy}] {(query.SortDirection == SortOrder.Ascending ? "ASC" : "DESC")}"; + } - var oldUrlParam = _dataExecutor.CreateParameter("oldUrl", DbType.String, 2000); - oldUrlParam.Value = url; + isPaginated = false; + if (query.PageSize is int ps && ps > 0) + { + isPaginated = true; + if (!hasSortBy) + { + // Adds dummy ORDER BY for pagination + suffixString += @" + ORDER BY(SELECT NULL)"; + } + parameters.Add(_dataExecutor.CreateIntParameter("pageSize", ps)); + parameters.Add(_dataExecutor.CreateIntParameter("skip", (query.Page - 1) * ps)); + suffixString += $@" + OFFSET {(query.Page - 1) * ps} ROWS + FETCH NEXT @pageSize ROWS ONLY"; + } - return _dataExecutor.ExecuteQuery(sqlCommand, oldUrlParam); + return suffixString; } } } diff --git a/src/Geta.NotFoundHandler/Geta.NotFoundHandler.csproj b/src/Geta.NotFoundHandler/Geta.NotFoundHandler.csproj index 742fcba..5ae51fb 100644 --- a/src/Geta.NotFoundHandler/Geta.NotFoundHandler.csproj +++ b/src/Geta.NotFoundHandler/Geta.NotFoundHandler.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/Geta.NotFoundHandler/Infrastructure/Web/FromFormOrQueryAttribute.cs b/src/Geta.NotFoundHandler/Infrastructure/Web/FromFormOrQueryAttribute.cs new file mode 100644 index 0000000..2c74012 --- /dev/null +++ b/src/Geta.NotFoundHandler/Infrastructure/Web/FromFormOrQueryAttribute.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Geta.NotFoundHandler.Infrastructure.Web +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class FromFormOrQueryAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromQueryMetadata + { + public BindingSource BindingSource => CompositeBindingSource.Create( + new[] { BindingSource.Form, BindingSource.Query }, nameof(FromFormOrQueryAttribute)); + + public string Name { get; set; } + } +}