Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Auto-update taskId in layout files when taskId is updated from process editor #13648

Merged
merged 27 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9283f81
Add event handler for Task IDs in layouts
ErlingHauan Sep 26, 2024
01e113a
Add error handling when a layoutSet does not contain any layouts
ErlingHauan Sep 27, 2024
a7e113c
Add specific catch clause FileNotFoundException
ErlingHauan Sep 27, 2024
09446c2
Use LINQ selector instead of immediate reassignment of variable
ErlingHauan Sep 27, 2024
5008f9e
Add tests for ProcessTaskIdChangedLayoutsHandler
ErlingHauan Sep 27, 2024
e7b2742
Small fixes
ErlingHauan Sep 27, 2024
7d5c9eb
Merge branch 'main' into summary2-taskid-event-handling
ErlingHauan Sep 27, 2024
83bace1
Merge branch 'main' into summary2-taskid-event-handling
ErlingHauan Sep 30, 2024
7fa247e
Refactor code into smaller functions
ErlingHauan Oct 1, 2024
fd39fb4
Remove comment
ErlingHauan Oct 1, 2024
e3cb1a7
Move for loop into function
ErlingHauan Oct 1, 2024
b0ab8a1
Merge branch 'main' into summary2-taskid-event-handling
ErlingHauan Oct 3, 2024
a1cfe03
Merge branch 'main' into summary2-taskid-event-handling
ErlingHauan Oct 7, 2024
4a42d73
Move logic outside for loop
ErlingHauan Oct 8, 2024
3714e1c
Fix PR comments
ErlingHauan Oct 8, 2024
8fefe50
Merge branch 'main' into summary2-taskid-event-handling
ErlingHauan Oct 8, 2024
154bc5f
Switch 'return' with 'continue' when layouts aren't found in a layout…
ErlingHauan Oct 8, 2024
8bd424f
Merge branch 'summary2-taskid-event-handling' of https://github.com/A…
ErlingHauan Oct 8, 2024
06e619f
Merge branch 'main' into summary2-taskid-event-handling
Jondyr Oct 14, 2024
90034d1
Invalidate LayoutSets and FormLayouts cache when using bpmnMutation
ErlingHauan Oct 14, 2024
7efda02
Add new StudioComponent with default size sm `StudioAlert`
Jondyr Oct 14, 2024
d90130a
Add null check before searching for tasks in layoutSets
ErlingHauan Oct 14, 2024
d929e98
Merge branch 'summary2-taskid-event-handling' of https://github.com/A…
ErlingHauan Oct 14, 2024
7550778
Remove reference to StudioAlert (belongs to a different branch)
ErlingHauan Oct 14, 2024
7ebcf1e
Remove FormLayouts query invalidation, as it caused issues in an unre…
ErlingHauan Oct 15, 2024
7b4e09a
Change handling path in backend and make forEach more local. Remove i…
ErlingHauan Oct 15, 2024
7f3e491
Merge branch 'main' into summary2-taskid-event-handling
Jondyr Oct 15, 2024
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
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;
bool hasChanged = false;
foreach (var layoutSet in layoutSets.Sets.Where(layoutSet => layoutSet.Tasks.Contains(oldId)))
{
layoutSet.Tasks.Remove(oldId);
layoutSet.Tasks.Add(newId);
changed = true;
hasChanged = true;
}

return changed;
return hasChanged;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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;
}

foreach (string layoutName in layoutNames)
{
string layoutPath = $"App/ui/{layoutSetName}/{layoutName}.json";

await _fileSyncHandlerExecutor.ExecuteWithExceptionHandlingAndConditionalNotification(
notification.EditingContext,
SyncErrorCodes.LayoutTaskIdSyncError,
layoutPath,
async () =>
{
bool hasChanged = false;
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"
}
]
}
}
Loading