Skip to content

Commit

Permalink
feat: implemented page breadcrumbs
Browse files Browse the repository at this point in the history
  • Loading branch information
mattb-hippo committed Oct 11, 2024
1 parent ce3a3c1 commit ff65498
Show file tree
Hide file tree
Showing 13 changed files with 438 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using Childrens_Social_Care_CPD;
using Childrens_Social_Care_CPD.Contentful;
using Childrens_Social_Care_CPD.Contentful.Models;
using Childrens_Social_Care_CPD.Controllers;
using Childrens_Social_Care_CPD.Models;
using Contentful.Core.Models;
using Contentful.Core.Search;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Azure;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Childrens_Social_Care_CPD_Tests.Controllers;

public class ContentControllerBreadcrumbTests
{
private ContentController _contentController;
private IRequestCookieCollection _cookies;
private HttpContext _httpContext;
private HttpRequest _httpRequest;
private ICpdContentfulClient _contentfulClient;

private void SetContent(List<KeyValuePair<string, Content>> content)
{
var contentCollections = new List<KeyValuePair<string, ContentfulCollection<Content>>>();
foreach (var contentDefinition in content)
{
var contentCollection = new ContentfulCollection<Content>();

contentCollection.Items = contentDefinition.Value == null
? new List<Content>()
: contentCollection.Items = new List<Content> { contentDefinition.Value };

contentCollections.Add(new KeyValuePair<string, ContentfulCollection<Content>>(contentDefinition.Key, contentCollection));
}

_contentfulClient
.GetEntries(Arg.Any<QueryBuilder<Content>>(), Arg.Any<CancellationToken>())
.Returns(x => {
var query = x.Arg<QueryBuilder<Content>>().Build();
foreach (var contentDefinition in content)
{
if (query.Contains("fields.id=" + contentDefinition.Key)) return contentCollections.First(x => x.Key == contentDefinition.Key).Value;
}
return new ContentfulCollection<Content>();
});

}

private void SetContent() {
var page = new Content()
{
Id = "page",
Title = "Content Page"
};

var content = new List<KeyValuePair<string, Content>>()
{
new KeyValuePair<string, Content>("page", page),
};

SetContent(content);
}

[SetUp]
public void SetUp()
{
_cookies = Substitute.For<IRequestCookieCollection>();
_httpContext = Substitute.For<HttpContext>();
_httpRequest = Substitute.For<HttpRequest>();
var controllerContext = Substitute.For<ControllerContext>();

_httpRequest.Cookies.Returns(_cookies);
_httpContext.Request.Returns(_httpRequest);

controllerContext.HttpContext = _httpContext;

_contentfulClient = Substitute.For<ICpdContentfulClient>();

_contentController = new ContentController(_contentfulClient)
{
ControllerContext = controllerContext,
TempData = Substitute.For<ITempDataDictionary>()
};
}

[TearDown]
public void TearDown()
{
_contentfulClient = Substitute.For<ICpdContentfulClient>();
}

[Test]
public async Task Index_Sets_Breadcrumbs_In_ContextModel()
{
// arrange
var parentPage = new Content()
{
Id = "parent",
Title = "Parent Page"
};
var childPage = new Content(){
Id = "child",
Title = "Child Page",
ParentPages = new List<Content>(){parentPage}
};

var content = new List<KeyValuePair<string, Content>>(){
new KeyValuePair<string, Content>("parent", parentPage),
new KeyValuePair<string, Content>("child", childPage)
};

SetContent(content);

// act
await _contentController.Index("child");
var actual = _contentController.ViewData["ContextModel"] as ContextModel;
var breadcrumbTrail = actual?.BreadcrumbTrail;

// assert
actual.Should().NotBeNull();
breadcrumbTrail[0].Key.Should().Be("Child Page");
breadcrumbTrail[0].Value.Should().Be("child");
breadcrumbTrail[1].Key.Should().Be("Parent Page");
breadcrumbTrail[1].Value.Should().Be("parent");
}

[Test]
public async Task Index_Sets_Blank_Breadcrumbs_In_ContextModel_If_Page_Has_No_Parent()
{
// arrange
SetContent();

// act
await _contentController.Index("page");
var actual = _contentController.ViewData["ContextModel"] as ContextModel;
var breadcrumbTrail = actual?.BreadcrumbTrail;

// assert
actual.Should().NotBeNull();
breadcrumbTrail.Should().BeEmpty();
}
}
2 changes: 2 additions & 0 deletions Childrens-Social-Care-CPD/Contentful/Models/Content.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class Content : IContent
public NavigationMenu Navigation { get; set; }
public RelatedContent RelatedContent { get; set; }
public int? EstimatedReadingTime { get; set; }
public List<Content> ParentPages { get; set; }
public string BreadcrumbText { get; set; }

[JsonProperty("$metadata")]
public ContentfulMetadata Metadata { get; set; }
Expand Down
49 changes: 45 additions & 4 deletions Childrens-Social-Care-CPD/Controllers/ContentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Childrens_Social_Care_CPD.Models;
using Contentful.Core.Search;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;

namespace Childrens_Social_Care_CPD.Controllers;
Expand All @@ -21,9 +22,49 @@ private async Task<Content> FetchPageContentAsync(string contentId, Cancellation
return result?.FirstOrDefault();
}

private async Task<Stack<KeyValuePair<string, string>>> BuildBreadcrumbTrail(List<string> pagesVisited)
private async Task<List<KeyValuePair<string, string>>> BuildBreadcrumbTrail(
List<KeyValuePair<string, string>> trail,
Content page,
List<string> pagesVisited,
CancellationToken ct)
{
return new Stack<KeyValuePair<string, string>>();
var trailItem = new KeyValuePair<string, string>(
page.BreadcrumbText.IsNullOrEmpty() ?
page.Title :
page.BreadcrumbText,
page.Id);

if (page.ParentPages == null || page.ParentPages.Count == 0) {
if (trail.Count > 0) trail.Add(trailItem);
return trail;
}

trail.Add(trailItem);

Content parentPage = new Content();

if (page.ParentPages?.Count == 1) {
parentPage = page.ParentPages[0] as Content;
}
else
{
var parentPageIds = page.ParentPages
.Select(parent => parent.Id)
.ToList();

var checkPages = pagesVisited.Reverse<string>();

foreach (var pageId in checkPages)
{
if (parentPageIds.Contains(pageId))
{
parentPage = page.ParentPages.First(p => p.Id == pageId);
break;
}
};
}
var parentObject = await FetchPageContentAsync(parentPage.Id, ct);
return await BuildBreadcrumbTrail(trail, parentObject, pagesVisited, ct);
}

[HttpGet]
Expand Down Expand Up @@ -58,7 +99,7 @@ public async Task<IActionResult> Index(string pageName = "home", bool preference
var pagesVisited = HttpContext.Session.Get<List<string>>("pagesVisited");
if (pagesVisited == null) pagesVisited = new List<string>();
pagesVisited.Add(pageName);
HttpContext.Session.Set<List<string>>("pagesVisited", pagesVisited);
HttpContext.Session.Set("pagesVisited", pagesVisited);
Console.WriteLine("\n\nPages Visited: " + JsonConvert.SerializeObject(pagesVisited));

var contextModel = new ContextModel(
Expand All @@ -70,7 +111,7 @@ public async Task<IActionResult> Index(string pageName = "home", bool preference
PreferenceSet: preferenceSet,
BackLink: content.BackLink,
FeedbackSubmitted: fs,
BreadcrumbTrail: await BuildBreadcrumbTrail(pagesVisited));
BreadcrumbTrail: await BuildBreadcrumbTrail(new List<KeyValuePair<string, string>>(), content, pagesVisited, cancellationToken));

ViewData["ContextModel"] = contextModel;
ViewData["StateModel"] = new StateModel();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Newtonsoft.Json;

#nullable enable
Expand Down
2 changes: 1 addition & 1 deletion Childrens-Social-Care-CPD/Models/ContextModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public record ContextModel(
bool HideConsent = false,
ContentLink BackLink = null,
bool FeedbackSubmitted = false,
Stack<KeyValuePair<string, string>> BreadcrumbTrail = null)
List<KeyValuePair<string, string>> BreadcrumbTrail = null)
{
public Stack<string> ContentStack { get; } = new Stack<string>();
}
20 changes: 20 additions & 0 deletions Childrens-Social-Care-CPD/Views/Shared/_BreadcrumbTrail.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@using Childrens_Social_Care_CPD.Contentful.Models;

@{
var contextModel = (ContextModel)ViewData["ContextModel"];
var breadcrumbs = contextModel.BreadcrumbTrail;
breadcrumbs.Reverse();
}

<div class="govuk-breadcrumbs govuk-breadcrumbs--collapse-on-mobile">
<ul>
@foreach(KeyValuePair<string, string> trailItem in breadcrumbs)
{
<li class="govuk-breadcrumbs__list-item">
<a class="govuk-breadcrumbs__link" href="@trailItem.Value">
@trailItem.Key
</a>
</li>
}
</ul>
</div>
3 changes: 3 additions & 0 deletions Childrens-Social-Care-CPD/Views/Shared/_PageBanner.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<section id="content-banner" class="app-section-content app-section-content--blue govuk-!-margin-bottom-8">
<div class="dfe-width-container">
<div class="govuk-grid-row govuk-!-padding-top-3 govuk-!-padding-bottom-0">
<div class="govuk-grid-column-two-thirds-from-desktop banner-breadcrumbs">
<partial name="_BreadcrumbTrail" />
</div>
<div class="govuk-grid-column-two-thirds-from-desktop govuk-!-padding-top-3">
<h1 id="content-banner-title" class="govuk-heading-xl">@Model.ContentTitle</h1>
<p id="content-banner-subtitle" class="govuk-body-l">@Model.ContentSubtitle</p>
Expand Down
Loading

0 comments on commit ff65498

Please sign in to comment.