Skip to content

Commit

Permalink
feat: Auto-update taskId in layout files
Browse files Browse the repository at this point in the history
...when taskId is updated from process editor (#13648)
  • Loading branch information
ErlingHauan authored Oct 15, 2024
1 parent 0a51994 commit 0726885
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,45 @@ public ProcessTaskIdChangedLayoutSetsHandler(IAltinnGitRepositoryFactory altinnG

public async Task Handle(ProcessTaskIdChangedEvent notification, CancellationToken cancellationToken)
{
bool hasChanges = false;
var repository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(
notification.EditingContext.Org,
notification.EditingContext.Repo,
notification.EditingContext.Developer);

if (!repository.AppUsesLayoutSets())
{
return;
}

await _fileSyncHandlerExecutor.ExecuteWithExceptionHandlingAndConditionalNotification(
notification.EditingContext,
SyncErrorCodes.LayoutSetsTaskIdSyncError,
"App/ui/layout-sets.json",
async () =>
{
var repository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(
notification.EditingContext.Org,
notification.EditingContext.Repo,
notification.EditingContext.Developer);

if (!repository.AppUsesLayoutSets())
{
return hasChanges;
}
bool hasChanged = false;

var layoutSets = await repository.GetLayoutSetsFile(cancellationToken);
if (TryChangeTaskIds(layoutSets, notification.OldId, notification.NewId))
if (TryChangeLayoutSetTaskIds(layoutSets, notification.OldId, notification.NewId))
{
await repository.SaveLayoutSets(layoutSets);
hasChanges = true;
hasChanged = true;
}

return hasChanges;
return hasChanged;
});
}

private static bool TryChangeTaskIds(LayoutSets layoutSets, string oldId, string newId)
private static bool TryChangeLayoutSetTaskIds(LayoutSets layoutSets, string oldId, string newId)
{
bool changed = false;
foreach (var layoutSet in layoutSets.Sets.Where(layoutSet => layoutSet.Tasks.Contains(oldId)))
bool hasChanged = false;
foreach (var layoutSet in layoutSets.Sets.Where(layoutSet => layoutSet.Tasks != null && layoutSet.Tasks.Contains(oldId)))
{
layoutSet.Tasks.Remove(oldId);
layoutSet.Tasks.Add(newId);
changed = true;
layoutSet.Tasks!.Remove(oldId);
layoutSet.Tasks!.Add(newId);
hasChanged = true;
}

return changed;
return hasChanged;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Events;
using Altinn.Studio.Designer.Hubs.SyncHub;
using Altinn.Studio.Designer.Services.Interfaces;
using MediatR;

namespace Altinn.Studio.Designer.EventHandlers.ProcessTaskIdChanged;

public class ProcessTaskIdChangedLayoutsHandler : INotificationHandler<ProcessTaskIdChangedEvent>
{
private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory;
private readonly IFileSyncHandlerExecutor _fileSyncHandlerExecutor;

public ProcessTaskIdChangedLayoutsHandler(IAltinnGitRepositoryFactory altinnGitRepositoryFactory,
IFileSyncHandlerExecutor fileSyncHandlerExecutor)
{
_altinnGitRepositoryFactory = altinnGitRepositoryFactory;
_fileSyncHandlerExecutor = fileSyncHandlerExecutor;
}

public async Task Handle(ProcessTaskIdChangedEvent notification, CancellationToken cancellationToken)
{
var repository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(
notification.EditingContext.Org,
notification.EditingContext.Repo,
notification.EditingContext.Developer);

if (!repository.AppUsesLayoutSets())
{
return;
}

var layoutSetsFile = await repository.GetLayoutSetsFile(cancellationToken);

foreach (string layoutSetName in layoutSetsFile.Sets.Select(layoutSet => layoutSet.Id))
{
string[] layoutNames;
try
{
layoutNames = repository.GetLayoutNames(layoutSetName);
}
catch (FileNotFoundException)
{
continue;
}

await _fileSyncHandlerExecutor.ExecuteWithExceptionHandlingAndConditionalNotification(
notification.EditingContext,
SyncErrorCodes.LayoutTaskIdSyncError,
$"App/ui/{layoutSetName}/layouts",
async () =>
{
bool hasChanged = false;

foreach (string layoutName in layoutNames)
{
var layout = await repository.GetLayout(layoutSetName, layoutName, cancellationToken);
if (TryChangeLayoutTaskIds(layout, notification.OldId, notification.NewId))
{
await repository.SaveLayout(layoutSetName, layoutName, layout, cancellationToken);
hasChanged = true;
}
}

return hasChanged;
});
}
}

private static bool TryChangeLayoutTaskIds(JsonNode node, string oldId, string newId)
{
bool hasChanged = false;

if (node is JsonObject jsonObject)
{
foreach (var property in jsonObject.ToList())
{
if (property.Key == "taskId" && property.Value?.ToString() == oldId)
{
jsonObject["taskId"] = newId;
hasChanged = true;
}

hasChanged |= TryChangeLayoutTaskIds(property.Value, oldId, newId);
}
}
else if (node is JsonArray jsonArray)
{
foreach (var element in jsonArray)
{
hasChanged |= TryChangeLayoutTaskIds(element, oldId, newId);
}
}

return hasChanged;
}
}
1 change: 1 addition & 0 deletions backend/src/Designer/Hubs/SyncHub/SyncErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public static class SyncErrorCodes
{
public const string ApplicationMetadataTaskIdSyncError = nameof(ApplicationMetadataTaskIdSyncError);
public const string LayoutSetsTaskIdSyncError = nameof(LayoutSetsTaskIdSyncError);
public const string LayoutTaskIdSyncError = nameof(LayoutTaskIdSyncError);
public const string PolicyFileTaskIdSyncError = nameof(PolicyFileTaskIdSyncError);
public const string ApplicationMetadataDataTypeSyncError = nameof(ApplicationMetadataDataTypeSyncError);
public const string LayoutSetsDataTypeSyncError = nameof(LayoutSetsDataTypeSyncError);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models.Dto;
using Designer.Tests.Controllers.ApiTests;
using Designer.Tests.Utils;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using SharedResources.Tests;
using Xunit;

namespace Designer.Tests.Controllers.ProcessModelingController.FileSync.TaskIdChangeTests;

public class LayoutFileSyncTaskIdTests : DesignerEndpointsTestsBase<LayoutFileSyncTaskIdTests>, IClassFixture<WebApplicationFactory<Program>>
{
public LayoutFileSyncTaskIdTests(WebApplicationFactory<Program> factory) : base(factory)
{
}

private static string GetVersionPrefix(string org, string repository)
{
return $"/designer/api/{org}/{repository}/process-modelling/process-definition-latest";
}

[Theory]
[MemberData(nameof(GetReferencedTaskIdTestData))]
public async Task UpsertProcessDefinitionAndNotify_ShouldUpdateLayout_WhenReferencedTaskIdIsChanged(
string org,
string app,
string developer,
string bpmnFilePath,
ProcessDefinitionMetadata metadata)
{
// Arrange
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);

string processContent = SharedResourcesHelper.LoadTestDataAsString(bpmnFilePath)
.Replace(metadata.TaskIdChange.OldId, metadata.TaskIdChange.NewId);

using var processStream = new MemoryStream(Encoding.UTF8.GetBytes(processContent));

string url = GetVersionPrefix(org, targetRepository);

using var form = new MultipartFormDataContent();
form.Add(new StreamContent(processStream), "content", "process.bpmn");
form.Add(new StringContent(
JsonSerializer.Serialize(metadata, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }),
Encoding.UTF8,
MediaTypeNames.Application.Json), "metadata");

// Act
using var response = await HttpClient.PutAsync(url, form);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.Accepted);

string layoutFilePath = "App/ui/layoutSet2/layouts/layoutFile2InSet2.json";
string layoutContent = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, layoutFilePath);

JsonNode layout = JsonSerializer.Deserialize<JsonNode>(layoutContent);
string newTaskId = layout["data"]?["layout"]?[0]?["target"]?["taskId"]?.ToString();

newTaskId.Should().Be(metadata.TaskIdChange.NewId);
newTaskId.Should().NotBe(metadata.TaskIdChange.OldId);
}

[Theory]
[MemberData(nameof(GetUnreferencedTaskIdTestData))]
public async Task UpsertProcessDefinitionAndNotify_ShouldNotUpdateLayout_WhenUnreferencedTaskIdIsChanged(
string org,
string app,
string developer,
string bpmnFilePath,
ProcessDefinitionMetadata metadata)
{
// Arrange
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);

string layoutPath = "App/ui/layoutSet2/layouts/layoutFile2InSet2.json";
string layoutBeforeUpdate = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, layoutPath);

string processContent = SharedResourcesHelper.LoadTestDataAsString(bpmnFilePath)
.Replace(metadata.TaskIdChange.OldId, metadata.TaskIdChange.NewId);

using var processStream = new MemoryStream(Encoding.UTF8.GetBytes(processContent));

string url = GetVersionPrefix(org, targetRepository);

using var form = new MultipartFormDataContent();
form.Add(new StreamContent(processStream), "content", "process.bpmn");
form.Add(new StringContent(
JsonSerializer.Serialize(metadata, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }),
Encoding.UTF8,
MediaTypeNames.Application.Json), "metadata");

// Act
using var response = await HttpClient.PutAsync(url, form);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.Accepted);

string layoutAfterUpdate = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, layoutPath);
layoutAfterUpdate.Should().Be(layoutBeforeUpdate);
}

public static IEnumerable<object[]> GetReferencedTaskIdTestData()
{
// "Task_1" is targeted by Summary2 component in "app-with-layoutsets"
yield return new object[]
{
"ttd",
"app-with-layoutsets",
"testUser", "App/config/process/process.bpmn",
new ProcessDefinitionMetadata { TaskIdChange = new TaskIdChange { OldId = "Task_1", NewId = "SomeNewId" } }
};
}

public static IEnumerable<object[]> GetUnreferencedTaskIdTestData()
{
// "Task_2" is not targeted by Summary2 component in "app-with-layoutsets"
yield return new object[] { "ttd",
"app-with-layoutsets",
"testUser",
"App/config/process/process.bpmn",
new ProcessDefinitionMetadata { TaskIdChange = new TaskIdChange { OldId = "Task_2", NewId = "SomeNewId" } } };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ public async Task UpsertProcessDefinition_ShouldSyncLayoutSets(string org, strin

string processContent = SharedResourcesHelper.LoadTestDataAsString(bpmnFilePath);
processContent.Replace(metadata.TaskIdChange.OldId, metadata.TaskIdChange.NewId);
//processContent = metadata.TaskIdChange.Aggregate(processContent,
// (current, metadataTaskIdChange) => current.Replace(metadataTaskIdChange.OldId, metadataTaskIdChange.NewId));
using var processStream = new MemoryStream(Encoding.UTF8.GetBytes(processContent));

string url = VersionPrefix(org, targetRepository);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
{
"schema":"https://altinncdn.no/schemas/json/layout/layout.schema.v1.json",
"data": {
"layout": []
"layout": [
{
"target": {
"type": "page",
"id": "layoutFile1inSet1",
"taskId": "Task_1"
},
"id": "Summary2-B5VMK2",
"type": "Summary2"
}
]
}
}

0 comments on commit 0726885

Please sign in to comment.