From cf5d3ac25f2bfc18ad2014e338a7aa4f17ff3752 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Mon, 13 Apr 2026 11:40:47 -0700 Subject: [PATCH 1/3] AB#32133 update report config for submissions report label --- .../reporting/reporting-configuration.md | 101 +++++ .../Configuration/UpsertColumnMappingDto.cs | 4 +- .../UpsertReportColumnsMapDto.cs | 3 +- .../Configuration/ReportMappingUtils.cs | 31 +- .../ReportingConfiguration/Default.js | 12 +- .../ColumnsMappingServiceTests.cs | 387 ++++++++++++++++++ 6 files changed, 533 insertions(+), 5 deletions(-) create mode 100644 applications/Unity.GrantManager/documentation/reporting/reporting-configuration.md diff --git a/applications/Unity.GrantManager/documentation/reporting/reporting-configuration.md b/applications/Unity.GrantManager/documentation/reporting/reporting-configuration.md new file mode 100644 index 0000000000..e771241980 --- /dev/null +++ b/applications/Unity.GrantManager/documentation/reporting/reporting-configuration.md @@ -0,0 +1,101 @@ +# Reporting Configuration + +## Overview + +The Reporting Configuration system allows administrators to define how source field data maps to PostgreSQL database columns for generated reporting views. It supports three correlation providers, each sourcing field metadata from a different system: + +| Provider | Source | Correlation ID | Description | +|----------|--------|----------------|-------------| +| `formversion` | CHEFS form submissions | Form Version ID | Immutable form schema fields | +| `worksheet` | Unity.Flex worksheets | Form Version ID | Dynamic worksheet fields | +| `scoresheet` | Unity.Flex scoresheets | Form ID | Evaluation/scoring fields | + +## Architecture + +### Layered Components + +- **`ReportMappingUtils`** (`Unity.Reporting.Application`) — Static utility methods for column name sanitization, validation, uniqueness enforcement, and mapping creation/update logic. +- **`ReportMappingService`** (`Unity.Reporting.Application`) — Application service orchestrating CRUD operations, field metadata retrieval, view generation, and provider resolution. +- **`IFieldsProvider`** (`Unity.Reporting.Application`) — Interface for correlation-specific field metadata extraction and change detection. +- **`ReportingConfigurationController`** (`Unity.Reporting.Web`) — MVC controller providing AJAX API endpoints for the UI. +- **`Default.js`** (`Unity.Reporting.Web`) — Client-side DataTable configuration, validation, and provider-aware UI logic. + +### Field Providers + +Each provider implements `IFieldsProvider`: + +- **`FormVersionFieldsProvider`** — Retrieves field metadata from immutable CHEFS form version schemas. Change detection always returns null since form versions are immutable. +- **`WorksheetFieldsProvider`** — Retrieves field metadata from Unity.Flex worksheet definitions. Supports change detection for dynamic schema evolution. +- **`ScoresheetFieldsProvider`** — Retrieves field metadata from Unity.Flex scoresheet definitions. Supports change detection for scoring structure changes. + +## Default Column Name Generation + +When a user creates or saves a report configuration for the first time (or when new fields are discovered during an update), the system auto-generates default column names for fields that don't have user-specified names. + +### Provider-Specific Column Name Source + +The source used for generating default column names differs by provider: + +| Provider | Default Column Name Source | Rationale | +|----------|---------------------------|-----------| +| `formversion` | **Key** (CHEFS Property Name, e.g., `firstName`) | CHEFS property names are stable, developer-defined identifiers that produce clean, predictable column names (e.g., `firstname`). Labels can be verbose or contain special characters. | +| `worksheet` | **Label** (e.g., `First Name`) | Worksheet labels are human-readable names configured by administrators, producing descriptive column names (e.g., `first_name`). | +| `scoresheet` | **Label** (e.g., `Score One`) | Scoresheet labels provide meaningful, user-facing descriptions that map well to reporting column names. | + +### Column Name Sanitization + +All auto-generated column names go through the same sanitization pipeline regardless of provider: + +1. Convert to lowercase +2. Replace spaces and hyphens with underscores +3. Remove all non-alphanumeric characters (except underscores) +4. Remove consecutive underscores +5. Trim leading/trailing underscores +6. Prefix with `col_` if name starts with a digit +7. Truncate to 60 characters maximum +8. Ensure uniqueness by appending numeric suffixes (`_1`, `_2`, etc.) when collisions occur + +### Implementation Details + +The column name source selection is implemented in `ReportMappingUtils.GetDefaultColumnNameSource()`: + +- **Server-side**: Used by `CreateNewMap()` and `UpdateExistingMap()` when auto-generating column names for unmapped or newly discovered fields. +- **Client-side**: The `getDefaultColumnNameSource()` function in `Default.js` mirrors this logic for the initial DataTable display when no saved configuration exists. + +### Impact on Existing Configurations + +- **Existing saved configurations are not affected.** Column names are persisted in the database; the provider-specific logic only determines defaults for new/unsaved fields. +- **Existing column names are preserved during updates.** The three-tier priority system (user-provided → existing → auto-generated) ensures established mappings are maintained. + +## Column Name Validation + +Both client-side and server-side enforce PostgreSQL column name rules: + +- Maximum 60 characters +- Must start with a letter or underscore +- Contains only letters, digits, and underscores +- Cannot be a PostgreSQL reserved word +- Must be unique within the configuration + +## View Generation + +After saving a configuration, users can generate a PostgreSQL database view: + +1. User provides a view name (validated for availability and PostgreSQL compliance) +2. A background job is queued to create/update the view in the `Reporting` schema +3. View status is tracked and displayed via a widget + +View names follow similar sanitization rules with a 63-character maximum (PostgreSQL identifier limit). + +## Configuration Lifecycle + +``` +Fields Metadata → [Save] → Configuration → [Generate View] → Database View + ↓ + [Delete] → Removes configuration and optionally the view +``` + +1. **Initial Load**: Field metadata is fetched from the appropriate provider; default column names are generated. +2. **Save**: Creates or updates the mapping configuration with user-specified and auto-generated column names. +3. **Generate View**: Creates a PostgreSQL view in the `Reporting` schema based on the saved mapping. +4. **Delete**: Removes the configuration and optionally drops the associated database view. diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/UpsertColumnMappingDto.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/UpsertColumnMappingDto.cs index d0498cde3b..01042b9ebc 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/UpsertColumnMappingDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/UpsertColumnMappingDto.cs @@ -3,7 +3,9 @@ /// /// Data transfer object containing user-specified field-to-column mapping overrides for report configuration. /// Allows users to customize column names for specific fields instead of relying entirely on auto-generated names. - /// Fields not included in this mapping will receive auto-generated column names based on their labels. + /// Fields not included in this mapping will receive auto-generated column names based on the correlation provider: + /// for form versions, column names are derived from field keys (CHEFS Property Names); for worksheets and scoresheets, + /// column names are derived from field labels. /// public class UpsertColumnMappingDto { diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/UpsertReportColumnsMapDto.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/UpsertReportColumnsMapDto.cs index 74ae0b8e75..c063c59a61 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/UpsertReportColumnsMapDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/UpsertReportColumnsMapDto.cs @@ -24,7 +24,8 @@ public class UpsertReportColumnsMapDto /// /// Gets or sets the optional column mapping configuration containing user-specified field-to-column mappings. /// When provided, these mappings override auto-generated column names for specific fields. - /// Empty mappings will result in fully auto-generated column names based on field labels. + /// Empty mappings will result in fully auto-generated column names derived from field keys + /// (for form versions) or field labels (for worksheets and scoresheets). /// public UpsertColumnMappingDto Mapping { get; set; } = new(); } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs index f23f9b2441..99b0400aae 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs @@ -16,6 +16,25 @@ internal static partial class ReportMappingUtils { private const int MaxColumnNameLength = 60; + /// + /// Determines the appropriate source value for auto-generating a default column name based on the correlation provider. + /// For the "formversion" provider, the field's Key (CHEFS Property Name) is used because it provides more stable, + /// developer-friendly identifiers. For all other providers (worksheets, scoresheets), the field's Label is used + /// as it provides more human-readable column names. + /// + /// The field metadata containing both Key and Label properties. + /// The correlation provider identifier (e.g., "formversion", "worksheet", "scoresheet"). + /// The Key for formversion providers, or the Label (falling back to empty string) for all other providers. + internal static string GetDefaultColumnNameSource(FieldPathTypeDto field, string correlationProvider) + { + if (string.Equals(correlationProvider, Providers.FormVersion, StringComparison.OrdinalIgnoreCase)) + { + return field.Key ?? string.Empty; + } + + return field.Label ?? string.Empty; + } + /// /// Generates sanitized and unique PostgreSQL-compatible column names from a dictionary of field keys and their display labels. /// Processes each label through sanitization to remove invalid characters, enforces uniqueness with numeric suffixes, @@ -269,6 +288,8 @@ private static bool IsValidColumnName(string columnName) /// derived from field metadata. Prioritizes user-specified column names where provided, while automatically generating /// sanitized and unique column names for unmapped fields. Ensures all column names are PostgreSQL-compliant and unique /// across the entire mapping configuration. + /// For the "formversion" provider, auto-generated column names are derived from the field Key (CHEFS Property Name) + /// rather than the Label, providing more stable and developer-friendly default names. /// /// DTO containing correlation information and optional user-provided column mappings organized by field path. /// Tuple containing an array of field metadata (with keys, labels, types, paths) and additional mapping metadata from the correlation provider. @@ -299,12 +320,14 @@ internal static ReportColumnsMap CreateNewMap(UpsertReportColumnsMapDto upsertRe var usedColumnNames = new HashSet(userProvidedMappings.Values, StringComparer.OrdinalIgnoreCase); // Generate column names for fields without user-provided mappings + // For formversion provider, use Key (CHEFS Property Name) as the source; for others, use Label var autoGeneratedColumnNames = new Dictionary(); foreach (var field in fieldsMap.Fields) { if (!userProvidedMappings.ContainsKey(field.Path)) { - var sanitizedName = SanitizeColumnName(field.Label ?? string.Empty); + var columnNameSource = GetDefaultColumnNameSource(field, upsertReportColmnsMapDto.CorrelationProvider); + var sanitizedName = SanitizeColumnName(columnNameSource); var uniqueName = EnsureUniqueness(sanitizedName, usedColumnNames); autoGeneratedColumnNames[field.Path] = uniqueName; usedColumnNames.Add(uniqueName); @@ -355,6 +378,8 @@ internal static ReportColumnsMap CreateNewMap(UpsertReportColumnsMapDto upsertRe /// and user-provided updates. Implements a three-tier priority system: user-provided column names take precedence, /// followed by existing column names from the database, with auto-generated names for new fields. Maintains column /// name uniqueness across the entire mapping while preserving established mappings where possible. + /// For newly discovered fields in the "formversion" provider, auto-generated column names are derived from the + /// field Key (CHEFS Property Name) rather than the Label. /// /// DTO containing optional user-provided column mappings to update or add, organized by field path. /// The existing ReportColumnsMap entity from the database containing current mapping configuration and correlation details. @@ -412,9 +437,11 @@ internal static ReportColumnsMap UpdateExistingMap(UpsertReportColumnsMapDto upd columnName = existingRow.ColumnName; } // Priority 3: Auto-generate for new fields + // For formversion provider, use Key (CHEFS Property Name) as the source; for others, use Label else { - var sanitizedName = SanitizeColumnName(field.Label ?? string.Empty); + var columnNameSource = GetDefaultColumnNameSource(field, updateReportColumnsMapDto.CorrelationProvider); + var sanitizedName = SanitizeColumnName(columnNameSource); columnName = EnsureUniqueness(sanitizedName, usedColumnNames); usedColumnNames.Add(columnName); } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js index c094098fb2..0869848a50 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js @@ -467,6 +467,16 @@ $(function () { }; } + // Helper function to get the default column name source based on the current provider. + // For formversion (Submissions), use the CHEFS Property Name (key) as it provides more stable identifiers. + // For worksheets and scoresheets, use the label as it provides more human-readable column names. + function getDefaultColumnNameSource(field) { + if (currentProvider === 'formversion') { + return field.key || field.label || ''; + } + return field.label || field.key || ''; + } + // Helper function to transform fields metadata (module-level to reduce nesting) function transformFieldsMetadata(fieldsMetadata) { const items = fieldsMetadata.fields.map(field => ({ @@ -475,7 +485,7 @@ $(function () { type: field.type, path: field.path, dataPath: field.dataPath, - columnName: field.label || field.key, + columnName: getDefaultColumnNameSource(field), typePath: field.typePath })); diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/test/Unity.Reporting.Application.Tests/Configuration/ColumnsMappingServiceTests.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/test/Unity.Reporting.Application.Tests/Configuration/ColumnsMappingServiceTests.cs index 32ca6d8646..0c2490dd55 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/test/Unity.Reporting.Application.Tests/Configuration/ColumnsMappingServiceTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/test/Unity.Reporting.Application.Tests/Configuration/ColumnsMappingServiceTests.cs @@ -748,5 +748,392 @@ await Should.ThrowAsync(async () => } #endregion + + #region Column Name Source by Provider Tests + + // Helper method to call internal GetDefaultColumnNameSource method via reflection + private static string CallGetDefaultColumnNameSource(FieldPathTypeDto field, string correlationProvider) + { + var type = typeof(ReportMappingUtils); + var method = type.GetMethod("GetDefaultColumnNameSource", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); + return (string)method!.Invoke(null, [field, correlationProvider])!; + } + + // Helper method to call internal CreateNewMap method via reflection + private static ReportColumnsMap CallCreateNewMap(UpsertReportColumnsMapDto dto, FieldPathMetaMapDto fieldsMap) + { + var type = typeof(ReportMappingUtils); + var method = type.GetMethod("CreateNewMap", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); + return (ReportColumnsMap)method!.Invoke(null, [dto, fieldsMap])!; + } + + // Helper method to call internal UpdateExistingMap method via reflection + private static ReportColumnsMap CallUpdateExistingMap(UpsertReportColumnsMapDto dto, ReportColumnsMap existing, FieldPathMetaMapDto fieldsMap) + { + var type = typeof(ReportMappingUtils); + var method = type.GetMethod("UpdateExistingMap", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); + return (ReportColumnsMap)method!.Invoke(null, [dto, existing, fieldsMap])!; + } + + [Fact] + public void GetDefaultColumnNameSource_Should_Return_Key_For_FormVersion_Provider() + { + // Arrange + var field = new FieldPathTypeDto + { + Key = "firstName", + Label = "First Name" + }; + + // Act + var result = CallGetDefaultColumnNameSource(field, "formversion"); + + // Assert + result.ShouldBe("firstName"); + } + + [Fact] + public void GetDefaultColumnNameSource_Should_Return_Label_For_Worksheet_Provider() + { + // Arrange + var field = new FieldPathTypeDto + { + Key = "firstName", + Label = "First Name" + }; + + // Act + var result = CallGetDefaultColumnNameSource(field, "worksheet"); + + // Assert + result.ShouldBe("First Name"); + } + + [Fact] + public void GetDefaultColumnNameSource_Should_Return_Label_For_Scoresheet_Provider() + { + // Arrange + var field = new FieldPathTypeDto + { + Key = "score1", + Label = "Score One" + }; + + // Act + var result = CallGetDefaultColumnNameSource(field, "scoresheet"); + + // Assert + result.ShouldBe("Score One"); + } + + [Fact] + public void GetDefaultColumnNameSource_Should_Be_Case_Insensitive_For_FormVersion() + { + // Arrange + var field = new FieldPathTypeDto + { + Key = "myKey", + Label = "My Label" + }; + + // Act + var result = CallGetDefaultColumnNameSource(field, "FormVersion"); + + // Assert + result.ShouldBe("myKey"); + } + + [Fact] + public void GetDefaultColumnNameSource_Should_Return_Empty_When_Key_Is_Null_For_FormVersion() + { + // Arrange + var field = new FieldPathTypeDto + { + Key = null!, + Label = "Some Label" + }; + + // Act + var result = CallGetDefaultColumnNameSource(field, "formversion"); + + // Assert + result.ShouldBe(string.Empty); + } + + [Fact] + public void GetDefaultColumnNameSource_Should_Return_Empty_When_Label_Is_Null_For_Worksheet() + { + // Arrange + var field = new FieldPathTypeDto + { + Key = "someKey", + Label = null + }; + + // Act + var result = CallGetDefaultColumnNameSource(field, "worksheet"); + + // Assert + result.ShouldBe(string.Empty); + } + + [Fact] + public void CreateNewMap_Should_Use_Key_For_FormVersion_Auto_Generated_ColumnNames() + { + // Arrange + var dto = new UpsertReportColumnsMapDto + { + CorrelationId = Guid.NewGuid(), + CorrelationProvider = "formversion", + Mapping = new UpsertColumnMappingDto { Rows = [] } + }; + + var fieldsMap = new FieldPathMetaMapDto + { + Fields = + [ + new FieldPathTypeDto + { + Id = "1", + Key = "firstName", + Label = "First Name", + Type = "textfield", + Path = "firstName", + DataPath = "firstName", + TypePath = "textfield" + }, + new FieldPathTypeDto + { + Id = "2", + Key = "emailAddress", + Label = "Email Address", + Type = "email", + Path = "emailAddress", + DataPath = "emailAddress", + TypePath = "email" + } + ] + }; + + // Act + var result = CallCreateNewMap(dto, fieldsMap); + + // Assert + result.ShouldNotBeNull(); + var mapping = System.Text.Json.JsonSerializer.Deserialize(result.Mapping)!; + + // Column names should be derived from Key (not Label) + mapping.Rows[0].ColumnName.ShouldBe("firstname"); + mapping.Rows[1].ColumnName.ShouldBe("emailaddress"); + } + + [Fact] + public void CreateNewMap_Should_Use_Label_For_Worksheet_Auto_Generated_ColumnNames() + { + // Arrange + var dto = new UpsertReportColumnsMapDto + { + CorrelationId = Guid.NewGuid(), + CorrelationProvider = "worksheet", + Mapping = new UpsertColumnMappingDto { Rows = [] } + }; + + var fieldsMap = new FieldPathMetaMapDto + { + Fields = + [ + new FieldPathTypeDto + { + Id = "1", + Key = "firstName", + Label = "First Name", + Type = "textfield", + Path = "ws1->firstName", + DataPath = "ws1->firstName", + TypePath = "textfield" + } + ] + }; + + // Act + var result = CallCreateNewMap(dto, fieldsMap); + + // Assert + result.ShouldNotBeNull(); + var mapping = System.Text.Json.JsonSerializer.Deserialize(result.Mapping)!; + + // Column name should be derived from Label (not Key) for worksheet + mapping.Rows[0].ColumnName.ShouldBe("first_name"); + } + + [Fact] + public void UpdateExistingMap_Should_Use_Key_For_FormVersion_New_Field_ColumnNames() + { + // Arrange + var existingMapping = new Mapping + { + Rows = + [ + new MapRow + { + PropertyName = "firstName", + ColumnName = "existing_col", + Path = "firstName", + DataPath = "firstName", + Label = "First Name", + Type = "textfield", + TypePath = "textfield", + Id = "1" + } + ] + }; + + var existing = new ReportColumnsMap + { + CorrelationId = Guid.NewGuid(), + CorrelationProvider = "formversion", + Mapping = System.Text.Json.JsonSerializer.Serialize(existingMapping) + }; + + var dto = new UpsertReportColumnsMapDto + { + CorrelationId = existing.CorrelationId, + CorrelationProvider = "formversion", + Mapping = new UpsertColumnMappingDto + { + Rows = + [ + new UpsertMapRowDto { PropertyName = "firstName", ColumnName = "existing_col", Path = "firstName" } + ] + } + }; + + var fieldsMap = new FieldPathMetaMapDto + { + Fields = + [ + new FieldPathTypeDto + { + Id = "1", + Key = "firstName", + Label = "First Name", + Type = "textfield", + Path = "firstName", + DataPath = "firstName", + TypePath = "textfield" + }, + // New field discovered + new FieldPathTypeDto + { + Id = "2", + Key = "lastName", + Label = "Last Name", + Type = "textfield", + Path = "lastName", + DataPath = "lastName", + TypePath = "textfield" + } + ] + }; + + // Act + var result = CallUpdateExistingMap(dto, existing, fieldsMap); + + // Assert + result.ShouldNotBeNull(); + var mapping = System.Text.Json.JsonSerializer.Deserialize(result.Mapping)!; + + // Existing field should keep its column name + mapping.Rows[0].ColumnName.ShouldBe("existing_col"); + + // New field should be auto-generated from Key (not Label) for formversion + mapping.Rows[1].ColumnName.ShouldBe("lastname"); + } + + [Fact] + public void UpdateExistingMap_Should_Use_Label_For_Worksheet_New_Field_ColumnNames() + { + // Arrange + var existingMapping = new Mapping + { + Rows = + [ + new MapRow + { + PropertyName = "score1", + ColumnName = "existing_score", + Path = "ws1->score1", + DataPath = "ws1->score1", + Label = "Score 1", + Type = "number", + TypePath = "number", + Id = "1" + } + ] + }; + + var existing = new ReportColumnsMap + { + CorrelationId = Guid.NewGuid(), + CorrelationProvider = "worksheet", + Mapping = System.Text.Json.JsonSerializer.Serialize(existingMapping) + }; + + var dto = new UpsertReportColumnsMapDto + { + CorrelationId = existing.CorrelationId, + CorrelationProvider = "worksheet", + Mapping = new UpsertColumnMappingDto + { + Rows = + [ + new UpsertMapRowDto { PropertyName = "score1", ColumnName = "existing_score", Path = "ws1->score1" } + ] + } + }; + + var fieldsMap = new FieldPathMetaMapDto + { + Fields = + [ + new FieldPathTypeDto + { + Id = "1", + Key = "score1", + Label = "Score 1", + Type = "number", + Path = "ws1->score1", + DataPath = "ws1->score1", + TypePath = "number" + }, + // New field discovered + new FieldPathTypeDto + { + Id = "2", + Key = "totalScore", + Label = "Total Score", + Type = "number", + Path = "ws1->totalScore", + DataPath = "ws1->totalScore", + TypePath = "number" + } + ] + }; + + // Act + var result = CallUpdateExistingMap(dto, existing, fieldsMap); + + // Assert + result.ShouldNotBeNull(); + var mapping = System.Text.Json.JsonSerializer.Deserialize(result.Mapping)!; + + // Existing field should keep its column name + mapping.Rows[0].ColumnName.ShouldBe("existing_score"); + + // New field should be auto-generated from Label (not Key) for worksheet + mapping.Rows[1].ColumnName.ShouldBe("total_score"); + } + + #endregion } } \ No newline at end of file From 899ff4aa4ffe2f27ba2e3ba81ab4b81f8c2f6d1f Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Mon, 13 Apr 2026 12:05:37 -0700 Subject: [PATCH 2/3] AB#32133 copilot fixes --- .../documentation/reporting/reporting-configuration.md | 8 +++++--- .../Components/ReportingConfiguration/Default.js | 10 ++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/applications/Unity.GrantManager/documentation/reporting/reporting-configuration.md b/applications/Unity.GrantManager/documentation/reporting/reporting-configuration.md index e771241980..846023fb3f 100644 --- a/applications/Unity.GrantManager/documentation/reporting/reporting-configuration.md +++ b/applications/Unity.GrantManager/documentation/reporting/reporting-configuration.md @@ -57,10 +57,12 @@ All auto-generated column names go through the same sanitization pipeline regard ### Implementation Details -The column name source selection is implemented in `ReportMappingUtils.GetDefaultColumnNameSource()`: +The column name source selection is implemented identically on server and client with no cross-field fallbacks: -- **Server-side**: Used by `CreateNewMap()` and `UpdateExistingMap()` when auto-generating column names for unmapped or newly discovered fields. -- **Client-side**: The `getDefaultColumnNameSource()` function in `Default.js` mirrors this logic for the initial DataTable display when no saved configuration exists. +- **Server-side** — `ReportMappingUtils.GetDefaultColumnNameSource()` returns `field.Key ?? ""` for `formversion`, or `field.Label ?? ""` for all other providers. Used by `CreateNewMap()` and `UpdateExistingMap()` when auto-generating column names for unmapped or newly discovered fields. The provider comparison is case-insensitive via `StringComparison.OrdinalIgnoreCase`. +- **Client-side** — `getDefaultColumnNameSource()` in `Default.js` uses the same logic: `field.key || ''` when `currentProvider === 'formversion'`, otherwise `field.label || ''`. This is used by `transformFieldsMetadata()` to set initial column name values in the DataTable when no saved configuration exists. + +> **Important:** Neither implementation falls back from Key to Label or vice versa. If the source value is null/empty, an empty string is used, and the downstream sanitization produces `"col_1"` as a placeholder. This ensures client and server always produce identical defaults. ### Impact on Existing Configurations diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js index 0869848a50..3a1d3400e1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js @@ -468,13 +468,15 @@ $(function () { } // Helper function to get the default column name source based on the current provider. - // For formversion (Submissions), use the CHEFS Property Name (key) as it provides more stable identifiers. - // For worksheets and scoresheets, use the label as it provides more human-readable column names. + // Mirrors the server-side ReportMappingUtils.GetDefaultColumnNameSource() logic exactly: + // - formversion: use Key only (CHEFS Property Name), no fallback to Label + // - worksheet/scoresheet: use Label only, no fallback to Key + // No cross-field fallback ensures the client and server produce identical defaults. function getDefaultColumnNameSource(field) { if (currentProvider === 'formversion') { - return field.key || field.label || ''; + return field.key || ''; } - return field.label || field.key || ''; + return field.label || ''; } // Helper function to transform fields metadata (module-level to reduce nesting) From 00e4564d73ff5151e722587629cd4d1c0c1d3ee4 Mon Sep 17 00:00:00 2001 From: "Todosichuk, Daryl" Date: Mon, 13 Apr 2026 12:28:20 -0700 Subject: [PATCH 3/3] AB#32613 Move Unity.GrantManager.SonarScan.Tests into workflow file --- .../coverlet.runsettings | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.SonarScan.Tests/coverlet.runsettings diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.SonarScan.Tests/coverlet.runsettings b/applications/Unity.GrantManager/test/Unity.GrantManager.SonarScan.Tests/coverlet.runsettings deleted file mode 100644 index 357a5ec14e..0000000000 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.SonarScan.Tests/coverlet.runsettings +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - opencover - [*]*.Migrations.* - **/Migrations/** - - - - - \ No newline at end of file