Skip to content
Merged

Dev #2331

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
838a544
AB#32704 - Fixed Scoring Generate button.
hasanpour Apr 18, 2026
f3eb53d
AB#32704 - Added spinner
hasanpour Apr 18, 2026
01321bc
[AB#31682] Adjust minimum height for DataTable scroll body
plavoie-BC Apr 20, 2026
dce545b
AB#31194 improvements and bugfixes for reporting configuration
AndreGAot Apr 20, 2026
333172f
AB#32705 - Removed dead AI queue methods and background jobs.
hasanpour Apr 20, 2026
85c299b
AB#31194 copilot feedback
AndreGAot Apr 20, 2026
6d5ecf3
Merge pull request #2321 from bcgov/feature/AB#31194-worksheets-dynam…
AndreGAot Apr 20, 2026
c70ddb1
AB#32709: Bugfix - Download All Is Not Working
aurelio-aot Apr 20, 2026
9ec452f
Merge pull request #2322 from bcgov/bugfix/AB#32709-Download-All-Not-…
AndreGAot Apr 20, 2026
a8abfb6
AB#32301 - Fixed auth error in AI pipeline job via ForPipeline intern…
hasanpour Apr 20, 2026
cefdcf4
AB#32583 refine chefs toolbar layout
jacobwillsmith Apr 21, 2026
b79ed0a
AB#32583 keep chefs toolbar on one line
jacobwillsmith Apr 21, 2026
8addd74
AB#32299 use standard OpenAI env var keys
jacobwillsmith Apr 21, 2026
ccb8689
AB#32465 simplify OpenAI config resolution
jacobwillsmith Apr 21, 2026
5fe35ad
AB#32613 missing sonar.projectVersion
DarylTodosichuk Apr 22, 2026
9fa324b
Merge pull request #2327 from bcgov/feature/AB#32613-sonarcloud-githu…
DarylTodosichuk Apr 22, 2026
add43b9
Merge pull request #2326 from bcgov/feature/AB#32299-split-openaiserv…
JamesPasta Apr 22, 2026
4821740
Merge pull request #2325 from bcgov/bugfix/AB#32583-chefs-double-line…
JamesPasta Apr 22, 2026
a87d08c
Merge pull request #2324 from bcgov/bugfix/AB#32704-Manual-Generate-S…
JamesPasta Apr 22, 2026
8ab638f
Merge pull request #2320 from bcgov/bugfix/AB#31682-table-whitespace
JamesPasta Apr 22, 2026
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
17 changes: 13 additions & 4 deletions .github/workflows/sonarsource-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,15 @@ jobs:
run: |
VERSION="${{ vars.UGM_BUILD_VERSION }}"
echo "Debug: UGM_BUILD_VERSION variable value: '$VERSION'"
if [ -z "$VERSION" ]; then
echo "BUILD_VERSION=1.0.0-dev" >> $GITHUB_ENV
echo "Using fallback version: 1.0.0-dev (UGM_BUILD_VERSION variable not set)"
else
if [ -n "$VERSION" ]; then
echo "BUILD_VERSION=$VERSION" >> $GITHUB_ENV
echo "Using project version: $VERSION"
# Replace ${BUILD_VERSION} with actual value
sed -i "s/\${BUILD_VERSION}/$VERSION/g" applications/Unity.GrantManager/sonar-project.properties
else
echo "UGM_BUILD_VERSION variable not set - removing sonar.projectVersion property"
# Remove the projectVersion line entirely if no version is available
sed -i "/sonar.projectVersion=/d" applications/Unity.GrantManager/sonar-project.properties
fi

- name: Restore dependencies
Expand All @@ -78,6 +81,12 @@ jobs:
working-directory: ./applications/Unity.GrantManager
run: dotnet build Unity.GrantManager.sln --no-restore

- name: SonarCloud sonar.projectVersion
run: |
echo "BUILD_VERSION environment variable: $BUILD_VERSION"
echo "Updated sonar-project.properties:"
cat applications/Unity.GrantManager/sonar-project.properties | grep "sonar.projectVersion" || echo "No projectVersion set"

- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@v7
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public interface IAttachmentSummaryAppService : IApplicationService
{
Task<AttachmentSummaryResultDto> GenerateAttachmentSummaryAsync(Guid attachmentId, string? promptVersion = null);
Task<List<AttachmentSummaryResultDto>> GenerateAttachmentSummariesAsync(List<Guid> attachmentIds, string? promptVersion = null);
Task<List<AttachmentSummaryResultDto>> GenerateAttachmentSummariesForPipelineAsync(List<Guid> attachmentIds, string? promptVersion = null);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Unity.AI.Automation;

public interface IApplicationAIGenerationQueue
{
Task QueueAttachmentSummariesAsync(IReadOnlyList<Guid> attachmentIds, Guid? tenantId, string? promptVersion = null);
Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null);
Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null);
Task QueueApplicationPipelineAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ namespace Unity.GrantManager.GrantApplications
public interface IApplicationAnalysisAppService : IApplicationService
{
Task<ApplicationAnalysisResultDto> GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null);
Task<ApplicationAnalysisResultDto> GenerateApplicationAnalysisForPipelineAsync(Guid applicationId, string? promptVersion = null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ namespace Unity.GrantManager.GrantApplications
public interface IApplicationScoringAppService : IApplicationService
{
Task<ApplicationScoringResultDto> GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null);
Task<ApplicationScoringResultDto> GenerateApplicationScoringForPipelineAsync(Guid applicationId, string? promptVersion = null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ public class OpenAIConfigurationResolver(IConfiguration configuration) : ITransi
private const string DefaultMaxTokensParameterName = "max_completion_tokens";
private const string LegacyMaxTokensParameterName = "max_tokens";
private const string DefaultProviderName = "OpenAI";
private const string OpenAiApiKeyEnvironmentVariableName = "AZURE_OPENAI_API_KEY";
private const string OpenAiEndpointEnvironmentVariableName = "AZURE_OPENAI_ENDPOINT";

private readonly IConfiguration _configuration = configuration;

Expand All @@ -33,15 +31,6 @@ public string ResolveProviderName(string? operationName = null)
public string ResolveApiKey(string? operationName = null)
{
var providerName = ResolveProviderName(operationName);
if (string.Equals(providerName, DefaultProviderName, StringComparison.Ordinal))
{
var injectedApiKey = _configuration[OpenAiApiKeyEnvironmentVariableName];
if (!string.IsNullOrWhiteSpace(injectedApiKey))
{
return injectedApiKey;
}
}

return _configuration[$"Azure:{providerName}:ApiKey"] ?? string.Empty;
}

Expand Down Expand Up @@ -117,18 +106,7 @@ private static string ResolveMaxTokensParameterName(string? configuredParameterN

private string? ResolveInjectedEndpoint(string providerName)
{
if (!string.Equals(providerName, DefaultProviderName, StringComparison.Ordinal))
{
return _configuration[$"Azure:{providerName}:Endpoint"];
}

var injectedEndpoint = _configuration[OpenAiEndpointEnvironmentVariableName];
if (!string.IsNullOrWhiteSpace(injectedEndpoint))
{
return injectedEndpoint;
}

return _configuration["Azure:OpenAI:Endpoint"];
return _configuration[$"Azure:{providerName}:Endpoint"];
}

private string? ResolveProfileName(string? operationName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,20 @@ public async Task<List<AttachmentSummaryResultDto>> GenerateAttachmentSummariesA

return results;
}

// Internal-only: no HTTP endpoint, no auth check — safe for background job callers
[AllowAnonymous]
[RemoteService(IsEnabled = false)]
public virtual async Task<List<AttachmentSummaryResultDto>> GenerateAttachmentSummariesForPipelineAsync(List<System.Guid> attachmentIds, string? promptVersion = null)
{
if (attachmentIds.Count == 0) return [];

var results = new List<AttachmentSummaryResultDto>();
foreach (var attachmentId in attachmentIds)
{
await attachmentSummaryService.GenerateAndSaveAsync(attachmentId, promptVersion);
results.Add(new AttachmentSummaryResultDto { Completed = true });
}
return results;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class ApplicationAnalysisAppService(
IFeatureChecker featureChecker)
: AIAppService, IApplicationAnalysisAppService
{
public async Task<ApplicationAnalysisResultDto> GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null)
public virtual async Task<ApplicationAnalysisResultDto> GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null)
{
if (!await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis"))
{
Expand All @@ -25,4 +25,13 @@ public async Task<ApplicationAnalysisResultDto> GenerateApplicationAnalysisAsync
await applicationAnalysisService.RegenerateAndSaveAsync(applicationId, promptVersion);
return new ApplicationAnalysisResultDto { Completed = true };
}

// Internal-only: no HTTP endpoint, no auth check — safe for background job callers
[AllowAnonymous]
[RemoteService(IsEnabled = false)]
public virtual async Task<ApplicationAnalysisResultDto> GenerateApplicationAnalysisForPipelineAsync(Guid applicationId, string? promptVersion = null)
{
await applicationAnalysisService.RegenerateAndSaveAsync(applicationId, promptVersion);
return new ApplicationAnalysisResultDto { Completed = true };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,60 @@
using Unity.AI;
using Unity.AI.Operations;
using Unity.AI.Permissions;
using Unity.GrantManager.GrantApplications.Automation.Events;
using Volo.Abp;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Features;

namespace Unity.GrantManager.GrantApplications;

[Authorize(AIPermissions.Analysis.GenerateScoring)]
public class ApplicationScoringAppService(
IApplicationScoringService applicationScoringService,
IFeatureChecker featureChecker)
IFeatureChecker featureChecker,
ILocalEventBus localEventBus)
: AIAppService, IApplicationScoringAppService
{
public async Task<ApplicationScoringResultDto> GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null)
public virtual async Task<ApplicationScoringResultDto> GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null)
{
if (!await featureChecker.IsEnabledAsync("Unity.AI.Scoring"))
{
throw new UserFriendlyException("AI scoring is not enabled.");
}

await applicationScoringService.RegenerateAndSaveAsync(applicationId, promptVersion);

if (UnitOfWorkManager.Current != null)
{
var capturedId = applicationId;
UnitOfWorkManager.Current.OnCompleted(async () =>
{
await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent
{
ApplicationId = capturedId
});
});
}
else
{
await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent
{
ApplicationId = applicationId
});
}

return new ApplicationScoringResultDto
{
Completed = true
};
}

// Internal-only: no HTTP endpoint, no auth check — safe for background job callers
[AllowAnonymous]
[RemoteService(IsEnabled = false)]
public virtual async Task<ApplicationScoringResultDto> GenerateApplicationScoringForPipelineAsync(Guid applicationId, string? promptVersion = null)
{
await applicationScoringService.RegenerateAndSaveAsync(applicationId, promptVersion);
return new ApplicationScoringResultDto { Completed = true };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,26 +121,21 @@ private static List<WorksheetComponentMetaDataItemDto> ParseDataGridField(Custom
var worksheetName = SanitizeName(worksheet.Name);
var dataGridName = SanitizeName(field.Key);

// Track whether we successfully extracted columns from CHEFS schema
bool extractedFromChefs = false;

// If dynamic is true, try to extract columns from form schema
if (dataGridDefinition.Dynamic)
{
var headerMappingKey = MatchHeaderMapping(field.Name + ".DataGrid", submissionHeaderMapping);
List<DataGridDefinitionColumn>? dynamicColumns = null;

if (!string.IsNullOrWhiteSpace(headerMappingKey))
{
dynamicColumns = ExtractDynamicDataGridColumns(headerMappingKey, formSchema);
}

if (dynamicColumns != null && dynamicColumns.Count > 0)
{
// We found columns in the form schema, use them
// CHEFS schema includes ALL columns (both static and dynamic), so we mark this as extracted
extractedFromChefs = true;

// We found dynamic columns in the CHEFS form schema; emit them first.
// Any statically-defined columns on the DataGrid are merged in below.
foreach (var column in dynamicColumns)
{
// Use the key for the component Key (becomes PropertyName), sanitize for ID
Expand Down Expand Up @@ -179,16 +174,25 @@ private static List<WorksheetComponentMetaDataItemDto> ParseDataGridField(Custom
}
}

// Process additional defined columns only if we haven't already extracted them from CHEFS
// When dynamic is true and CHEFS extraction succeeded, the CHEFS schema already includes
// all columns (both static and dynamic), so we skip this to avoid duplicates
if (!extractedFromChefs && dataGridDefinition.Columns != null && dataGridDefinition.Columns.Count > 0)
// Process additional defined columns from the DataGrid definition.
// For mixed grids (Dynamic == true), CHEFS only returns the dynamic wide columns and omits
// statically-defined ones, so we must merge them in here (dynamic wins on key collision).
if (dataGridDefinition.Columns != null && dataGridDefinition.Columns.Count > 0)
{
// Create a component for each column in the DataGrid
var existingKeys = new HashSet<string>(
components.Select(c => c.Key ?? string.Empty),
StringComparer.OrdinalIgnoreCase);

foreach (var column in dataGridDefinition.Columns)
{
// Skip columns that were already emitted from the CHEFS extraction
if (existingKeys.Contains(column.Name))
{
continue;
}

var columnName = SanitizeName(column.Name);

var component = new WorksheetComponentMetaDataItemDto
{
Id = $"{field.Id}_{columnName}",
Expand All @@ -199,8 +203,9 @@ private static List<WorksheetComponentMetaDataItemDto> ParseDataGridField(Custom
TypePath = $"worksheet->section->datagrid->{MapDataGridColumnType(column.Type)}",
DataPath = $"({worksheetName}){dataGridName}->{column.Name}"
};

components.Add(component);
existingKeys.Add(column.Name);
}
}
else if (!dataGridDefinition.Dynamic && (dataGridDefinition.Columns == null || dataGridDefinition.Columns.Count == 0))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public async Task ParseDataGridField_DynamicWithFormSchema_ShouldExtractColumnsF
}

[Fact]
public async Task ParseDataGridField_DynamicWithFormSchema_ShouldSkipDefinedColumnsWhenChefsExtracted()
public async Task ParseDataGridField_DynamicWithFormSchema_ShouldMergeDefinedColumnsWithChefsExtracted()
{
// Arrange
using var uow = _unitOfWorkManager.Begin();
Expand All @@ -238,7 +238,7 @@ public async Task ParseDataGridField_DynamicWithFormSchema_ShouldSkipDefinedColu
await _worksheetRepository.InsertAsync(worksheet, true);
await uow.SaveChangesAsync();

// Definition has both dynamic=true AND static columns defined
// Definition has both dynamic=true AND static columns defined (mixed grid scenario)
var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid",
CustomFieldType.DataGrid,
@"{""dynamic"": true, ""columns"": [{""name"": ""staticCol"", ""type"": ""Text""}], ""summaryOption"": ""None""}");
Expand All @@ -264,16 +264,19 @@ public async Task ParseDataGridField_DynamicWithFormSchema_ShouldSkipDefinedColu
// Act
var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping);

// Assert — CHEFS extraction succeeded, so static columns should be skipped to avoid duplicates
// Assert — CHEFS only returns dynamic columns; statically-defined columns must still be emitted
result.ShouldNotBeNull();
result.Count.ShouldBe(1);
result.ShouldNotContain(c => c.Key == "staticCol");
result.Count.ShouldBe(2);
result.ShouldNotContain(c => c.Key == "dynamic_columns");

var dynamicCol = result.First();
dynamicCol.Key.ShouldBe("dynamicCol");
var dynamicCol = result.FirstOrDefault(c => c.Key == "dynamicCol");
dynamicCol.ShouldNotBeNull();
dynamicCol.Label.ShouldBe("Dynamic Column");
dynamicCol.Type.ShouldBe("Text");

var staticCol = result.FirstOrDefault(c => c.Key == "staticCol");
staticCol.ShouldNotBeNull();
staticCol.Type.ShouldBe("Text");
}

[Fact]
Expand Down Expand Up @@ -312,6 +315,44 @@ public async Task ParseDataGridField_DynamicWithNoHeaderMapping_ShouldFallBackTo
result.ShouldContain(c => c.Key == "col1");
}

[Fact]
public async Task ParseDataGridField_DynamicOnlyWithNoHeaderMapping_ShouldReturnOnlyPlaceholder()
{
// Arrange
using var uow = _unitOfWorkManager.Begin();

var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet");
var section = new WorksheetSection(Guid.NewGuid(), "TestSection");
worksheet.Sections.Add(section);

await _worksheetRepository.InsertAsync(worksheet, true);
await uow.SaveChangesAsync();

// Purely dynamic grid: dynamic=true and no static columns defined
var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid",
CustomFieldType.DataGrid,
@"{""dynamic"": true, ""columns"": [], ""summaryOption"": ""None""}");
section.AddField(field);
await uow.SaveChangesAsync();

worksheet = await _worksheetRepository.GetAsync(worksheet.Id);

// Header mapping does NOT contain an entry for this field, so CHEFS extraction cannot resolve
var submissionHeaderMapping = @"{""unrelated_key.DataGrid"": ""someGrid""}";
var formSchema = @"{ ""components"": [] }";

// Act
var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping);

// Assert — dynamic-only with no CHEFS resolution should yield exactly the placeholder
result.ShouldNotBeNull();
result.Count.ShouldBe(1);

var placeholder = result.First();
placeholder.Key.ShouldBe("dynamic_columns");
placeholder.Type.ShouldBe("Dynamic");
}

[Fact]
public async Task ParseDataGridField_DynamicWithNestedFormSchema_ShouldFindDataGridInPanel()
{
Expand Down
Loading
Loading