Skip to content

Commit bc12a28

Browse files
Confirmation integration with allowMultiple for autocomplete (#66)
* Confirmation integration with allowMultiple for autocomplete * Integration for allowMultiple for autocomplete * Styled remove button for when it should look like a link and added back link to collection flow summary * Updated summary page for showing label for upload files even when nothing has been uploaded
1 parent d242389 commit bc12a28

10 files changed

Lines changed: 467 additions & 106 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public string GetFieldDisplayName(string fieldName)
7474
{
7575
{ "trustName", "Trust Name" },
7676
{ "ukprn", "UKPRN" },
77+
{ "urn", "URN" },
7778
{ "companiesHouseNumber", "Companies House Number" },
7879
{ "contributorEmail", "Email Address" },
7980
{ "contributorName", "Full Name" },

src/DfE.ExternalApplications.Web/Extensions/ConfirmationExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ public static IHtmlContent RenderConfirmationButton(
3636
html.Append($" id=\"{buttonId}\"");
3737
}
3838

39+
// Add any additional attributes (e.g. style)
40+
if (additionalAttributes != null)
41+
{
42+
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(additionalAttributes);
43+
foreach (var attribute in attributes)
44+
{
45+
var attributeName = attribute.Key;
46+
var attributeValue = System.Net.WebUtility.HtmlEncode(attribute.Value?.ToString());
47+
html.Append($" {attributeName}=\"{attributeValue}\"");
48+
}
49+
}
50+
3951
// Additional attributes placeholder
4052
html.Append(">");
4153
html.Append(buttonText);
@@ -128,6 +140,10 @@ public static IHtmlContent RenderLinkConfirmationButton(
128140
displayFields: displayFields,
129141
buttonType: "submit",
130142
buttonId: buttonId,
143+
additionalAttributes: new
144+
{
145+
style = "background: none; border: 0; padding: 0; font: inherit; cursor: pointer; font-family: GDS Transport, arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-decoration: underline; text-decoration-thickness: max(1px, .0625rem); text-underline-offset: .1578em; color: #1d70b8;"
146+
},
131147
title: title);
132148
}
133149
}

src/DfE.ExternalApplications.Web/Filters/ConfirmationInterceptorFilter.cs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,40 @@ public void OnActionExecuting(ActionExecutingContext context)
6868
// Create confirmation request
6969
var (title, message) = ReadCustomMeta(form, confirmationInfo.Handler);
7070

71+
// Allow overriding the action path via hidden meta field
72+
var overrideActionKey = $"confirmation-action-{confirmationInfo.Handler}";
73+
var originalPath = context.HttpContext.Request.Path;
74+
if (form.ContainsKey(overrideActionKey))
75+
{
76+
var values = form[overrideActionKey];
77+
var desired = values.Count > 0 ? values[0] : string.Empty;
78+
if (!string.IsNullOrWhiteSpace(desired))
79+
{
80+
// Guard against accidental CSV joining from multiple inputs
81+
originalPath = desired.Split(',')[0].Trim();
82+
}
83+
}
84+
85+
// Allow overriding the return URL (where to go on cancel/back)
86+
var overrideReturnKey = $"confirmation-return-{confirmationInfo.Handler}";
87+
var returnUrl = $"{context.HttpContext.Request.Path}{context.HttpContext.Request.QueryString}";
88+
if (form.ContainsKey(overrideReturnKey))
89+
{
90+
var values = form[overrideReturnKey];
91+
var desiredReturn = values.Count > 0 ? values[0] : string.Empty;
92+
if (!string.IsNullOrWhiteSpace(desiredReturn))
93+
{
94+
returnUrl = desiredReturn.Split(',')[0].Trim();
95+
}
96+
}
97+
7198
var confirmationRequest = new ConfirmationRequest
7299
{
73-
OriginalPagePath = context.HttpContext.Request.Path,
100+
OriginalPagePath = originalPath,
74101
OriginalHandler = confirmationInfo.Handler,
75102
OriginalFormData = ExtractFormData(form),
76103
DisplayFields = confirmationInfo.DisplayFields,
77-
ReturnUrl = $"{context.HttpContext.Request.Path}{context.HttpContext.Request.QueryString}",
104+
ReturnUrl = returnUrl,
78105
Title = title
79106
};
80107

@@ -148,13 +175,39 @@ public async System.Threading.Tasks.Task OnPageHandlerExecutionAsync(PageHandler
148175

149176
var (title2, message2) = ReadCustomMeta(form, confirmationInfo.Handler);
150177

178+
// Allow overriding the action path via hidden meta field
179+
var overrideActionKey2 = $"confirmation-action-{confirmationInfo.Handler}";
180+
var originalPath2 = context.HttpContext.Request.Path;
181+
if (form.ContainsKey(overrideActionKey2))
182+
{
183+
var values = form[overrideActionKey2];
184+
var desired = values.Count > 0 ? values[0] : string.Empty;
185+
if (!string.IsNullOrWhiteSpace(desired))
186+
{
187+
originalPath2 = desired.Split(',')[0].Trim();
188+
}
189+
}
190+
191+
// Allow overriding the return URL (where to go on cancel/back)
192+
var overrideReturnKey2 = $"confirmation-return-{confirmationInfo.Handler}";
193+
var returnUrl2 = $"{context.HttpContext.Request.Path}{context.HttpContext.Request.QueryString}";
194+
if (form.ContainsKey(overrideReturnKey2))
195+
{
196+
var values = form[overrideReturnKey2];
197+
var desiredReturn2 = values.Count > 0 ? values[0] : string.Empty;
198+
if (!string.IsNullOrWhiteSpace(desiredReturn2))
199+
{
200+
returnUrl2 = desiredReturn2.Split(',')[0].Trim();
201+
}
202+
}
203+
151204
var confirmationRequest = new ConfirmationRequest
152205
{
153-
OriginalPagePath = context.HttpContext.Request.Path,
206+
OriginalPagePath = originalPath2,
154207
OriginalHandler = confirmationInfo.Handler,
155208
OriginalFormData = ExtractFormData(form),
156209
DisplayFields = confirmationInfo.DisplayFields,
157-
ReturnUrl = $"{context.HttpContext.Request.Path}{context.HttpContext.Request.QueryString}",
210+
ReturnUrl = returnUrl2,
158211
Title = title2,
159212
};
160213

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@page "/FormEngine/RemoveFieldItem"
2+
@model DfE.ExternalApplications.Web.Pages.FormEngine.RemoveFieldItemModel
3+
4+
<!-- This page has no UI; it's used solely for POST handlers. -->
5+
6+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using DfE.ExternalApplications.Application.Interfaces;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.AspNetCore.Mvc.RazorPages;
4+
using System.Text.Json;
5+
6+
namespace DfE.ExternalApplications.Web.Pages.FormEngine
7+
{
8+
public class RemoveFieldItemModel(
9+
IApplicationResponseService applicationResponseService,
10+
ILogger<RemoveFieldItemModel> logger)
11+
: PageModel
12+
{
13+
private readonly IApplicationResponseService _applicationResponseService = applicationResponseService;
14+
private readonly ILogger<RemoveFieldItemModel> _logger = logger;
15+
16+
public async Task<IActionResult> OnPostRemoveFieldItemAsync(string referenceNumber, string taskId, string fieldId, int index)
17+
{
18+
if (string.IsNullOrWhiteSpace(fieldId) || index < 0)
19+
{
20+
return BadRequest("Field ID and valid index are required");
21+
}
22+
23+
var acc = _applicationResponseService.GetAccumulatedFormData(HttpContext.Session);
24+
if (acc.TryGetValue(fieldId, out var existing))
25+
{
26+
var json = existing?.ToString() ?? "[]";
27+
try
28+
{
29+
var list = JsonSerializer.Deserialize<List<object>>(json) ?? new();
30+
if (index >= 0 && index < list.Count)
31+
{
32+
list.RemoveAt(index);
33+
var updated = JsonSerializer.Serialize(list);
34+
_applicationResponseService.AccumulateFormData(new Dictionary<string, object> { [fieldId] = updated }, HttpContext.Session);
35+
}
36+
}
37+
catch (Exception ex)
38+
{
39+
_logger.LogError(ex, "Failed to remove field item at index {Index} for field {FieldId}", index, fieldId);
40+
}
41+
}
42+
43+
var url = $"/applications/{referenceNumber}/{taskId}";
44+
return Redirect(url);
45+
}
46+
}
47+
}
48+
49+

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

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class RenderFormModel(
3434
IConditionalLogicOrchestrator conditionalLogicOrchestrator,
3535
INotificationsClient notificationsClient,
3636
IFormErrorStore formErrorStore,
37+
IComplexFieldConfigurationService complexFieldConfigurationService,
3738
ILogger<RenderFormModel> logger)
3839
: BaseFormEngineModel(renderer, applicationResponseService, fieldFormattingService, templateManagementService,
3940
applicationStateService, formStateManager, formNavigationService, formDataManager, formValidationOrchestrator, formConfigurationService, logger)
@@ -42,6 +43,7 @@ public class RenderFormModel(
4243
private readonly IConditionalLogicOrchestrator _conditionalLogicOrchestrator = conditionalLogicOrchestrator;
4344
private readonly INotificationsClient _notificationsClient = notificationsClient;
4445
private readonly IFormErrorStore _formErrorStore = formErrorStore;
46+
private readonly IComplexFieldConfigurationService _complexFieldConfigurationService = complexFieldConfigurationService;
4547

4648
[BindProperty] public Dictionary<string, object> Data { get; set; } = new();
4749

@@ -457,6 +459,120 @@ public async Task<IActionResult> OnPostPageAsync()
457459
return Page();
458460
}
459461

462+
// When AllowMultiple is true for an autocomplete complex field, append new selection
463+
// to any existing array value instead of replacing it
464+
if (CurrentPage != null)
465+
{
466+
try
467+
{
468+
foreach (var field in CurrentPage.Fields.Where(f => f.Type == "complexField" && f.ComplexField != null))
469+
{
470+
var cfg = _complexFieldConfigurationService.GetConfiguration(field.ComplexField.Id);
471+
if (!string.Equals(cfg.FieldType, "autocomplete", StringComparison.OrdinalIgnoreCase) || !cfg.AllowMultiple)
472+
{
473+
continue;
474+
}
475+
476+
var key = field.FieldId;
477+
if (!Data.TryGetValue(key, out var newValObj))
478+
{
479+
continue;
480+
}
481+
482+
var newVal = newValObj?.ToString();
483+
if (string.IsNullOrWhiteSpace(newVal))
484+
{
485+
continue;
486+
}
487+
488+
// Load existing selections from accumulated session
489+
var acc = _applicationResponseService.GetAccumulatedFormData(HttpContext.Session);
490+
var list = new List<object>();
491+
if (acc.TryGetValue(key, out var existing) && !string.IsNullOrWhiteSpace(existing?.ToString()))
492+
{
493+
var existingText = existing!.ToString()!;
494+
var addedExisting = false;
495+
// Try parse as array of objects
496+
try
497+
{
498+
var parsedArray = JsonSerializer.Deserialize<List<object>>(existingText);
499+
if (parsedArray != null)
500+
{
501+
list = parsedArray;
502+
addedExisting = true;
503+
}
504+
}
505+
catch { }
506+
507+
// If not an array, try parse as single object and add it as first element
508+
if (!addedExisting)
509+
{
510+
try
511+
{
512+
using var doc = JsonDocument.Parse(existingText);
513+
if (doc.RootElement.ValueKind == JsonValueKind.Object)
514+
{
515+
list.Add(doc.RootElement.Clone());
516+
addedExisting = true;
517+
}
518+
}
519+
catch { }
520+
}
521+
522+
// If still not added and it's a non-empty string, include as string element
523+
if (!addedExisting && !string.IsNullOrWhiteSpace(existingText))
524+
{
525+
list.Add(existingText);
526+
}
527+
}
528+
529+
// Avoid duplicates by comparing JSON string
530+
bool exists = false;
531+
try
532+
{
533+
var newJson = newVal;
534+
exists = list.Any(x => (x?.ToString() ?? "") == newJson);
535+
}
536+
catch { }
537+
538+
if (!exists)
539+
{
540+
try
541+
{
542+
using var newDoc = JsonDocument.Parse(newVal);
543+
if (newDoc.RootElement.ValueKind == JsonValueKind.Object || newDoc.RootElement.ValueKind == JsonValueKind.Array)
544+
{
545+
list.Add(newDoc.RootElement.Clone());
546+
}
547+
else if (newDoc.RootElement.ValueKind == JsonValueKind.String)
548+
{
549+
list.Add(newDoc.RootElement.GetString() ?? string.Empty);
550+
}
551+
else
552+
{
553+
list.Add(newDoc.RootElement.ToString());
554+
}
555+
}
556+
catch
557+
{
558+
// If not JSON, store as string value
559+
list.Add(newVal);
560+
}
561+
}
562+
563+
var updatedJson = JsonSerializer.Serialize(list);
564+
// Update both normalized and Data_ forms to be safe
565+
Data[key] = updatedJson;
566+
Data[$"Data_{key}"] = updatedJson;
567+
_applicationResponseService.AccumulateFormData(new Dictionary<string, object> { [key] = updatedJson }, HttpContext.Session);
568+
}
569+
}
570+
catch (Exception ex)
571+
{
572+
_logger.LogError(ex, "Failed to merge multi-select autocomplete values");
573+
}
574+
}
575+
460576
// Save the current page data to the API (skip for sub-flows as they accumulate data differently)
461577
bool isSubFlow = TryParseFlowRoute(CurrentPageId, out _, out _, out _);
462578
if (ApplicationId.HasValue && Data.Any() && !isSubFlow)
@@ -758,6 +874,8 @@ public async Task<IActionResult> OnGetAutocompleteAsync(string endpoint, string
758874
}
759875
}
760876

877+
// Removed: superseded by RemoveFieldItem page handler
878+
761879
public async Task<IActionResult> OnPostRemoveCollectionItemAsync(string fieldId, string itemId, string? flowId = null)
762880
{
763881
await CommonFormEngineInitializationAsync();

0 commit comments

Comments
 (0)