Skip to content

Commit 6f184eb

Browse files
authored
Feature/231048-notifcations (#45)
1 parent 2bbb6db commit 6f184eb

13 files changed

Lines changed: 405 additions & 153 deletions

File tree

src/DfE.ExternalApplications.Application/DfE.ExternalApplications.Application.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="GovUK.Dfe.ExternalApplications.Api.Client" Version="0.1.19" />
10+
<PackageReference Include="DfE.CoreLibs.Notifications" Version="0.1.0-prerelease-135" />
11+
<PackageReference Include="GovUK.Dfe.ExternalApplications.Api.Client" Version="0.1.20" />
1112
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="2.3.0" />
1213
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.0" />
1314
</ItemGroup>

src/DfE.ExternalApplications.Domain/DfE.ExternalApplications.Domain.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="DfE.CoreLibs.Contracts" Version="1.0.42" />
10+
<PackageReference Include="DfE.CoreLibs.Contracts" Version="1.0.43" />
1111
</ItemGroup>
1212

1313
</Project>

src/DfE.ExternalApplications.Infrastructure/Services/FieldFormattingService.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,11 @@ private bool LooksLikeUploadData(string value)
251251
if (doc.RootElement.ValueKind == JsonValueKind.Array && doc.RootElement.GetArrayLength() > 0)
252252
{
253253
var first = doc.RootElement[0];
254-
return first.ValueKind == JsonValueKind.Object && first.TryGetProperty("OriginalFileName", out _);
254+
// Check for upload file properties - either OriginalFileName or Name with file-like properties
255+
return first.ValueKind == JsonValueKind.Object &&
256+
(first.TryGetProperty("originalFileName", out _) ||
257+
(first.TryGetProperty("name", out _) &&
258+
(first.TryGetProperty("fileSize", out _) || first.TryGetProperty("id", out _))));
255259
}
256260
}
257261
catch
@@ -270,7 +274,15 @@ private string FormatUploadValue(string value)
270274
if (doc.RootElement.ValueKind == JsonValueKind.Array)
271275
{
272276
var names = doc.RootElement.EnumerateArray()
273-
.Select(e => e.TryGetProperty("OriginalFileName", out var n) ? n.GetString() ?? string.Empty : string.Empty)
277+
.Select(e =>
278+
{
279+
// Try OriginalFileName first, then fall back to Name if OriginalFileName doesn't exist
280+
if (e.TryGetProperty("originalFileName", out var originalFileName))
281+
return originalFileName.GetString() ?? string.Empty;
282+
if (e.TryGetProperty("name", out var name))
283+
return name.GetString() ?? string.Empty;
284+
return string.Empty;
285+
})
274286
.Where(n => !string.IsNullOrEmpty(n));
275287
return string.Join("<br />", names);
276288
}
@@ -290,7 +302,15 @@ private List<string> FormatUploadValuesList(string value)
290302
if (doc.RootElement.ValueKind == JsonValueKind.Array)
291303
{
292304
var names = doc.RootElement.EnumerateArray()
293-
.Select(e => e.TryGetProperty("OriginalFileName", out var n) ? n.GetString() ?? string.Empty : string.Empty)
305+
.Select(e =>
306+
{
307+
// Try OriginalFileName first, then fall back to Name if OriginalFileName doesn't exist
308+
if (e.TryGetProperty("originalFileName", out var originalFileName))
309+
return originalFileName.GetString() ?? string.Empty;
310+
if (e.TryGetProperty("name", out var name))
311+
return name.GetString() ?? string.Empty;
312+
return string.Empty;
313+
})
294314
.Where(n => !string.IsNullOrEmpty(n))
295315
.ToList();
296316
return names;

src/DfE.ExternalApplications.Web/Pages/Applications/Dashboard.cshtml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
<div class="govuk-grid-row">
1616
<div class="govuk-grid-column-full govuk-!-margin-bottom-6">
17+
@* Notification Display *@
18+
@await Html.PartialAsync("_NotificationDisplay")
19+
1720
<h1 class="govuk-heading-xl govuk-!-margin-bottom-9">
1821
Your applications
1922
</h1>

src/DfE.ExternalApplications.Web/Pages/Applications/TaskSummary.cshtml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
<govuk-back-link href="/applications/@Model.ReferenceNumber">Back to application task list</govuk-back-link>
99

10+
@* Notification Display *@
11+
@await Html.PartialAsync("_NotificationDisplay")
12+
1013
<h1 class="govuk-heading-xl govuk-!-margin-bottom-6">@Model.CurrentTask.TaskName</h1>
1114

1215
<dl class="govuk-summary-list">
@@ -26,6 +29,11 @@
2629
var formattedValues = Model.GetFormattedFieldValues(field.FieldId);
2730
var itemLabel = Model.GetFieldItemLabel(field.FieldId);
2831
var allowMultiple = Model.IsFieldAllowMultiple(field.FieldId);
32+
33+
// Check if this is specifically an autocomplete complex field (not upload or other types)
34+
var isAutocompleteField = field.Type == "autocomplete" ||
35+
(field.Type == "complexField" && field.ComplexField != null &&
36+
ComplexFieldConfigurationService.GetConfiguration(field.ComplexField.Id).FieldType.Equals("autocomplete", StringComparison.OrdinalIgnoreCase));
2937

3038
var itemLabelHyphenated = itemLabel.Replace(" ", "-").ToLower();
3139

@@ -68,7 +76,7 @@
6876
@Html.Raw(formattedValues[i])
6977
</dd>
7078
<dd class="govuk-summary-list__actions">
71-
@if (Model.IsApplicationEditable())
79+
@if (Model.IsApplicationEditable() && isAutocompleteField)
7280
{
7381
<a class="govuk-link" href="@changeUrl?removeItem=@i" id="field-@itemLabelHyphenated-@fieldIndex-remove-link" data-testid="field-@itemLabelHyphenated-@fieldIndex-remove-link">
7482
Remove<span class="govuk-visually-hidden"> @($"{itemLabel} {fieldIndex}")</span>

src/DfE.ExternalApplications.Web/Pages/FormEngine/RenderForm.cshtml

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
<form method="post" asp-page-handler="Page">
2929
<input type="hidden" asp-for="CurrentPageId" />
3030
<govuk-back-link href="/applications/@Model.ReferenceNumber/@Model.TaskId/summary">Back</govuk-back-link>
31+
32+
@* Notification Display *@
33+
@await Html.PartialAsync("_NotificationDisplay")
34+
3135
<h1 class="govuk-heading-xl govuk-!-margin-bottom-6">@Model.CurrentTask.TaskName</h1>
3236

3337
<govuk-error-summary>
@@ -55,15 +59,38 @@
5559
var fieldModelState = ModelState.Where(f => f.Key == field.FieldId).FirstOrDefault();
5660
var errorMessage = fieldModelState.Value?.Errors?.FirstOrDefault()?.ErrorMessage ?? String.Empty;
5761

58-
@await FieldRenderer.RenderFieldAsync(field, "Data", currentValue, errorMessage)
59-
;
62+
@* Only render non-upload fields inside the main form *@
63+
@if (field.Type != "complexField" ||
64+
field.ComplexField == null ||
65+
!ComplexFieldConfigurationService.GetConfiguration(field.ComplexField.Id).FieldType.Equals("upload", StringComparison.OrdinalIgnoreCase))
66+
{
67+
@await FieldRenderer.RenderFieldAsync(field, "Data", currentValue, errorMessage)
68+
}
6069
}
6170

6271
@if (!hasUploadField)
6372
{
6473
<govuk-button type="submit" name="handler" value="Page" id="save-and-continue-button">Save and continue</govuk-button>
6574
}
6675
</form>
76+
77+
@* Render upload fields outside the main form to avoid nesting *@
78+
@if (hasUploadField)
79+
{
80+
@foreach (var field in Model.CurrentPage.Fields.OrderBy(f => f.Order))
81+
{
82+
@if (field.Type == "complexField" &&
83+
field.ComplexField != null &&
84+
ComplexFieldConfigurationService.GetConfiguration(field.ComplexField.Id).FieldType.Equals("upload", StringComparison.OrdinalIgnoreCase))
85+
{
86+
var currentValue = Model.Data.TryGetValue(field.FieldId, out var val) ? val.ToString() : String.Empty;
87+
var fieldModelState = ModelState.Where(f => f.Key == field.FieldId).FirstOrDefault();
88+
var errorMessage = fieldModelState.Value?.Errors?.FirstOrDefault()?.ErrorMessage ?? String.Empty;
89+
90+
@await FieldRenderer.RenderFieldAsync(field, "Data", currentValue, errorMessage)
91+
}
92+
}
93+
}
6794
}
6895
else
6996
{
Lines changed: 17 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,25 @@
11
@page "/applications/{referenceNumber}/{taskId?}/{pageId?}/upload-file"
22
@model DfE.ExternalApplications.Web.Pages.FormEngine.UploadFileModel
33
@{
4-
ViewData["Title"] = "Upload File";
5-
var applicationId = Model.ApplicationId;
6-
var fieldId = Model.FieldId;
7-
var files = Model.Files;
8-
var successMessage = Model.SuccessMessage;
9-
var errorMessage = Model.ErrorMessage;
4+
ViewData["Title"] = "Upload File - Backend Handler";
105
}
11-
<govuk-back-link href="/applications/@Model.ReferenceNumber/@Model.TaskId/@Model.CurrentPageId" />
12-
<h1 class="govuk-heading-xl">Upload File</h1>
13-
@if (!string.IsNullOrEmpty(successMessage))
14-
{
15-
<div class="govuk-notification-banner govuk-notification-banner--success" role="alert" data-module="govuk-notification-banner">
16-
<div class="govuk-notification-banner__header">
17-
<h2 class="govuk-notification-banner__title" id="govuk-notification-banner-title">Success</h2>
18-
</div>
19-
<div class="govuk-notification-banner__content">
20-
<p class="govuk-notification-banner__heading">@successMessage</p>
21-
</div>
22-
</div>
23-
}
24-
@if (!string.IsNullOrEmpty(errorMessage))
6+
7+
@*
8+
This page now serves as a backend handler only.
9+
The UI has been moved to _UploadComplexField.cshtml partial.
10+
This page handles form submissions and redirects back to the original page.
11+
*@
12+
13+
@if (!string.IsNullOrEmpty(Model.ReturnUrl))
2514
{
26-
<div class="govuk-error-summary" data-module="govuk-error-summary">
27-
<div role="alert">
28-
<h2 class="govuk-error-summary__title">There is a problem</h2>
29-
<div class="govuk-error-summary__body">
30-
<p class="govuk-body">@errorMessage</p>
31-
</div>
32-
</div>
33-
</div>
15+
<script>
16+
// If there's a return URL but we're still on this page (shouldn't happen), redirect
17+
window.location.href = '@Model.ReturnUrl';
18+
</script>
3419
}
35-
<govuk-error-summary>
36-
@if (!ViewData.ModelState.IsValid)
37-
{
38-
@foreach (var modelError in ViewData.ModelState.Where(x => x.Value.Errors.Count > 0))
39-
{
40-
@foreach (var error in modelError.Value.Errors)
41-
{
42-
<govuk-error-summary-item>
43-
@error.ErrorMessage
44-
</govuk-error-summary-item>
45-
}
46-
}
47-
}
48-
</govuk-error-summary>
49-
50-
<form method="post" asp-page-handler="UploadFile" enctype="multipart/form-data">
51-
<input type="hidden" name="ApplicationId" value="@applicationId" />
52-
<input type="hidden" name="FieldId" value="@fieldId" />
53-
<div class="govuk-form-group">
54-
<label class="govuk-label" for="upload-file">File</label>
55-
<input class="govuk-file-upload" id="upload-file" name="UploadFile" type="file" required />
56-
</div>
57-
<div class="govuk-form-group">
58-
<label class="govuk-label" for="upload-name">Name</label>
59-
<input class="govuk-input" id="upload-name" name="UploadName" type="text" placeholder="File name (optional)" />
60-
</div>
61-
<div class="govuk-form-group">
62-
<label class="govuk-label" for="upload-desc">Description</label>
63-
<input class="govuk-input" id="upload-desc" name="UploadDescription" type="text" placeholder="Description (optional)" />
64-
</div>
65-
<button class="govuk-button" type="submit">Upload</button>
66-
</form>
67-
@if (files.Any())
20+
else
6821
{
69-
<h3 class="govuk-heading-m">Uploaded files</h3>
70-
<table class="govuk-table">
71-
<thead class="govuk-table__head">
72-
<tr class="govuk-table__row">
73-
<th class="govuk-table__header">File name</th>
74-
<th class="govuk-table__header">Description</th>
75-
<th class="govuk-table__header">Uploaded on</th>
76-
<th class="govuk-table__header">Actions</th>
77-
</tr>
78-
</thead>
79-
<tbody class="govuk-table__body">
80-
@foreach (var file in files)
81-
{
82-
<tr class="govuk-table__row">
83-
<td class="govuk-table__cell">@file.OriginalFileName</td>
84-
<td class="govuk-table__cell">@file.Description</td>
85-
<td class="govuk-table__cell">@file.UploadedOn.ToString("g")</td>
86-
<td class="govuk-table__cell">
87-
<form method="post" asp-page-handler="DownloadFile" style="display:inline">
88-
<input type="hidden" name="FileId" value="@file.Id" />
89-
<input type="hidden" name="FieldId" value="@fieldId" />
90-
<input type="hidden" name="ApplicationId" value="@applicationId" />
91-
<govuk-button type="submit" id="download-file">Download</govuk-button>
92-
</form>
93-
<form method="post" asp-page-handler="DeleteFile" style="display:inline">
94-
<input type="hidden" name="FileId" value="@file.Id" />
95-
<input type="hidden" name="FieldId" value="@fieldId" />
96-
<input type="hidden" name="ApplicationId" value="@applicationId" />
97-
<button type="submit" class="govuk-button govuk-button--warning govuk-button--small"
98-
data-module="govuk-button"
99-
onclick="return confirm('Are you sure you want to delete this file?');">
100-
Remove
101-
</button>
102-
</form>
103-
</td>
104-
</tr>
105-
}
106-
</tbody>
107-
</table>
22+
<h1>Upload File Handler</h1>
23+
<p>This page handles file upload operations. You should not see this page directly.</p>
24+
<a href="/applications/@Model.ReferenceNumber/@Model.TaskId/@Model.CurrentPageId" class="govuk-link">Return to form</a>
10825
}

src/DfE.ExternalApplications.Web/Pages/FormEngine/UploadFile.cshtml.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@
44
using Microsoft.AspNetCore.Mvc;
55
using Microsoft.AspNetCore.Mvc.RazorPages;
66
using System.Text.Json;
7+
using DfE.CoreLibs.Notifications.Interfaces;
8+
using DfE.CoreLibs.Notifications.Models;
79

810
namespace DfE.ExternalApplications.Web.Pages.FormEngine
911
{
1012
public class UploadFileModel(
1113
IFileUploadService fileUploadService,
12-
IApplicationResponseService applicationResponseService)
14+
IApplicationResponseService applicationResponseService,
15+
INotificationService notificationService)
1316
: PageModel
1417
{
1518
[BindProperty(SupportsGet = true)] public string ApplicationId { get; set; }
1619
[BindProperty(SupportsGet = true)] public string FieldId { get; set; }
1720
[BindProperty(SupportsGet = true, Name = "referenceNumber")] public string ReferenceNumber { get; set; }
1821
[BindProperty(SupportsGet = true, Name = "taskId")] public string TaskId { get; set; }
1922
[BindProperty(SupportsGet = true, Name = "pageId")] public string CurrentPageId { get; set; }
23+
[BindProperty] public string ReturnUrl { get; set; }
2024
public IReadOnlyList<UploadDto> Files { get; set; } = new List<UploadDto>();
2125
public string SuccessMessage { get; set; }
2226
public string ErrorMessage { get; set; }
@@ -31,6 +35,12 @@ public async Task<IActionResult> OnGetAsync()
3135

3236
public async Task<IActionResult> OnPostUploadFileAsync()
3337
{
38+
var notificationOptions = new NotificationOptions
39+
{
40+
Context = FieldId,
41+
Category = "file-upload"
42+
};
43+
3444
if (!Guid.TryParse(ApplicationId, out var appId))
3545
return NotFound();
3646
var file = Request.Form.Files["UploadFile"];
@@ -56,13 +66,35 @@ public async Task<IActionResult> OnPostUploadFileAsync()
5666
if (string.IsNullOrEmpty(ErrorMessage))
5767
{
5868
await SaveUploadedFilesToResponseAsync(appId, FieldId, Files);
69+
70+
// If we have a return URL (from partial form), redirect back
71+
if (!string.IsNullOrEmpty(ReturnUrl))
72+
{
73+
await notificationService.AddSuccessAsync(SuccessMessage, notificationOptions);
74+
return Redirect(ReturnUrl);
75+
}
76+
}
77+
else
78+
{
79+
// If there's an error and we have a return URL, redirect back with error
80+
if (!string.IsNullOrEmpty(ReturnUrl))
81+
{
82+
await notificationService.AddErrorAsync(ErrorMessage, notificationOptions);
83+
return Redirect(ReturnUrl);
84+
}
5985
}
6086

6187
return Page();
6288
}
6389

6490
public async Task<IActionResult> OnPostDeleteFileAsync()
6591
{
92+
var notificationOptions = new NotificationOptions
93+
{
94+
Context = FieldId,
95+
Category = "file-upload"
96+
};
97+
6698
if (!Guid.TryParse(ApplicationId, out var appId))
6799
return NotFound();
68100
var fileIdStr = Request.Form["FileId"].ToString();
@@ -84,6 +116,13 @@ public async Task<IActionResult> OnPostDeleteFileAsync()
84116
if (string.IsNullOrEmpty(ErrorMessage))
85117
{
86118
await SaveUploadedFilesToResponseAsync(appId, FieldId, Files);
119+
120+
// If we have a return URL (from partial form), redirect back
121+
if (!string.IsNullOrEmpty(ReturnUrl))
122+
{
123+
await notificationService.AddSuccessAsync(SuccessMessage, notificationOptions);
124+
return Redirect(ReturnUrl);
125+
}
87126
}
88127

89128
return Page();

src/DfE.ExternalApplications.Web/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using Microsoft.AspNetCore.Mvc.Rendering;
2424
using Microsoft.AspNetCore.Mvc.ViewFeatures;
2525
using System.Diagnostics.CodeAnalysis;
26+
using DfE.CoreLibs.Notifications.Extensions;
2627

2728

2829
var builder = WebApplication.CreateBuilder(args);
@@ -145,6 +146,8 @@
145146

146147
builder.Services.AddSingleton<ITemplateStore, ApiTemplateStore>();
147148

149+
builder.Services.AddNotificationServices();
150+
148151
// Add test token handler and services when test authentication is enabled
149152
if (isTestAuthEnabled)
150153
{

0 commit comments

Comments
 (0)