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

12571 add endpoint to delete layout set #12598

Merged
merged 11 commits into from
Apr 4, 2024
25 changes: 25 additions & 0 deletions backend/src/Designer/Controllers/AppDevelopmentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,31 @@ public async Task<ActionResult> UpdateLayoutSet(string org, string app, [FromRou
return Ok(layoutSets);
}

/// <summary>
/// Delete an existing layout set
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="app">Application identifier which is unique within an organisation.</param>
/// <param name="layoutSetIdToUpdate">The id of the layout set to delete</param>
/// <param name="cancellationToken">An <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
[HttpDelete]
[UseSystemTextJson]
[Route("layout-set/{layoutSetIdToUpdate}")]
public async Task<ActionResult> DeleteLayoutSet(string org, string app, [FromRoute] string layoutSetIdToUpdate, CancellationToken cancellationToken)
{
try
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);
LayoutSets layoutSets = await _appDevelopmentService.DeleteLayoutSet(editingContext, layoutSetIdToUpdate, cancellationToken);
return Ok(layoutSets);
}
catch (FileNotFoundException exception)
{
return NotFound($"Layout-sets.json not found: {exception}");
}
}

/// <summary>
/// Get rule handler in JSON structure
/// </summary>
Expand Down
11 changes: 0 additions & 11 deletions backend/src/Designer/Models/App/LayoutSets.cs

This file was deleted.

2 changes: 1 addition & 1 deletion backend/src/Designer/Models/LayoutSets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
public string Schema { get; set; }

[JsonPropertyName("sets")]
public List<LayoutSetConfig> Sets { get; set; }
public new List<LayoutSetConfig> Sets { get; set; }

[JsonExtensionData]
public IDictionary<string, object?> UnknownProperties { get; set; }
Expand All @@ -20,7 +20,7 @@
public class LayoutSetConfig
{
[JsonPropertyName("id")]
public string Id { get; set; }

Check warning on line 23 in backend/src/Designer/Models/LayoutSets.cs

View workflow job for this annotation

GitHub Actions / Run integration tests against actual gitea

Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

[JsonPropertyName("dataType")]
[CanBeNull] public string DataType { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,40 @@ public async Task<LayoutSets> UpdateLayoutSet(AltinnRepoEditingContext altinnRep
return await UpdateExistingLayoutSet(altinnAppGitRepository, layoutSets, layoutSetToReplace, newLayoutSet);
}

public async Task<LayoutSets> DeleteLayoutSet(AltinnRepoEditingContext altinnRepoEditingContext,
string layoutSetToDeleteId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
AltinnAppGitRepository altinnAppGitRepository =
_altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org,
altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
LayoutSets layoutSets = await altinnAppGitRepository.GetLayoutSetsFile(cancellationToken);
var layoutSetToDelete = layoutSets.Sets.Find(set => set.Id == layoutSetToDeleteId);
var dataTypeNameToRemoveTaskIdRef = layoutSetToDelete?.DataType;
if (!string.IsNullOrEmpty(dataTypeNameToRemoveTaskIdRef))
{
await DeleteTaskRefInApplicationMetadata(altinnAppGitRepository, dataTypeNameToRemoveTaskIdRef);
}

return await DeleteExistingLayoutSet(altinnAppGitRepository, layoutSets, layoutSetToDeleteId);
}

private async Task DeleteTaskRefInApplicationMetadata(AltinnAppGitRepository altinnAppGitRepository, string dataTypeId)
{
var applicationMetadata = await altinnAppGitRepository.GetApplicationMetadata();
var dataType = applicationMetadata.DataTypes.Find(dataType => dataType.Id == dataTypeId);
dataType.TaskId = null;
await altinnAppGitRepository.SaveApplicationMetadata(applicationMetadata);
}

private static async Task<LayoutSets> DeleteExistingLayoutSet(AltinnAppGitRepository altinnAppGitRepository, LayoutSets layoutSets, string layoutSetToDeleteId)
{
LayoutSetConfig layoutSetToDelete = layoutSets.Sets.Find(set => set.Id == layoutSetToDeleteId);
layoutSets.Sets.Remove(layoutSetToDelete);
await altinnAppGitRepository.SaveLayoutSetsFile(layoutSets);
return layoutSets;
}

private static async Task<LayoutSets> AddNewLayoutSet(AltinnAppGitRepository altinnAppGitRepository, LayoutSets layoutSets, LayoutSetConfig layoutSet)
{
layoutSets.Sets.Add(layoutSet);
Expand All @@ -328,7 +362,6 @@ private static async Task<LayoutSets> UpdateExistingLayoutSet(AltinnAppGitReposi
return layoutSets;
}


/// <inheritdoc />
public async Task<string> GetRuleHandler(AltinnRepoEditingContext altinnRepoEditingContext,
string layoutSetName, CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ public Task<ModelMetadata> GetModelMetadata(
/// <param name="cancellationToken">An <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
public Task<LayoutSets> UpdateLayoutSet(AltinnRepoEditingContext altinnRepoEditingContext, string layoutSetToUpdateId, LayoutSetConfig newLayoutSet, CancellationToken cancellationToken = default);

/// <summary>
/// Deletes an existing layout set in layout-sets.json based on layoutSetId and deletes connection between related dataType/task in application metadata
/// </summary>
/// <param name="altinnRepoEditingContext">An <see cref="AltinnRepoEditingContext"/>.</param>
/// <param name="layoutSetToDeleteId">The id of the layout set to replace</param>
/// <param name="cancellationToken">An <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
public Task<LayoutSets> DeleteLayoutSet(AltinnRepoEditingContext altinnRepoEditingContext, string layoutSetToDeleteId, CancellationToken cancellationToken = default);

/// <summary>
/// Gets the rule handler for a specific organization, application, developer, and layout set name.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Altinn.Platform.Storage.Interface.Models;
using Altinn.Studio.Designer.Factories;
using Altinn.Studio.Designer.Infrastructure.GitRepository;
using Altinn.Studio.Designer.Models;
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.AppDevelopmentController
{
public class DeleteLayoutSetTests(WebApplicationFactory<Program> factory)
: DisagnerEndpointsTestsBase<DeleteLayoutSetTests>(factory), IClassFixture<WebApplicationFactory<Program>>
{
private static string VersionPrefix(string org, string repository) =>
$"/designer/api/{org}/{repository}/app-development";

[Theory]
[InlineData("ttd", "app-with-layoutsets", "testUser", "layoutSet2")]
public async Task DeleteLayoutSets_SetWithoutDataTypeConnection_ReturnsOk(string org, string app, string developer,
string layoutSetToDeleteId)
{
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);
LayoutSets layoutSetsBefore = await GetLayoutSetsFile(org, targetRepository, developer);

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);

LayoutSets layoutSetsAfter = await GetLayoutSetsFile(org, targetRepository, developer);

layoutSetsBefore.Sets.Should().HaveCount(3);
Assert.True(layoutSetsBefore.Sets.Exists(set => set.Id == layoutSetToDeleteId));
layoutSetsAfter.Sets.Should().HaveCount(2);
Assert.False(layoutSetsAfter.Sets.Exists(set => set.Id == layoutSetToDeleteId));
}

[Theory]
[InlineData("ttd", "app-with-layoutsets", "testUser", "layoutSet1")]
public async Task DeleteLayoutSets_SetWithDataTypeConnection_ReturnsOk(string org, string app, string developer,
string layoutSetToDeleteId)
{
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);
string connectedDataType = "datamodel";
string connectedTaskId = "Task_1";
LayoutSets layoutSetsBefore = await GetLayoutSetsFile(org, targetRepository, developer);
Application appMetadataBefore = await GetApplicationMetadataFile(org, targetRepository, developer);

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);

LayoutSets layoutSetsAfter = await GetLayoutSetsFile(org, targetRepository, developer);
Application appMetadataAfter = await GetApplicationMetadataFile(org, targetRepository, developer);

layoutSetsBefore.Sets.Should().HaveCount(3);
appMetadataBefore.DataTypes.Find(dataType => dataType.Id == connectedDataType).TaskId.Should()
.Be(connectedTaskId);
Assert.True(layoutSetsBefore.Sets.Exists(set => set.Id == layoutSetToDeleteId));
layoutSetsAfter.Sets.Should().HaveCount(2);
appMetadataAfter.DataTypes.Find(dataType => dataType.Id == connectedDataType).TaskId.Should().BeNull();
Assert.False(layoutSetsAfter.Sets.Exists(set => set.Id == layoutSetToDeleteId));
}

[Theory]
[InlineData("ttd", "app-with-layoutsets", "testUser", "non-existing-layout-set")]
public async Task DeleteLayoutSet_IdNotFound_ReturnsUnAlteredLayoutSets(string org, string app, string developer,
string layoutSetToDeleteId)
{
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);

string layoutSetsBefore = TestDataHelper.GetFileFromRepo(org, app, developer, "App/ui/layout-sets.json");

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);

string responseContent = await response.Content.ReadAsStringAsync();

Assert.True(JsonUtils.DeepEquals(layoutSetsBefore, responseContent));
}

[Theory]
[InlineData("ttd", "app-without-layoutsets", "testUser", null)]
public async Task AddLayoutSet_AppWithoutLayoutSets_ReturnsNotFound(string org, string app, string developer,
string layoutSetToDeleteId)
{
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.NotFound);
}

private async Task<LayoutSets> GetLayoutSetsFile(string org, string app, string developer)
{
AltinnGitRepositoryFactory altinnGitRepositoryFactory =
new(TestDataHelper.GetTestDataRepositoriesRootDirectory());
AltinnAppGitRepository altinnAppGitRepository =
altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);

return await altinnAppGitRepository.GetLayoutSetsFile();
}

private async Task<Application> GetApplicationMetadataFile(string org, string app, string developer)
{
AltinnGitRepositoryFactory altinnGitRepositoryFactory =
new(TestDataHelper.GetTestDataRepositoriesRootDirectory());
AltinnAppGitRepository altinnAppGitRepository =
altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);

return await altinnAppGitRepository.GetApplicationMetadata();
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models.App;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
using Designer.Tests.Controllers.ApiTests;
using Designer.Tests.Utils;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { renderHookWithMockStore } from '../../test/mocks';
import { act, waitFor } from '@testing-library/react';
import { useDeleteLayoutSetMutation } from './useDeleteLayoutSetMutation';

// Test data:
const org = 'org';
const app = 'app';
const layoutSetToDeleteId = 'oldLayoutSetName';

describe('useDeleteLayoutSetMutation', () => {
it('Calls deleteLayoutSetMutation with correct arguments and payload', async () => {
const deleteLayoutSetResult = renderHookWithMockStore()(() =>
useDeleteLayoutSetMutation(org, app),
).renderHookResult.result;
await act(() =>
deleteLayoutSetResult.current.mutateAsync({
layoutSetIdToUpdate: layoutSetToDeleteId,
}),
);
await waitFor(() => expect(deleteLayoutSetResult.current.isSuccess).toBe(true));

expect(queriesMock.deleteLayoutSet).toHaveBeenCalledTimes(1);
expect(queriesMock.deleteLayoutSet).toHaveBeenCalledWith(org, app, layoutSetToDeleteId);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';

export const useDeleteLayoutSetMutation = (org: string, app: string) => {
const { deleteLayoutSet } = useServicesContext();
const queryClient = useQueryClient();

return useMutation({
mutationFn: ({ layoutSetIdToUpdate }: { layoutSetIdToUpdate: string }) =>
deleteLayoutSet(org, app, layoutSetIdToUpdate),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QueryKey.LayoutSets, org, app] });
},
});
};
1 change: 1 addition & 0 deletions frontend/packages/shared/src/api/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const headers = {
export const addAppAttachmentMetadata = (org: string, app: string, payload: ApplicationAttachmentMetadata) => post<void, ApplicationAttachmentMetadata>(appMetadataAttachmentPath(org, app), payload);
export const addLanguageCode = (org: string, app: string, language: string, payload: AddLanguagePayload) => post<void, AddLanguagePayload>(textResourcesPath(org, app, language), payload);
export const addLayoutSet = (org: string, app: string, layoutSetIdToUpdate: string, payload: LayoutSetConfig) => post(layoutSetPath(org, app, layoutSetIdToUpdate), payload);
export const deleteLayoutSet = (org: string, app: string, layoutSetIdToUpdate: string) => del(layoutSetPath(org, app, layoutSetIdToUpdate));
export const updateLayoutSet = (org: string, app: string, layoutSetIdToUpdate: string, payload: LayoutSetConfig) => put(layoutSetPath(org, app, layoutSetIdToUpdate), payload);
export const addRepo = (repoToAdd: AddRepoParams) => post<Repository>(`${createRepoPath()}${buildQueryParams(repoToAdd)}`);
export const addXsdFromRepo = (org: string, app: string, modelPath: string) => post<JsonSchema>(datamodelAddXsdFromRepoPath(org, app, modelPath));
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const ruleConfigPath = (org, app, layoutSetName) => `${basePath}/${org}/$
export const datamodelMetadataPath = (org, app, layoutSetName) => `${basePath}/${org}/${app}/app-development/model-metadata?${s({ layoutSetName })}`; // Get
export const layoutNamesPath = (org, app) => `${basePath}/${org}/${app}/app-development/layout-names`; // Get
export const layoutSetsPath = (org, app) => `${basePath}/${org}/${app}/app-development/layout-sets`; // Get
export const layoutSetPath = (org, app, layoutSetIdToUpdate) => `${basePath}/${org}/${app}/app-development/layout-set/${layoutSetIdToUpdate}`; // Put
export const layoutSetPath = (org, app, layoutSetIdToUpdate) => `${basePath}/${org}/${app}/app-development/layout-set/${layoutSetIdToUpdate}`; // Put, Delete
export const layoutSettingsPath = (org, app, layoutSetName) => `${basePath}/${org}/${app}/app-development/layout-settings?${s({ layoutSetName })}`; // Get, Post
export const formLayoutsPath = (org, app, layoutSetName) => `${basePath}/${org}/${app}/app-development/form-layouts?${s({ layoutSetName })}`; // Get
export const formLayoutPath = (org, app, layout, layoutSetName) => `${basePath}/${org}/${app}/app-development/form-layout/${layout}?${s({ layoutSetName })}`; // Post, Delete
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/shared/src/mocks/queriesMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export const queriesMock: ServicesContextProps = {
deleteDatamodel: jest.fn().mockImplementation(() => Promise.resolve()),
deleteFormLayout: jest.fn().mockImplementation(() => Promise.resolve()),
deleteLanguageCode: jest.fn().mockImplementation(() => Promise.resolve()),
deleteLayoutSet: jest.fn().mockImplementation(() => Promise.resolve()),
generateModels: jest.fn().mockImplementation(() => Promise.resolve()),
logout: jest.fn().mockImplementation(() => Promise.resolve()),
pushRepoChanges: jest.fn().mockImplementation(() => Promise.resolve()),
Expand Down
Loading