Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/Tenant-Admin-User-Manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ You do not need to be a developer to use this manual. Where a change is made in
9. [Roles](#9-roles)
10. [Permissions — how they work](#10-permissions--how-they-work)
11. [Organisation settings](#11-organisation-settings)
- [11.4 Application submitted page](#114-application-submitted-page)
12. [Event mappings](#12-event-mappings)
- [12.1 What this page is for](#121-what-this-page-is-for)
- [12.2 Events in plain English](#122-events-in-plain-english)
Expand Down Expand Up @@ -626,6 +627,25 @@ Show a GOV.UK notification banner on **every page** (for example “This is a te

Listing options do **not** change which columns appear. Columns come from the template JSON ([section 7](#7-dashboard-columns-via-the-template)).

### 11.4 Application submitted page

Customise the confirmation page after submit (`/application-submitted/{reference}`). This category is **per template**.

| Field | Purpose |
|-------|---------|
| **Template** | A form from this tenant’s catalogue, or **Default (all templates)** as the fallback |
| **Confirmation title** | Green panel heading (for example `Plan submitted`). Leave blank to use **{Singular} submitted** |
| **Page body** | Markdown for everything below the reference number |

Markdown uses the same **Markdig** renderer as form hints. Supported:

- `## Heading` / `### Subheading`
- Bullet lists (`- item`) and numbered lists (`1. item`)
- **Bold** and *italic*
- Links: `[text](https://…)` or `[email](mailto:name@example.com)`

Raw HTML is stripped. Save settings before switching template if you have unsaved changes.

---

## 12. Event mappings
Expand Down Expand Up @@ -1519,6 +1539,7 @@ Prefer the dedicated screens first. If you must use this page, these categories
| **ApplicationTerminology** | Web | Singular / plural labels | Organisation settings |
| **NotificationBanner** | Web | Site-wide banner | Organisation settings |
| **Dashboard** | Web | Page size, filters, and dashboard display text | Organisation settings |
| **ApplicationSubmittedPage** | Web | Per-template confirmation title and markdown body | Organisation settings |
| **EventMappings** | Shared | Field mappings | Event mappings |
| **SchemaEvents** | Shared | Tenant event shapes | Event mappings |
| **EventTriggers** | Shared | Submit / upload publish bindings | Event mappings |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
using System.Text.Json;
using GovUK.Dfe.CoreLibs.Contracts.ExternalApplications.Models.Request;
using GovUK.Dfe.FlexForms.Api.Client.Contracts;
using GovUK.Dfe.FlexForms.Application.Options;
using Microsoft.Extensions.Logging;

namespace GovUK.Dfe.FlexForms.Application.Admin;

/// <summary>
/// Loads and saves non-secret organisation settings (terminology, banner, dashboard, application preview).
/// Loads and saves non-secret organisation settings (terminology, banner, dashboard, preview, submitted page).
/// </summary>
public interface IOrganisationSettingsAdmin
{
Task LoadAsync(OrganisationSettingsWorkState state, CancellationToken cancellationToken = default);

Task LoadTemplateOptionsAsync(OrganisationSettingsWorkState state, CancellationToken cancellationToken = default);

Task<AdminPageOutcome> SaveAsync(OrganisationSettingsWorkState state, CancellationToken cancellationToken = default);
}

public sealed class OrganisationSettingsAdminService(
ITenantAdminClient tenantAdminClient,
ITemplatesClient templatesClient,
ILogger<OrganisationSettingsAdminService> logger) : IOrganisationSettingsAdmin
{
private const string TargetWeb = "Web";
private const string CategoryTerminology = "ApplicationTerminology";
private const string CategoryBanner = "NotificationBanner";
private const string CategoryDashboard = "Dashboard";
private const string CategoryApplicationPreview = "ApplicationPreview";
private const string CategoryApplicationSubmittedPage = "ApplicationSubmittedPage";

private static readonly JsonSerializerOptions JsonOptions = new()
{
Expand All @@ -42,6 +47,9 @@ public async Task LoadAsync(OrganisationSettingsWorkState state, CancellationTok
{
ApplySettingJson(state, setting.Category, setting.SettingsJson);
}

await LoadTemplateOptionsAsync(state, cancellationToken);
ApplySelectedSubmittedCopy(state);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -100,6 +108,13 @@ await UpsertCategoryAsync(
},
cancellationToken);

MergeSelectedSubmittedCopy(state);
await UpsertCategoryAsync(
state.TenantId,
CategoryApplicationSubmittedPage,
state.SubmittedPageByTemplate,
cancellationToken);

await tenantAdminClient.RefreshTenantConfigurationAsync(cancellationToken);

return AdminPageOutcome.Redirect(
Expand Down Expand Up @@ -188,6 +203,10 @@ private void ApplySettingJson(OrganisationSettingsWorkState state, string catego
if (TryGetBool(root, "HideSubmitSection", out var hideSubmit))
state.PreviewHideSubmitSection = hideSubmit;
}
else if (string.Equals(category, CategoryApplicationSubmittedPage, StringComparison.OrdinalIgnoreCase))
{
ApplySubmittedPageJson(state, root);
}
}
catch (JsonException ex)
{
Expand Down Expand Up @@ -244,4 +263,96 @@ private static bool TryGetProperty(JsonElement root, string name, out JsonElemen
property = default;
return false;
}

public Task LoadTemplateOptionsAsync(
OrganisationSettingsWorkState state,
CancellationToken cancellationToken = default) =>
LoadSubmittedTemplateOptionsAsync(state, cancellationToken);

private async Task LoadSubmittedTemplateOptionsAsync(
OrganisationSettingsWorkState state,
CancellationToken cancellationToken)
{
var options = new List<AdminSelectOption>
{
new(
"Default (all templates)",
ApplicationSubmittedPageCopy.DefaultTemplateKey,
string.Equals(
state.SubmittedTemplateId,
ApplicationSubmittedPageCopy.DefaultTemplateKey,
StringComparison.OrdinalIgnoreCase))
};

try
{
var templates = await templatesClient.GetAccessibleTemplatesAsync(cancellationToken) ?? [];
options.AddRange(
templates
.Where(t => t.TemplateId != Guid.Empty)
.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
.Select(t =>
{
var id = t.TemplateId.ToString();
var label = string.IsNullOrWhiteSpace(t.Name) ? id : $"{t.Name} ({id})";
return new AdminSelectOption(
label,
id,
string.Equals(id, state.SubmittedTemplateId, StringComparison.OrdinalIgnoreCase));
}));
}
catch (Exception ex)
{
logger.LogWarning(ex, "Could not load templates for application submitted page settings");
}

if (string.IsNullOrWhiteSpace(state.SubmittedTemplateId))
{
state.SubmittedTemplateId = options[0].Value;
options[0] = options[0] with { Selected = true };
}

state.SubmittedTemplateOptions = options;
}

private static void ApplySelectedSubmittedCopy(OrganisationSettingsWorkState state)
{
if (string.IsNullOrWhiteSpace(state.SubmittedTemplateId))
return;

if (!state.SubmittedPageByTemplate.TryGetValue(state.SubmittedTemplateId, out var copy))
return;

state.SubmittedPanelTitle = copy.PanelTitle;
state.SubmittedBodyMarkdown = copy.BodyMarkdown;
}

private static void MergeSelectedSubmittedCopy(OrganisationSettingsWorkState state)
{
if (string.IsNullOrWhiteSpace(state.SubmittedTemplateId))
return;

state.SubmittedPageByTemplate[state.SubmittedTemplateId] = new ApplicationSubmittedPageCopy
{
PanelTitle = state.SubmittedPanelTitle?.Trim() ?? string.Empty,
BodyMarkdown = state.SubmittedBodyMarkdown ?? string.Empty
};
}

private static void ApplySubmittedPageJson(OrganisationSettingsWorkState state, JsonElement root)
{
foreach (var property in root.EnumerateObject())
{
if (property.Value.ValueKind != JsonValueKind.Object)
continue;

var copy = new ApplicationSubmittedPageCopy();
if (TryGetString(property.Value, "PanelTitle", out var title))
copy.PanelTitle = title;
if (TryGetString(property.Value, "BodyMarkdown", out var body))
copy.BodyMarkdown = body;

state.SubmittedPageByTemplate[property.Name] = copy;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using GovUK.Dfe.FlexForms.Application.Options;

namespace GovUK.Dfe.FlexForms.Application.Admin;

/// <summary>
Expand Down Expand Up @@ -43,6 +45,17 @@ public sealed class OrganisationSettingsWorkState

public bool PreviewHideSubmitSection { get; set; }

public string? SubmittedTemplateId { get; set; }

public IReadOnlyList<AdminSelectOption> SubmittedTemplateOptions { get; set; } = [];

public string? SubmittedPanelTitle { get; set; }

public string? SubmittedBodyMarkdown { get; set; }

public Dictionary<string, ApplicationSubmittedPageCopy> SubmittedPageByTemplate { get; set; } =
new(StringComparer.OrdinalIgnoreCase);

public bool HasError { get; set; }

public string? ErrorMessage { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using GovUK.Dfe.FlexForms.Application.Interfaces;

namespace GovUK.Dfe.FlexForms.Application.Applications;

/// <summary>
/// Platform fallback copy for the application-submitted page when a tenant has not configured a template.
/// </summary>
public static class ApplicationSubmittedPageDefaults
{
public static string PanelTitle(IApplicationTerminologyProvider terminology) =>
$"{terminology.SingularCapitalised} submitted";

public static string BodyMarkdown(IApplicationTerminologyProvider terminology)
{
var singular = terminology.Singular;
return
$"""
We've sent you a confirmation email with your reference number.

## What happens next

Your {singular} will be assigned to a staff member in DfE's Regions Group.

The staff member will contact you:

- if we need anything else
- when the {singular} is ready for decision

You must have approval for your {singular} from DfE's Regions Group before you:

- make any changes to the articles of association or any other trust documents
- engage with stakeholders of the trust that academies are leaving

## Contact us

If you have any questions about your {singular}, you can email [RegionalServices.RG@education.gov.uk](mailto:RegionalServices.RG@education.gov.uk).
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace GovUK.Dfe.FlexForms.Application.Applications;

/// <summary>
/// Display state for the post-submit confirmation page.
/// </summary>
public sealed class ApplicationSubmittedWorkState
{
public string ReferenceNumber { get; set; } = string.Empty;

public string PanelTitle { get; set; } = string.Empty;

public string BodyMarkdown { get; set; } = string.Empty;
}
Loading
Loading