From 1f77a3a8ad545a4811ddf50e08e4257377f11936 Mon Sep 17 00:00:00 2001
From: ThomasWhittington <46750921+ThomasWhittington@users.noreply.github.com>
Date: Mon, 19 Aug 2024 16:50:17 +0100
Subject: [PATCH] Added new blocks and some refactors (#138)
* Added test project
* Added ADR docs
* Added basic unit tests
* added ADR
* updated folder structure
* restructure
* Added architecture diagram
* More unit tests
* Added DI test
* Added contentful model for richtext
* Updated content service (#82)
* Updated content service
* uncommented tests
* undid spacing change
---------
Co-authored-by: Simon FIRTH
Co-authored-by: Tom Whittington
* Created contentful stub
* Updated to query builder
* Add renderer for rich text content (#75)
* add renderer for rich text content
* Add interface for content renderer
* remove unused using statements
* refactor: use partial views to handle rich text rendering
* Add new project for e2e cypress tests
* remove magic strings
* wip: Add mock content for e2e tests
* Update page heading and add support for subheadings
* update mockContent for e2e tests
* add rich text support for horizontal rule
* add support for embedded image assets
* Add support for embedded video
* add support for embedded vimeo player
* Changed views to use explicit models
* Added dev settings to gitignore
* Removed main
---------
Co-authored-by: Tom Whittington
* updated e2e and app settings
* Updated tests
* Fixed/ added some tests for coverage
* Added tests for HttpClients
* Added service tests
* Refactored tests
---------
Co-authored-by: Tom Whittington
* Feature/ rework/linking (#86)
* Lowered HR to Hr
* Added support for linking to content
---------
Co-authored-by: Tom Whittington
* wip: render linked entry and asset from within rich text
* wip: render download component
* display corresponding icon for download filetype
* update e2e tests
* Feature/ rework/embedded entries (#87)
* Added basic support for embeddedEntries
* Added includes to configuration
* Updated tests for new model
* Simplified include logic
* Fixed client test
---------
Co-authored-by: Tom Whittington
* update entry partial
* revert ContentService changes
* use target variable in entry partial
* tweak entry partial
* update file icons
* Added terraform scripts
* add accordion component
* Added workflows and actions
* adding missing WAF and updated contentful environment
* update integration tests
* wip: add accessibility tweaks, accessibilty tests and page layout updates
* Added support for cards and grid containers. Updated dfeFrontend to 2.0.1 (#91)
* Updated DfeFrontend
* Added basic card support
* Card cleanup
* Added grid container
* Updated svgs
---------
Co-authored-by: Tom Whittington
* use dfe container width and update header styling
* updated workflow
* TF changes to get inital build going
* fixed tflint
* removed unused vars from pr-check
* added dev environment
* lock updated
* reformatted locals.tf
* terraform-docs: automated action
* include hero in header
* fix e2e tests
* Updated JS + CSS files
* add support for multiple accordion sections
* resolve assets for e2e testing
* Capitalise development branch name (#93)
* Capitalise development branch name
* force workflow to appear in actions
* Matrix deploy push
* made context root
* moved docker file location
* adjusted docker file location
* lowercase dockerfile name
* moved docker file
* updated matrix deploy
* updated deploy script
* updated image name
* revision create change
* Added env variable
* updated to main
* updated cli version
* removed env
* updated workflow to dispatch
* Feature/app insights (#97)
* Capitalise development branch name
* force workflow to appear in actions
* Matrix deploy push
* made context root
* moved docker file location
* adjusted docker file location
* lowercase dockerfile name
* moved docker file
* updated matrix deploy
* updated deploy script
* updated image name
* revision create change
* Added env variable
* updated to main
* updated cli version
* removed env
* updated workflow to dispatch
* Added application insights + secret management
* Feature/vault (#98)
* Capitalise development branch name
* force workflow to appear in actions
* Matrix deploy push
* made context root
* moved docker file location
* adjusted docker file location
* lowercase dockerfile name
* moved docker file
* updated matrix deploy
* updated deploy script
* updated image name
* revision create change
* Added env variable
* updated to main
* updated cli version
* removed env
* updated workflow to dispatch
* Added application insights + secret management
* modified matrix deploy to run on completed PR
* Added Keyvault secret API to application startup
* fixed YAML
* modifed matrix deloy (#100)
* Updated workflow so it can be manually fired (#102)
* modifed matrix deloy
* changed to workflow dispatch
* removed env
* changed keyvault name
* changed keyvault name
* re-added env
* Adding landing page (#104)
* Created temp home page
* updated is Preview to true for home page
* Preview mode bug fixes for home page
---------
Co-authored-by: Tom Whittington
* Added mapping layer to produce cleaner models (#96)
* Added basic caching layer (#106)
Co-authored-by: Tom Whittington
* Added cache clear endpoint and ignoring cache when in preview (#107)
* Added cache clear endpoint and ignoring cache when in preview
* Added some tests for caching
* modifed cache controller to API type + respond with OK
* removed unused controller map
---------
Co-authored-by: Tom Whittington
Co-authored-by: simonjfirth
* add gtm and clarity tracking to application
* remove unnecessary ms clarity tag
* add cookie consent banner
* remove comments
* change consent cookie values to true or false
* add download last updated date and update styling
* Feature/model mapping refactor (#110)
* Model rework for better testability
* Added better tests for model mapping
---------
Co-authored-by: Tom Whittington
* Feature/plantech prep (#112)
* Model rework for better testability
* Added better tests for model mapping
* Made modifications to allow c&s to run as part of plantech
---------
Co-authored-by: Tom Whittington
* Made app settings not publish
* Removed some code smells
* Explicit types in tests
* Feature/update contentful (#113)
* Updated contentful secret value to it doesn't collide with PT integration
* removed temp code
* Cleaned up some more smells (#114)
* Cleaned up some more smells
* changed to index
---------
Co-authored-by: Tom Whittington
* Hidden cookie banner (#115)
Co-authored-by: Tom Whittington
* Added default page to handle base route (#116)
* feat: removed header text (#117)
Co-authored-by: Tom Whittington
* feat: allowed accordions to display richtext (#118)
Co-authored-by: Tom Whittington
* feat: added support for excel sheets (#119)
Co-authored-by: Tom Whittington
* feat: added back to top button (#120)
Co-authored-by: Tom Whittington
* Update download component mapping and styling
* Added citation block (#121)
* feat: added citation block
* fix: fixed tests
---------
Co-authored-by: Tom Whittington
* feat: use current tab (#122)
Co-authored-by: Tom Whittington
* Updated styling to elements and added missing classes (#123)
* feature: add feedback banner to all pages
* Removed route sitemap base route which was causing a multiple route match in PT (#125)
* add thankyou message to feedback banner
* feat: render or hide feedback banner based on contentful boolean
* wip: reenable content cookies to test conditional rendering for feedback banner
* Feature/side nav (#128)
* Started to create vertical navigation
* First draft of unit tests
* update to tests
* cleaned unit tests
* Updated JS + CSS files
* Cleanup
* Update to broken unit tests
* reverted flag
* removed ref
* removed unused controller tests
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* remove comment
* Added basic print button (#130)
Co-authored-by: Tom Whittington
* add plan tech name to header (#131)
* Tidy up (#132)
* Added basic sonar check (#135)
* Added basic sonar check
* Build and test enabled
* Removed manual trigger on deploy
* Removed push builds
* Fixed branch name
---------
Co-authored-by: Tom Whittington
* feat: update beta banner feedback link
* feat: added basic retry on contentful api calls (#137)
Co-authored-by: Tom Whittington
---------
Co-authored-by: Tom Whittington
Co-authored-by: Simon FIRTH
Co-authored-by: simonjfirth
Co-authored-by: jack-coggin <119428483+jack-coggin@users.noreply.github.com>
Co-authored-by: jack.coggin
Co-authored-by: Iain STANGER
Co-authored-by: github-actions[bot]
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.github/workflows/code-pr-check.yml | 72 +++-
.github/workflows/matrix-deploy.yml | 8 +-
.../styles/scss/vertical-navigation.scss | 41 ++
.../Configuration/CsContentfulOptions.cs | 1 +
.../Controllers/ContentController.cs | 11 +-
.../WebApplicationBuilderExtensions.cs | 1 +
.../Http/HttpContentfulClient.cs | 18 +-
.../Models/ContentBase.cs | 4 +
.../Models/Mapped/CsContentItem.cs | 2 +
.../Models/Mapped/CsPage.cs | 5 +
.../Models/Mapped/PageLink.cs | 10 +
src/Dfe.ContentSupport.Web/Program.cs | 4 +-
.../Services/ILayoutService.cs | 10 +
.../Services/LayoutService.cs | 85 ++++
.../Services/ModelMapper.cs | 19 +-
.../ViewModels/ContentSupportPage.cs | 4 +
.../Views/Content/CsIndex.cshtml | 37 +-
.../Views/Shared/Error.cshtml | 2 +-
.../Shared/RichText/Custom/_Attachment.cshtml | 2 +-
.../Views/Shared/_BetaHeader.cshtml | 3 +-
.../Views/Shared/_CsHeader.cshtml | 3 +
.../Views/Shared/_CsLayout.cshtml | 4 +-
.../Views/Shared/_Feedback.cshtml | 36 ++
.../Views/Shared/_Print.cshtml | 3 +
.../wwwroot/assets/icon-print.png | Bin 0 -> 213 bytes
.../wwwroot/css/cands-site.css | 366 ++++++++++++++----
.../Controllers/ContentControllerTests.cs | 4 +-
.../Mapped/Custom/CustomAccordionTests.cs | 4 +-
.../Mapped/Custom/CustomAttachmentTests.cs | 8 +-
.../Services/LayoutServiceTests.cs | 231 +++++++++++
30 files changed, 881 insertions(+), 117 deletions(-)
create mode 100644 src/Dfe.ContentSupport.Web.Node/styles/scss/vertical-navigation.scss
create mode 100644 src/Dfe.ContentSupport.Web/Models/Mapped/PageLink.cs
create mode 100644 src/Dfe.ContentSupport.Web/Services/ILayoutService.cs
create mode 100644 src/Dfe.ContentSupport.Web/Services/LayoutService.cs
create mode 100644 src/Dfe.ContentSupport.Web/Views/Shared/_Feedback.cshtml
create mode 100644 src/Dfe.ContentSupport.Web/Views/Shared/_Print.cshtml
create mode 100644 src/Dfe.ContentSupport.Web/wwwroot/assets/icon-print.png
create mode 100644 tests/Dfe.ContentSupport.Web.Tests/Services/LayoutServiceTests.cs
diff --git a/.github/workflows/code-pr-check.yml b/.github/workflows/code-pr-check.yml
index 655db94..0c43a2b 100644
--- a/.github/workflows/code-pr-check.yml
+++ b/.github/workflows/code-pr-check.yml
@@ -1,11 +1,6 @@
name: Code PR Check
on:
- push:
- branches: ["main", "Development"]
- paths:
- - "src/**"
- - "tests/**"
pull_request:
branches: ["main", "Development"]
paths:
@@ -30,16 +25,61 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 8.0.x
-
- - name: Restore dependencies
- run: dotnet restore
-
- - name: Build
- run: dotnet build --no-restore
-
- - name: Test
- run: dotnet test --no-build --verbosity normal
-
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+ cache: false
+
- name: Install dotnet coverage
run: dotnet tool install --global dotnet-coverage --version 17.9.3
+
+ - name: Cache SonarCloud packages
+ uses: actions/cache@v4
+ with:
+ path: ~\sonar\cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+
+ - name: Install SonarCloud scanners
+ run: dotnet tool install --global dotnet-sonarscanner
+
+ - name: Install latest JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: "microsoft"
+ java-version: "17"
+
+# - name: Start SonarCloud scanner
+# run: |
+# dotnet-sonarscanner begin \
+# /k:"DFE-Digital_sts-content-support" \
+# /o:"dfe-digital" \
+# /d:sonar.login="${{ secrets.SONAR_TOKEN }}" \
+# /d:sonar.host.url="https://sonarcloud.io" \
+# /d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml \
+# /d:sonar.coverage.exclusions=**/Program.cs,**/wwwroot/** \
+# /d:sonar.issue.ignore.multicriteria=e1 \
+# /d:sonar.issue.ignore.multicriteria.e1.ruleKey=csharpsquid:S6602 \
+# /d:sonar.issue.ignore.multicriteria.e1.resourceKey=src/**/*.cs
+
+ - name: Build web app
+ uses: ./.github/actions/build-dotnet-app
+ with:
+ dotnet_version: ${{ env.DOTNET_VERSION }}
+ solution_filename: sts-contentsupport.sln
+
+ - name: Run unit tests
+ uses: ./.github/actions/run-unit-tests
+ with:
+ solution_filename: sts-contentsupport.sln
+
+# - name: Merge test results
+# run: dotnet-coverage merge -f xml -o "coverage.xml" -s "coverage.settings.xml" -r coverage.cobertura.xml
+#
+# - name: End SonarCloud Scanner
+# run: dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}"
+#
+# - name: Archive code coverage results
+# uses: actions/upload-artifact@v4
+# with:
+# name: code-coverage-report
+# path: coverage.xml
+#
\ No newline at end of file
diff --git a/.github/workflows/matrix-deploy.yml b/.github/workflows/matrix-deploy.yml
index b567062..ed258f9 100644
--- a/.github/workflows/matrix-deploy.yml
+++ b/.github/workflows/matrix-deploy.yml
@@ -3,7 +3,7 @@ name: Multi stage build & deploy
on:
workflow_dispatch:
push:
- branches: ["main", "development"]
+ branches: [ "main", "Development" ]
paths:
- "src/**"
- ".github/workflows/matrix-deploy.yml"
@@ -37,20 +37,20 @@ jobs:
checked-out-sha: ${{ needs.set-env.outputs.checked-out-sha }}
create-and-tag-release:
- needs: [set-env, create-and-publish-image]
+ needs: [ set-env, create-and-publish-image ]
name: Create & Tag Release
uses: ./.github/workflows/create-tag-release.yml
secrets: inherit
deploy-to-dev:
- needs: [set-env, create-and-publish-image]
+ needs: [ set-env, create-and-publish-image ]
name: Deployment to Dev & Test
strategy:
max-parallel: 1
fail-fast: true
matrix:
- target: [Dev]
+ target: [ Dev ]
uses: ./.github/workflows/deploy-image.yml
with:
environment: ${{ matrix.target }}
diff --git a/src/Dfe.ContentSupport.Web.Node/styles/scss/vertical-navigation.scss b/src/Dfe.ContentSupport.Web.Node/styles/scss/vertical-navigation.scss
new file mode 100644
index 0000000..6327975
--- /dev/null
+++ b/src/Dfe.ContentSupport.Web.Node/styles/scss/vertical-navigation.scss
@@ -0,0 +1,41 @@
+:root {
+ --govuk-link-color: #1d70b8;
+ --govuk-black: #0b0c0c;
+}
+
+.dfe-vertical-nav__section-item {
+ list-style-type: none;
+ font-size: 1rem;
+}
+
+.dfe-vertical-nav__link {
+ display: block;
+ padding: 7px 30px 8px 10px;
+ border-left: 4px solid #b1b4b6;
+ text-decoration: none;
+}
+
+.dfe-vertical-nav__link--selected {
+ border-left: 4px solid var(--govuk-link-color);
+ background-color: #f3f2f1;
+ font-weight: bold;
+}
+
+.dfe-vertical-nav__link,
+.dfe-vertical-nav__link--selected {
+ color: var(--govuk-link-color);
+
+ &:active, &:hover {
+ background-color: #fd0;
+ color: var(--govuk-black);
+ border-left: 4px solid var(--govuk-black);
+ font-weight: normal;
+ }
+}
+
+.dfe-vertical-nav__theme {
+ border-top: 1px solid var(--govuk-link-color);
+ padding-top: 5px;
+ margin-top: 10px;
+ margin-left: 0;
+}
diff --git a/src/Dfe.ContentSupport.Web/Configuration/CsContentfulOptions.cs b/src/Dfe.ContentSupport.Web/Configuration/CsContentfulOptions.cs
index 6cdc72d..6441cf0 100644
--- a/src/Dfe.ContentSupport.Web/Configuration/CsContentfulOptions.cs
+++ b/src/Dfe.ContentSupport.Web/Configuration/CsContentfulOptions.cs
@@ -5,4 +5,5 @@ namespace Dfe.ContentSupport.Web.Configuration;
public class CsContentfulOptions : ContentfulOptions
{
public int IncludeDepth { get; set; } = 10;
+ public int RetryAttempts { get; set; } = 3;
}
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs b/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs
index d9c4ec0..681091a 100644
--- a/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs
+++ b/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs
@@ -9,7 +9,7 @@ namespace Dfe.ContentSupport.Web.Controllers;
[Route("/content")]
[AllowAnonymous]
-public class ContentController(IContentService contentService)
+public class ContentController(IContentService contentService, ILayoutService layoutService)
: Controller
{
public async Task Home()
@@ -28,14 +28,17 @@ public async Task Home()
return View(defaultModel);
}
- [HttpGet("{slug}")]
- public async Task Index(string slug, bool isPreview = false)
+ [HttpGet("{slug}/{page?}")]
+ public async Task Index(string slug, string page = "", bool isPreview = false)
{
if (!ModelState.IsValid) return RedirectToAction("error");
if (string.IsNullOrEmpty(slug)) return RedirectToAction("error");
var resp = await contentService.GetContent(slug, isPreview);
if (resp is null) return RedirectToAction("error");
+
+ resp = layoutService.GenerateLayout(resp, Request, page);
+
return View("CsIndex", resp);
}
@@ -48,6 +51,6 @@ public IActionResult Privacy()
public IActionResult Error()
{
return View(new ErrorViewModel
- { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+ { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Extensions/WebApplicationBuilderExtensions.cs b/src/Dfe.ContentSupport.Web/Extensions/WebApplicationBuilderExtensions.cs
index 16a9078..71ea637 100644
--- a/src/Dfe.ContentSupport.Web/Extensions/WebApplicationBuilderExtensions.cs
+++ b/src/Dfe.ContentSupport.Web/Extensions/WebApplicationBuilderExtensions.cs
@@ -24,6 +24,7 @@ public static void InitCsDependencyInjection(this WebApplicationBuilder app)
app.Services.AddTransient();
app.Services.AddTransient();
app.Services.AddTransient();
+ app.Services.AddTransient();
app.Services.Configure(options =>
{
diff --git a/src/Dfe.ContentSupport.Web/Http/HttpContentfulClient.cs b/src/Dfe.ContentSupport.Web/Http/HttpContentfulClient.cs
index 0ff6a75..32c446a 100644
--- a/src/Dfe.ContentSupport.Web/Http/HttpContentfulClient.cs
+++ b/src/Dfe.ContentSupport.Web/Http/HttpContentfulClient.cs
@@ -12,6 +12,22 @@ public async Task> Query(QueryBuilder queryBuilder
CancellationToken cancellationToken = default) where T : class
{
queryBuilder = queryBuilder.Include(options.IncludeDepth);
- return await GetEntries(queryBuilder, cancellationToken);
+
+ for (int attempt = 1; attempt <= options.RetryAttempts; attempt++)
+ {
+ try
+ {
+ return await GetEntries(queryBuilder, cancellationToken);
+ }
+ catch (Exception)
+ {
+ if (attempt == options.RetryAttempts)
+ {
+ throw;
+ }
+ }
+ }
+
+ return default!;
}
}
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Models/ContentBase.cs b/src/Dfe.ContentSupport.Web/Models/ContentBase.cs
index 636506b..c1b04e3 100644
--- a/src/Dfe.ContentSupport.Web/Models/ContentBase.cs
+++ b/src/Dfe.ContentSupport.Web/Models/ContentBase.cs
@@ -6,4 +6,8 @@ namespace Dfe.ContentSupport.Web.Models;
public class ContentBase : ContentType
{
public string InternalName { get; set; } = null!;
+
+ public string? Title { get; set; } = null;
+
+ public string? Subtitle { get; set; } = null;
}
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Models/Mapped/CsContentItem.cs b/src/Dfe.ContentSupport.Web/Models/Mapped/CsContentItem.cs
index ce02e65..7255ede 100644
--- a/src/Dfe.ContentSupport.Web/Models/Mapped/CsContentItem.cs
+++ b/src/Dfe.ContentSupport.Web/Models/Mapped/CsContentItem.cs
@@ -6,4 +6,6 @@ namespace Dfe.ContentSupport.Web.Models.Mapped;
public class CsContentItem
{
public string InternalName { get; set; } = null!;
+ public string? Title { get; set; } = null;
+ public string? Subtitle { get; set; } = null;
}
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Models/Mapped/CsPage.cs b/src/Dfe.ContentSupport.Web/Models/Mapped/CsPage.cs
index 2017016..24863d3 100644
--- a/src/Dfe.ContentSupport.Web/Models/Mapped/CsPage.cs
+++ b/src/Dfe.ContentSupport.Web/Models/Mapped/CsPage.cs
@@ -9,8 +9,13 @@ public class CsPage
public string Slug { get; set; } = null!;
public bool IsSitemap { get; set; }
public bool HasCitation { get; set; }
+ public bool ShowVerticalNavigation { get; set; }
public bool HasBackToTop { get; set; }
+ public bool HasPrint { get; set; }
public List Content { get; set; } = null!;
public DateTime? CreatedAt { get; init; }
public DateTime? UpdatedAt { get; init; }
+ public bool HasFeedbackBanner { get; set; }
+ public List? MenuItems { get; set; }
+
}
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Models/Mapped/PageLink.cs b/src/Dfe.ContentSupport.Web/Models/Mapped/PageLink.cs
new file mode 100644
index 0000000..8e4c640
--- /dev/null
+++ b/src/Dfe.ContentSupport.Web/Models/Mapped/PageLink.cs
@@ -0,0 +1,10 @@
+namespace Dfe.ContentSupport.Web.Models.Mapped
+{
+ public class PageLink
+ {
+ public string? Title { get; set; } = null;
+ public string? Subtitle { get; set; } = null;
+ public required string Url { get; set; }
+ public required bool IsActive { get; set; }
+ }
+}
diff --git a/src/Dfe.ContentSupport.Web/Program.cs b/src/Dfe.ContentSupport.Web/Program.cs
index 62b2e40..b1b5f4c 100644
--- a/src/Dfe.ContentSupport.Web/Program.cs
+++ b/src/Dfe.ContentSupport.Web/Program.cs
@@ -21,6 +21,7 @@ public static void Main(string[] args)
builder.Services.AddControllers();
builder.Services.AddControllersWithViews();
builder.Services.AddApplicationInsightsTelemetry();
+ builder.Services.AddHealthChecks();
builder.Services.AddGovUkFrontend();
builder.Services.AddContentful(builder.Configuration);
@@ -39,6 +40,7 @@ public static void Main(string[] args)
app.UseRouting();
app.UseAuthorization();
app.UseCookiePolicy();
+ app.MapHealthChecks("/healthz");
app.MapControllerRoute(
"Default",
@@ -55,7 +57,7 @@ public static void Main(string[] args)
app.MapControllerRoute(
name: "slug",
- pattern: "{slug}",
+ pattern: "{slug}/{page?}",
defaults: new { controller = "Content", action = "Index" });
diff --git a/src/Dfe.ContentSupport.Web/Services/ILayoutService.cs b/src/Dfe.ContentSupport.Web/Services/ILayoutService.cs
new file mode 100644
index 0000000..d028752
--- /dev/null
+++ b/src/Dfe.ContentSupport.Web/Services/ILayoutService.cs
@@ -0,0 +1,10 @@
+using Dfe.ContentSupport.Web.Models.Mapped;
+
+namespace Dfe.ContentSupport.Web.Services
+{
+ public interface ILayoutService
+ {
+ CsPage GenerateLayout(CsPage page, HttpRequest request, string pageName);
+
+ }
+}
diff --git a/src/Dfe.ContentSupport.Web/Services/LayoutService.cs b/src/Dfe.ContentSupport.Web/Services/LayoutService.cs
new file mode 100644
index 0000000..1f559c0
--- /dev/null
+++ b/src/Dfe.ContentSupport.Web/Services/LayoutService.cs
@@ -0,0 +1,85 @@
+using Dfe.ContentSupport.Web.Models;
+using Dfe.ContentSupport.Web.Models.Mapped;
+
+
+namespace Dfe.ContentSupport.Web.Services
+{
+ public class LayoutService : ILayoutService
+ {
+ public CsPage GenerateLayout(CsPage page, HttpRequest request, string pageName)
+ {
+ if (!page.ShowVerticalNavigation) return page;
+
+ return new()
+ {
+ Heading = GetHeading(page, pageName),
+ MenuItems = GenerateVerticalNavigation(page, request, pageName),
+ Content = GetVisiblePageList(page, pageName),
+ UpdatedAt = page.UpdatedAt,
+ CreatedAt = page.CreatedAt,
+ HasCitation = page.HasCitation,
+ HasBackToTop = page.HasBackToTop,
+ IsSitemap = page.IsSitemap,
+ ShowVerticalNavigation = page.ShowVerticalNavigation,
+ Slug = page.Slug,
+ };
+ }
+
+
+ public Heading GetHeading(CsPage page, string pageName)
+ {
+ var selectedPage = page.Content.Find(o => o.InternalName == pageName);
+
+ if (selectedPage != null)
+ return new()
+ {
+ Title = selectedPage.Title ?? "",
+ Subtitle = selectedPage.Subtitle ?? ""
+ };
+
+
+ return new()
+ {
+ Title = page.Content[0]?.Title ?? "",
+ Subtitle = page.Content[0]?.Subtitle ?? ""
+ };
+ }
+
+
+ public List GenerateVerticalNavigation(CsPage page, HttpRequest request, string pageName)
+ {
+ var baseUrl = GetNavigationUrl(request);
+
+ var menuItems = page.Content.Select(o => new PageLink()
+ {
+ Title = o.Title ?? "",
+ Subtitle = o.Subtitle ?? "",
+ Url = $"{baseUrl}/{o.InternalName}",
+ IsActive = pageName == o.InternalName
+ }).ToList();
+
+ if (string.IsNullOrEmpty(pageName) && menuItems.Count > 0)
+ menuItems[0].IsActive = true;
+
+ return menuItems;
+ }
+
+
+ public List GetVisiblePageList(CsPage page, string pageName)
+ {
+ if (!string.IsNullOrEmpty(pageName))
+ return page.Content.Where(o => o.InternalName == pageName).ToList();
+
+
+ return page.Content.GetRange(0, 1);
+ }
+
+
+ public string GetNavigationUrl(HttpRequest request)
+ {
+ var splitUrl = request.Path.ToString().Split("/");
+ return string.Join("/", splitUrl.Take(3));
+ }
+
+ }
+}
diff --git a/src/Dfe.ContentSupport.Web/Services/ModelMapper.cs b/src/Dfe.ContentSupport.Web/Services/ModelMapper.cs
index a9b8262..d976b76 100644
--- a/src/Dfe.ContentSupport.Web/Services/ModelMapper.cs
+++ b/src/Dfe.ContentSupport.Web/Services/ModelMapper.cs
@@ -25,7 +25,10 @@ public CsPage MapToCsPage(ContentSupportPage incoming)
IsSitemap = incoming.IsSitemap,
HasCitation = incoming.HasCitation,
HasBackToTop = incoming.HasBackToTop,
+ HasFeedbackBanner = incoming.HasFeedbackBanner,
+ HasPrint = incoming.HasPrint,
Content = MapEntriesToContent(incoming.Content),
+ ShowVerticalNavigation = incoming.ShowVerticalNavigation,
CreatedAt = incoming.Sys.CreatedAt,
UpdatedAt = incoming.Sys.UpdatedAt
};
@@ -40,18 +43,20 @@ private List MapEntriesToContent(List entries)
public CsContentItem ConvertEntryToContentItem(Entry entry)
{
CsContentItem item = entry.RichText is not null
- ? MapRichTextContent(entry.RichText)!
- : new CsContentItem { InternalName = entry.InternalName };
+ ? MapRichTextContent(entry.RichText, entry)!
+ : new CsContentItem { InternalName = entry.InternalName, Title = entry.Title, Subtitle = entry.Subtitle };
return item;
}
- public RichTextContentItem? MapRichTextContent(ContentItemBase? richText)
+ public RichTextContentItem? MapRichTextContent(ContentItemBase? richText, Entry entry)
{
if (richText is null) return null;
RichTextContentItem item =
new RichTextContentItem
{
- InternalName = richText.InternalName,
+ InternalName = entry.InternalName,
+ Title = entry.Title,
+ Subtitle = entry.Subtitle,
NodeType = ConvertToRichTextNodeType(richText.NodeType),
Content = MapRichTextNodes(richText.Content),
};
@@ -61,7 +66,7 @@ public CsContentItem ConvertEntryToContentItem(Entry entry)
public List MapRichTextNodes(List nodes)
{
return nodes.Select(node => MapContent(node) ?? new RichTextContentItem
- { NodeType = RichTextNodeType.Unknown, InternalName = node.InternalName }).ToList();
+ { NodeType = RichTextNodeType.Unknown, InternalName = node.InternalName }).ToList();
}
public RichTextContentItem? MapContent(ContentItem contentItem)
@@ -101,7 +106,7 @@ public List MapRichTextNodes(List nodes)
item = new EmbeddedEntry
{
JumpIdentifier = target.JumpIdentifier,
- RichText = MapRichTextContent(target.RichText),
+ RichText = MapRichTextContent(target.RichText, target),
CustomComponent = GenerateCustomComponent(target)
};
break;
@@ -154,7 +159,7 @@ private CustomAccordion GenerateCustomAccordion(Target target)
return new CustomAccordion
{
InternalName = target.InternalName,
- Body = MapRichTextContent(target.RichText),
+ Body = MapRichTextContent(target.RichText, target),
SummaryLine = target.SummaryLine,
Title = target.Title,
Accordions = target.Content.Select(GenerateCustomAccordion).ToList()
diff --git a/src/Dfe.ContentSupport.Web/ViewModels/ContentSupportPage.cs b/src/Dfe.ContentSupport.Web/ViewModels/ContentSupportPage.cs
index 90b0f42..7668062 100644
--- a/src/Dfe.ContentSupport.Web/ViewModels/ContentSupportPage.cs
+++ b/src/Dfe.ContentSupport.Web/ViewModels/ContentSupportPage.cs
@@ -17,4 +17,8 @@ public class ContentSupportPage : ContentBase
public bool IsSitemap { get; init; }
public bool HasCitation { get; init; }
public bool HasBackToTop { get; init; }
+ public bool HasFeedbackBanner { get; init; }
+ public bool HasPrint { get; init; }
+ public bool ShowVerticalNavigation { get; init; }
+
}
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Views/Content/CsIndex.cshtml b/src/Dfe.ContentSupport.Web/Views/Content/CsIndex.cshtml
index 940966e..8ae4e3f 100644
--- a/src/Dfe.ContentSupport.Web/Views/Content/CsIndex.cshtml
+++ b/src/Dfe.ContentSupport.Web/Views/Content/CsIndex.cshtml
@@ -5,21 +5,52 @@
}
+
+ @if (Model.MenuItems is not null)
+ {
+
+
+
+
+ @foreach (var menuItem in Model.MenuItems)
+ {
+
+
+
+ }
+
+
+
+
+ }
+
@foreach (var content in Model.Content)
{
-
+
}
+
+
@if (Model.HasCitation)
{
-
+
}
@if (Model.HasBackToTop)
{
-
+
+ }
+
+ @if (Model.HasPrint)
+ {
+
+ }
+
+ @if (Model.HasFeedbackBanner)
+ {
+
}
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Views/Shared/Error.cshtml b/src/Dfe.ContentSupport.Web/Views/Shared/Error.cshtml
index e44e743..5c54262 100644
--- a/src/Dfe.ContentSupport.Web/Views/Shared/Error.cshtml
+++ b/src/Dfe.ContentSupport.Web/Views/Shared/Error.cshtml
@@ -10,7 +10,7 @@
{
Request ID: @Model.RequestId
-
+
}
Development Mode
diff --git a/src/Dfe.ContentSupport.Web/Views/Shared/RichText/Custom/_Attachment.cshtml b/src/Dfe.ContentSupport.Web/Views/Shared/RichText/Custom/_Attachment.cshtml
index 1b528f2..e7076ef 100644
--- a/src/Dfe.ContentSupport.Web/Views/Shared/RichText/Custom/_Attachment.cshtml
+++ b/src/Dfe.ContentSupport.Web/Views/Shared/RichText/Custom/_Attachment.cshtml
@@ -44,7 +44,7 @@
-
+
@fileExtension.ToUpper() ,
@(Model.Size / 1024) KB
diff --git a/src/Dfe.ContentSupport.Web/Views/Shared/_BetaHeader.cshtml b/src/Dfe.ContentSupport.Web/Views/Shared/_BetaHeader.cshtml
index a0c8d92..70580a5 100644
--- a/src/Dfe.ContentSupport.Web/Views/Shared/_BetaHeader.cshtml
+++ b/src/Dfe.ContentSupport.Web/Views/Shared/_BetaHeader.cshtml
@@ -3,7 +3,8 @@
@* Beta *@
Beta
This is a new service - your
-
+
feedback
will
help us to improve it.
diff --git a/src/Dfe.ContentSupport.Web/Views/Shared/_CsHeader.cshtml b/src/Dfe.ContentSupport.Web/Views/Shared/_CsHeader.cshtml
index 94f8019..0fb2a92 100644
--- a/src/Dfe.ContentSupport.Web/Views/Shared/_CsHeader.cshtml
+++ b/src/Dfe.ContentSupport.Web/Views/Shared/_CsHeader.cshtml
@@ -7,4 +7,7 @@
+
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Views/Shared/_CsLayout.cshtml b/src/Dfe.ContentSupport.Web/Views/Shared/_CsLayout.cshtml
index d4a3147..fa842bf 100644
--- a/src/Dfe.ContentSupport.Web/Views/Shared/_CsLayout.cshtml
+++ b/src/Dfe.ContentSupport.Web/Views/Shared/_CsLayout.cshtml
@@ -7,7 +7,7 @@
ViewData["Title"] = Model.Heading.Title;
ViewData["containerClasses"] = "dfe-width-container";
var consentCookie = Context.Request.Cookies[".AspNet.Consent"];
- var track = false;// consentCookie == "true";
+ var track = consentCookie == "true";
}
@section Head {
@@ -32,9 +32,7 @@
diff --git a/src/Dfe.ContentSupport.Web/Views/Shared/_Feedback.cshtml b/src/Dfe.ContentSupport.Web/Views/Shared/_Feedback.cshtml
new file mode 100644
index 0000000..09a9d3f
--- /dev/null
+++ b/src/Dfe.ContentSupport.Web/Views/Shared/_Feedback.cshtml
@@ -0,0 +1,36 @@
+@{
+ var consentCookie = Context.Request.Cookies[".AspNet.Consent"];
+ var track = consentCookie == "true";
+}
+
+@if (track)
+{
+
+
+
+}
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/Views/Shared/_Print.cshtml b/src/Dfe.ContentSupport.Web/Views/Shared/_Print.cshtml
new file mode 100644
index 0000000..402ac26
--- /dev/null
+++ b/src/Dfe.ContentSupport.Web/Views/Shared/_Print.cshtml
@@ -0,0 +1,3 @@
+
+ Print this page
+
\ No newline at end of file
diff --git a/src/Dfe.ContentSupport.Web/wwwroot/assets/icon-print.png b/src/Dfe.ContentSupport.Web/wwwroot/assets/icon-print.png
new file mode 100644
index 0000000000000000000000000000000000000000..54737a3739c0d7ef981a79103756663725a82038
GIT binary patch
literal 213
zcmeAS@N?(olHy`uVBq!ia0vp^0zfRp!3HFQtmCqP6lZ})WHAE+w=f7ZGR&GI0Tg5`
z4sv&5Sa(k5C6L3C?~tz_78O`%fY(ke}u0;uuoFcy^MtP=f-G3-4sFhyU1Fb4xXJ
ze(10`9bBF8De-U{-+|1x-AVo&U*_)&t*MSbqW65#ZpWiRJ o.GetContent(dummySlug, isPreview), Times.Once);
}
diff --git a/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAccordionTests.cs b/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAccordionTests.cs
index d34d40f..dd1b845 100644
--- a/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAccordionTests.cs
+++ b/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAccordionTests.cs
@@ -46,7 +46,7 @@ public class CustomAccordionTests
],
RichText = new ContentItem
{
- InternalName = ContentInternalName,
+ InternalName = null,
NodeType = "paragraph"
}
}
@@ -75,7 +75,7 @@ public void MapCorrectly()
var expectedBody= new RichTextContentItem
{
- InternalName = ContentInternalName,
+ InternalName = InternalName,
NodeType = RichTextNodeType.Paragraph,
Content = []
};
diff --git a/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAttachmentTests.cs b/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAttachmentTests.cs
index 62831d2..7b2ffdf 100644
--- a/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAttachmentTests.cs
+++ b/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAttachmentTests.cs
@@ -50,10 +50,12 @@ public class CustomAttachmentTests
Details = new FileDetails
{
Size = Size
- }
+ },
},
- SystemProperties =new SystemProperties()
-
+ SystemProperties = new SystemProperties
+ {
+ UpdatedAt = DateTime.Now
+ }
}
}
}
diff --git a/tests/Dfe.ContentSupport.Web.Tests/Services/LayoutServiceTests.cs b/tests/Dfe.ContentSupport.Web.Tests/Services/LayoutServiceTests.cs
new file mode 100644
index 0000000..d1c405a
--- /dev/null
+++ b/tests/Dfe.ContentSupport.Web.Tests/Services/LayoutServiceTests.cs
@@ -0,0 +1,231 @@
+
+using Dfe.ContentSupport.Web.Models.Mapped;
+using Microsoft.AspNetCore.Http;
+
+
+namespace Dfe.ContentSupport.Web.Tests.Services
+{
+ public class LayoutServiceTests
+ {
+ private readonly LayoutService _layoutService = new();
+
+ private readonly string Home = "Home";
+ private readonly string About = "About";
+ private readonly string Contact = "Contact";
+ private readonly string HomeTitle = "Home Title";
+ private readonly string AboutTitle = "About Title";
+ private readonly string HomeSubtitle = "Home Subtitle";
+ private readonly string AboutSubtitle = "About Subtitle";
+
+ private CsPage GetPage()
+ {
+ return new CsPage
+ {
+ Content = new()
+ {
+ new () { InternalName = Home, Title = HomeTitle, Subtitle = HomeSubtitle },
+ new () { InternalName = About, Title = AboutTitle, Subtitle = AboutSubtitle }
+ }
+ };
+ }
+
+
+ private string GetSegmentLength(int length)
+ {
+ var segment = "";
+ for (var i = 1; i <= length; i++)
+ {
+ segment += $"/segment{i}";
+ }
+
+ return segment;
+ }
+
+
+ [Fact]
+ public void GetHeading_PageExists_ReturnsCorrectHeading()
+ {
+ // Arrange
+ var page = GetPage();
+
+ // Act
+ var result = _layoutService.GetHeading(page, About);
+
+ // Assert
+ Assert.Equal(AboutTitle, result.Title);
+ Assert.Equal(AboutSubtitle, result.Subtitle);
+ }
+
+
+ [Fact]
+ public void GetHeading_PageDoesNotExist_ReturnsFirstPageHeading()
+ {
+ // Arrange
+ var page = GetPage();
+
+ // Act
+ var result = _layoutService.GetHeading(page, Contact);
+
+ // Assert
+ Assert.Equal(HomeTitle, result.Title);
+ Assert.Equal(HomeSubtitle, result.Subtitle);
+ }
+
+
+ [Fact]
+ public void GenerateVerticalNavigation_PageNameMatches_ReturnsCorrectMenuItems()
+ {
+ // Arrange
+ var page = GetPage();
+
+ var request = new DefaultHttpContext().Request;
+
+ // Act
+ var result = _layoutService.GenerateVerticalNavigation(page, request, About);
+
+ // Assert
+ Assert.Equal(page.Content.Count, result.Count);
+ Assert.Equal(AboutTitle, result[1].Title);
+ Assert.True(result[1].IsActive);
+ }
+
+
+ [Fact]
+ public void GenerateVerticalNavigation_PageNameDoesNotMatch_ReturnsMenuItemsWithFirstActive()
+ {
+ // Arrange
+ var page = GetPage();
+
+ var request = new DefaultHttpContext().Request;
+
+ // Act
+ var result = _layoutService.GenerateVerticalNavigation(page, request, Contact);
+
+ // Assert
+ Assert.Equal(page.Content.Count, result.Count);
+ Assert.Equal(HomeTitle, result[0].Title);
+ Assert.Equal(0, result.Count(o => o.IsActive));
+ }
+
+
+ [Fact]
+ public void GetVisiblePageList_PageNameProvidedAndMatches_ReturnsMatchingItems()
+ {
+ // Arrange
+ var page = GetPage();
+
+ // Act
+ var result = _layoutService.GetVisiblePageList(page, About);
+
+ // Assert
+ Assert.Single(result);
+ Assert.Equal(About, result[0].InternalName);
+ }
+
+
+ [Fact]
+ public void GetVisiblePageList_PageNameProvidedAndDoesNotMatch_ReturnsEmptyList()
+ {
+ // Arrange
+ var page = GetPage();
+
+ // Act
+ var result = _layoutService.GetVisiblePageList(page, Contact);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+
+ [Fact]
+ public void GetVisiblePageList_PageNameIsNullOrEmpty_ReturnsFirstItem()
+ {
+ // Arrange
+ var page = GetPage();
+
+ // Act
+ var result = _layoutService.GetVisiblePageList(page, string.Empty);
+
+ // Assert
+ Assert.Single(result);
+ Assert.Equal(Home, result[0].InternalName);
+ }
+
+
+ [Fact]
+ public void GetVisiblePageList_ContentListIsEmpty_ReturnsEmptyList()
+ {
+ // Arrange
+ var page = GetPage();
+ page.Content = new();
+
+ // Act
+ var result = _layoutService.GetVisiblePageList(page, Home);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+
+ [Fact]
+ public void GetNavigationUrl_MoreThanTwoSegments_ReturnsFirstTwoSegments()
+ {
+ // Arrange
+ var context = new DefaultHttpContext();
+ context.Request.Path = GetSegmentLength(4);
+
+ // Act
+ var result = _layoutService.GetNavigationUrl(context.Request);
+
+ // Assert
+ Assert.Equal(GetSegmentLength(2), result);
+ }
+
+
+ [Fact]
+ public void GetNavigationUrl_ExactlyTwoSegments_ReturnsAllSegments()
+ {
+ // Arrange
+ var context = new DefaultHttpContext();
+ context.Request.Path = GetSegmentLength(2);
+
+ // Act
+ var result = _layoutService.GetNavigationUrl(context.Request);
+
+ // Assert
+ Assert.Equal(GetSegmentLength(2), result);
+ }
+
+
+ [Fact]
+ public void GetNavigationUrl_FewerThanTwoSegments_ReturnsAllSegments()
+ {
+ // Arrange
+ var context = new DefaultHttpContext();
+ context.Request.Path = GetSegmentLength(1);
+
+ // Act
+ var result = _layoutService.GetNavigationUrl(context.Request);
+
+ // Assert
+ Assert.Equal(GetSegmentLength(1), result);
+ }
+
+
+ [Fact]
+ public void GetNavigationUrl_EmptyUrl_ReturnsEmptyString()
+ {
+ // Arrange
+ var context = new DefaultHttpContext();
+ var emptyRequestPath = string.Empty;
+ context.Request.Path = emptyRequestPath;
+
+ // Act
+ var result = _layoutService.GetNavigationUrl(context.Request);
+
+ // Assert
+ Assert.Equal(emptyRequestPath, result);
+ }
+
+ }
+}