From 0b3bc75b88b4a87a8b291c7211c6e78bc85dcef5 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Tue, 1 Oct 2024 13:54:28 +0200 Subject: [PATCH 1/5] Add MediatR event for layoutset deletion --- .../Designer/Controllers/AppDevelopmentController.cs | 6 ++++++ backend/src/Designer/Events/LayoutSetDeletedEvent.cs | 10 ++++++++++ 2 files changed, 16 insertions(+) create mode 100644 backend/src/Designer/Events/LayoutSetDeletedEvent.cs diff --git a/backend/src/Designer/Controllers/AppDevelopmentController.cs b/backend/src/Designer/Controllers/AppDevelopmentController.cs index 29c1c2afac5..e125b157385 100644 --- a/backend/src/Designer/Controllers/AppDevelopmentController.cs +++ b/backend/src/Designer/Controllers/AppDevelopmentController.cs @@ -383,6 +383,12 @@ public async Task DeleteLayoutSet(string org, string app, [FromRou string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer); LayoutSets layoutSets = await _appDevelopmentService.DeleteLayoutSet(editingContext, layoutSetIdToUpdate, cancellationToken); + + await _mediator.Publish(new LayoutSetDeletedEvent + { + EditingContext = editingContext, + LayoutSetId = layoutSetIdToUpdate + }, cancellationToken); return Ok(layoutSets); } diff --git a/backend/src/Designer/Events/LayoutSetDeletedEvent.cs b/backend/src/Designer/Events/LayoutSetDeletedEvent.cs new file mode 100644 index 00000000000..aadbd44a4c0 --- /dev/null +++ b/backend/src/Designer/Events/LayoutSetDeletedEvent.cs @@ -0,0 +1,10 @@ +using Altinn.Studio.Designer.Models; +using MediatR; + +namespace Altinn.Studio.Designer.Events; + +public class LayoutSetDeletedEvent : INotification +{ + public string LayoutSetId { get; set; } + public AltinnRepoEditingContext EditingContext { get; set; } +} From 4efe11162e5d4fd2eb07fda6f48d97cfe75df9a5 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Wed, 2 Oct 2024 08:25:19 +0200 Subject: [PATCH 2/5] Add sync handler for deleted layoutset --- .../LayoutSetDeletedComponentRefHandler.cs | 67 +++++++++++++++++++ .../Designer/Hubs/SyncHub/SyncErrorCodes.cs | 1 + .../SyncSuccessQueriesInvalidator.ts | 2 +- 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs diff --git a/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs b/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs new file mode 100644 index 00000000000..41cb1b226f7 --- /dev/null +++ b/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Altinn.App.Core.Helpers; +using Altinn.Studio.Designer.Events; +using Altinn.Studio.Designer.Hubs.SyncHub; +using Altinn.Studio.Designer.Infrastructure.GitRepository; +using Altinn.Studio.Designer.Services.Interfaces; +using MediatR; + +namespace Altinn.Studio.Designer.EventHandlers.LayoutSetDeleted; + +public class LayoutSetDeletedComponentRefHandler(IAltinnGitRepositoryFactory altinnGitRepositoryFactory, IFileSyncHandlerExecutor fileSyncHandlerExecutor) : INotificationHandler +{ + public async Task Handle(LayoutSetDeletedEvent notification, CancellationToken cancellationToken) + { + AltinnAppGitRepository altinnAppGitRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(notification.EditingContext.Org, notification.EditingContext.Repo, notification.EditingContext.Developer); + + string[] layoutSetNames = altinnAppGitRepository.GetLayoutSetNames(); + + await fileSyncHandlerExecutor.ExecuteWithExceptionHandlingAndConditionalNotification( + notification.EditingContext, + SyncErrorCodes.LayoutSetSubLayoutSyncError, + "layouts", + async () => + { + bool hasChanges = false; + foreach (string layoutSetName in layoutSetNames) + { + Dictionary task = await altinnAppGitRepository.GetFormLayouts(layoutSetName, cancellationToken); + foreach (KeyValuePair formLayout in task) + { + hasChanges |= await RemoveComponentsReferencingLayoutSet(notification, altinnAppGitRepository, layoutSetName, formLayout, cancellationToken); + } + } + return hasChanges; + }); + } + + async Task RemoveComponentsReferencingLayoutSet(LayoutSetDeletedEvent notification, AltinnAppGitRepository altinnAppGitRepository, string layoutSetName, KeyValuePair formLayout, CancellationToken cancellationToken) + { + if (formLayout.Value["data"] == null || formLayout.Value["data"]["layout"] == null) + { + return false; + } + + bool hasChanges = false; + JsonArray jsonArray = formLayout.Value["data"]["layout"] as JsonArray; + jsonArray.RemoveAll(jsonNode => + { + if (jsonNode["layoutSet"] != null && jsonNode["layoutSet"].GetValue() == notification.LayoutSetId) + { + hasChanges = true; + return true; + } + return false; + }); + + if (hasChanges) + { + await altinnAppGitRepository.SaveLayout(layoutSetName, formLayout.Key + ".json", formLayout.Value, cancellationToken); + return true; + } + return false; + } +} diff --git a/backend/src/Designer/Hubs/SyncHub/SyncErrorCodes.cs b/backend/src/Designer/Hubs/SyncHub/SyncErrorCodes.cs index 3c0337ee0b2..82d51fb6c24 100644 --- a/backend/src/Designer/Hubs/SyncHub/SyncErrorCodes.cs +++ b/backend/src/Designer/Hubs/SyncHub/SyncErrorCodes.cs @@ -8,6 +8,7 @@ public static class SyncErrorCodes public const string ApplicationMetadataDataTypeSyncError = nameof(ApplicationMetadataDataTypeSyncError); public const string LayoutSetsDataTypeSyncError = nameof(LayoutSetsDataTypeSyncError); public const string LayoutSetComponentIdSyncError = nameof(LayoutSetComponentIdSyncError); + public const string LayoutSetSubLayoutSyncError = nameof(LayoutSetSubLayoutSyncError); public const string SettingsComponentIdSyncError = nameof(SettingsComponentIdSyncError); public const string LayoutPageAddSyncError = nameof(LayoutPageAddSyncError); } diff --git a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts index 3bfd79198d4..0e240ae8590 100644 --- a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts +++ b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.ts @@ -30,7 +30,7 @@ export class SyncSuccessQueriesInvalidator extends Queue { // Maps folder names to their cache keys for invalidation upon sync success - can be extended to include more folders private readonly folderNameCacheKeyMap: Record> = { - layouts: [QueryKey.FormLayouts, '[org]', '[app]', '[layoutSetName]'], + layouts: [QueryKey.FormLayouts, '[org]', '[app]'], }; public set layoutSetName(layoutSetName: string) { From 7bdb17d8e4d1600fc5e2597cd89e1af843a9ff84 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Wed, 2 Oct 2024 13:18:07 +0200 Subject: [PATCH 3/5] Refactor for clarity and readability --- .../LayoutSetDeletedComponentRefHandler.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs b/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs index 41cb1b226f7..95b38685a47 100644 --- a/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs +++ b/backend/src/Designer/EventHandlers/LayoutSetDeleted/LayoutSetDeletedComponentRefHandler.cs @@ -15,7 +15,10 @@ public class LayoutSetDeletedComponentRefHandler(IAltinnGitRepositoryFactory alt { public async Task Handle(LayoutSetDeletedEvent notification, CancellationToken cancellationToken) { - AltinnAppGitRepository altinnAppGitRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(notification.EditingContext.Org, notification.EditingContext.Repo, notification.EditingContext.Developer); + AltinnAppGitRepository altinnAppGitRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository( + notification.EditingContext.Org, + notification.EditingContext.Repo, + notification.EditingContext.Developer); string[] layoutSetNames = altinnAppGitRepository.GetLayoutSetNames(); @@ -28,28 +31,32 @@ await fileSyncHandlerExecutor.ExecuteWithExceptionHandlingAndConditionalNotifica bool hasChanges = false; foreach (string layoutSetName in layoutSetNames) { - Dictionary task = await altinnAppGitRepository.GetFormLayouts(layoutSetName, cancellationToken); - foreach (KeyValuePair formLayout in task) + Dictionary formLayouts = await altinnAppGitRepository.GetFormLayouts(layoutSetName, cancellationToken); + foreach (var formLayout in formLayouts) { - hasChanges |= await RemoveComponentsReferencingLayoutSet(notification, altinnAppGitRepository, layoutSetName, formLayout, cancellationToken); + hasChanges |= await RemoveComponentsReferencingLayoutSet( + notification, + altinnAppGitRepository, + layoutSetName, + formLayout, + cancellationToken); } } return hasChanges; }); } - async Task RemoveComponentsReferencingLayoutSet(LayoutSetDeletedEvent notification, AltinnAppGitRepository altinnAppGitRepository, string layoutSetName, KeyValuePair formLayout, CancellationToken cancellationToken) + private static async Task RemoveComponentsReferencingLayoutSet(LayoutSetDeletedEvent notification, AltinnAppGitRepository altinnAppGitRepository, string layoutSetName, KeyValuePair formLayout, CancellationToken cancellationToken) { - if (formLayout.Value["data"] == null || formLayout.Value["data"]["layout"] == null) + if (formLayout.Value["data"] is not JsonObject data || data["layout"] is not JsonArray layoutArray) { return false; } bool hasChanges = false; - JsonArray jsonArray = formLayout.Value["data"]["layout"] as JsonArray; - jsonArray.RemoveAll(jsonNode => + layoutArray.RemoveAll(jsonNode => { - if (jsonNode["layoutSet"] != null && jsonNode["layoutSet"].GetValue() == notification.LayoutSetId) + if (jsonNode["layoutSet"]?.GetValue() == notification.LayoutSetId) { hasChanges = true; return true; @@ -59,9 +66,8 @@ async Task RemoveComponentsReferencingLayoutSet(LayoutSetDeletedEvent noti if (hasChanges) { - await altinnAppGitRepository.SaveLayout(layoutSetName, formLayout.Key + ".json", formLayout.Value, cancellationToken); - return true; + await altinnAppGitRepository.SaveLayout(layoutSetName, $"{formLayout.Key}.json", formLayout.Value, cancellationToken); } - return false; + return hasChanges; } } From c2e7124ba3aa3d2317ea123f3c9dda8370656aa2 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Wed, 2 Oct 2024 14:39:05 +0200 Subject: [PATCH 4/5] Add test case for layoutSetDeletion with referecing component --- .../DeleteLayoutSetTests.cs | 41 ++++++++++++++++++- .../layoutSet2/layouts/layoutFile1InSet2.json | 28 ++++++++----- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/DeleteLayoutSetTests.cs b/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/DeleteLayoutSetTests.cs index cb15df89f79..8acf823a8a2 100644 --- a/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/DeleteLayoutSetTests.cs +++ b/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/DeleteLayoutSetTests.cs @@ -1,7 +1,8 @@ -using System.IO; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; -using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Altinn.Platform.Storage.Interface.Models; using Altinn.Studio.Designer.Factories; @@ -133,6 +134,32 @@ public async Task DeleteLayoutSet_AppWithoutLayoutSets_ReturnsNotFound(string or response.StatusCode.Should().Be(HttpStatusCode.NotFound); } + [Theory] + [InlineData("ttd", "app-with-layoutsets", "testUser", "layoutSet3", + "layoutSet2", "layoutFile1InSet2", "subform-component-id")] + public async Task DeleteLayoutSet_RemovesComponentsReferencingLayoutSet(string org, string app, string developer, string layoutSetToDeleteId, + string layoutSetWithRef, string layoutSetFile, string deletedComponentId) + { + string targetRepository = TestDataHelper.GenerateTestRepoName(); + await CopyRepositoryForTest(org, app, developer, targetRepository); + + string url = $"{VersionPrefix(org, targetRepository)}/layout-set/{layoutSetToDeleteId}"; + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Delete, url); + + using var response = await HttpClient.SendAsync(httpRequestMessage); + response.StatusCode.Should().Be(HttpStatusCode.OK, await response.Content.ReadAsStringAsync()); + + JsonNode formLayout = (await GetFormLayouts(org, targetRepository, developer, layoutSetWithRef))[layoutSetFile]; + JsonArray layout = formLayout["data"]?["layout"] as JsonArray; + + layout.Should().NotBeNull(); + layout + .Where(jsonNode => jsonNode["layoutSet"] != null) + .Should() + .NotContain(jsonNode => jsonNode["layoutSet"].GetValue() == deletedComponentId, + $"No components should reference the deleted layout set {deletedComponentId}"); + } + private async Task GetLayoutSetsFile(string org, string app, string developer) { AltinnGitRepositoryFactory altinnGitRepositoryFactory = @@ -143,6 +170,16 @@ private async Task GetLayoutSetsFile(string org, string app, string return await altinnAppGitRepository.GetLayoutSetsFile(); } + private async Task> GetFormLayouts(string org, string app, string developer, string layoutSetName) + { + AltinnGitRepositoryFactory altinnGitRepositoryFactory = + new(TestDataHelper.GetTestDataRepositoriesRootDirectory()); + AltinnAppGitRepository altinnAppGitRepository = + altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer); + Dictionary formLayouts = await altinnAppGitRepository.GetFormLayouts(layoutSetName); + return formLayouts; + } + private async Task GetApplicationMetadataFile(string org, string app, string developer) { AltinnGitRepositoryFactory altinnGitRepositoryFactory = diff --git a/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layoutSet2/layouts/layoutFile1InSet2.json b/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layoutSet2/layouts/layoutFile1InSet2.json index 2e97608b86d..32c4c651167 100644 --- a/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layoutSet2/layouts/layoutFile1InSet2.json +++ b/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layoutSet2/layouts/layoutFile1InSet2.json @@ -1,13 +1,19 @@ { - "schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", - "data": { - "layout": [{ - "id": "component-id", - "type": "Header", - "textResourceBindings": { - "title": "some-old-id", - "body": "another-key" - } - }] - } + "schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "component-id", + "type": "Header", + "textResourceBindings": { + "title": "some-old-id", + "body": "another-key" + } + }, + { + "id": "subform-component-id", + "layoutSet": "layoutSet3" + } + ] + } } From 05e7352cdb30bcbdfb2d8ca07570eacbfb8ffca4 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Thu, 3 Oct 2024 15:50:22 +0200 Subject: [PATCH 5/5] Fix failing unit test --- .../src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts index e2611f292f1..87de0c60acd 100644 --- a/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts +++ b/frontend/packages/shared/src/queryInvalidator/SyncSuccessQueriesInvalidator.test.ts @@ -67,7 +67,7 @@ describe('SyncSuccessQueriesInvalidator', () => { await waitFor(() => expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({ - queryKey: [QueryKey.FormLayouts, org, app, selectedLayoutSet], + queryKey: [QueryKey.FormLayouts, org, app], }), ); expect(queryClientMock.invalidateQueries).toHaveBeenCalledTimes(1);