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

refactor(pages-api): implement page api on frontend #14694

Merged
merged 21 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e6abd94
refactor(pages-api): implement page api on frontend
Jondyr Feb 17, 2025
d030cd7
refactor(pages-api): create api endpoints for layout actions based on…
Jondyr Feb 17, 2025
8eea8d3
refactor(pages-api): change parameter for layout operations to just u…
Jondyr Feb 17, 2025
a188a99
refactor(pages-api): create layout service to handle more explicit la…
Jondyr Feb 17, 2025
5923eb8
Update frontend/packages/ux-editor/src/components/Properties/PageConf…
Jondyr Feb 25, 2025
29af2df
Update frontend/packages/ux-editor/src/components/Properties/PageConf…
Jondyr Feb 25, 2025
ac84aa3
style: formatting
Feb 25, 2025
9e04946
fix: ModifyPage should be PUT
Feb 25, 2025
63e5109
Merge branch 'main' into spr/main/cf35ff23
Jondyr Feb 25, 2025
82a53b8
Merge branch 'spr/main/cf35ff23' into spr/main/d6447eac
Jondyr Feb 25, 2025
14de480
Merge branch 'spr/main/d6447eac' into spr/main/2ac25caa
Jondyr Feb 25, 2025
45c986e
Merge branch 'spr/main/2ac25caa' into spr/main/8128c92c
Jondyr Feb 25, 2025
d948cc6
fix: modifyPage should be PUT
Feb 25, 2025
a9dc40e
Merge branch 'main' into spr/main/cf35ff23
Jondyr Feb 27, 2025
0fc87ee
Merge branch 'spr/main/cf35ff23' into spr/main/d6447eac
Jondyr Feb 27, 2025
f9ba3db
Merge branch 'spr/main/d6447eac' into spr/main/2ac25caa
Jondyr Feb 27, 2025
4706def
Merge branch 'spr/main/2ac25caa' into spr/main/8128c92c
Jondyr Feb 27, 2025
9050792
Merge branch 'main' into spr/main/cf35ff23
Jondyr Mar 3, 2025
0859b0e
Merge branch 'spr/main/cf35ff23' into spr/main/d6447eac
Jondyr Mar 3, 2025
6721a53
Merge branch 'spr/main/d6447eac' into spr/main/2ac25caa
Jondyr Mar 3, 2025
a0c3e0f
Merge branch 'spr/main/2ac25caa' into spr/main/8128c92c
Jondyr Mar 3, 2025
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
127 changes: 127 additions & 0 deletions backend/src/Designer/Controllers/LayoutController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;

namespace Altinn.Studio.Designer.Controllers
{
[ApiController]
[Authorize]
[AutoValidateAntiforgeryToken]
[Route("designer/api/{org}/{app}/layouts/layoutSet/{layoutSetId}/")]
public class LayoutController(ILayoutService layoutService) : Controller
{
[EndpointSummary("Retrieve pages")]
[ProducesResponseType<Pages>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet("pages")]
public async Task<ActionResult<Pages>> GetPages(
[FromRoute] string org,
[FromRoute] string app,
[FromRoute] string layoutSetId
)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);
Pages pages = await layoutService.GetPagesByLayoutSetId(editingContext, layoutSetId);
return Ok(pages);
}

[EndpointSummary("Create page")]
[ProducesResponseType<Pages>(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[HttpPost("pages")]
public async Task<ActionResult<Page>> CreatePage(
[FromRoute] string org,
[FromRoute] string app,
[FromRoute] string layoutSetId,
[FromBody] Page page
)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);

Page existingPage = await layoutService.GetPageById(editingContext, layoutSetId, page.id);
if (existingPage != null)
{
return Conflict();
}
await layoutService.CreatePage(editingContext, layoutSetId, page.id);
return Created();
}

[EndpointSummary("Retrieve page")]
[ProducesResponseType<Page>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet("pages/{pageId}")]
[ProducesResponseType<Page>(StatusCodes.Status200OK)]
public async Task<ActionResult<Page>> GetPage(
[FromRoute] string org,
[FromRoute] string app,
[FromRoute] string layoutSetId,
[FromRoute] string pageId
)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);
Page page = await layoutService.GetPageById(editingContext, layoutSetId, pageId);
if (page == null)
{
return NotFound();
}
return Ok(page);
}

[EndpointSummary("Modify page")]
[ProducesResponseType<Page>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPut("pages/{pageId}")]
public async Task<ActionResult<Page>> ModifyPage(
[FromRoute] string org,
[FromRoute] string app,
[FromRoute] string layoutSetId,
[FromRoute] string pageId,
[FromBody] Page page
)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);
Page existingPage = await layoutService.GetPageById(editingContext, layoutSetId, pageId);
if (existingPage == null)
{
return NotFound();
}

await layoutService.UpdatePage(editingContext, layoutSetId, pageId, page);
return Ok();
}

[EndpointSummary("Delete page")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpDelete("pages/{pageId}")]
public async Task<ActionResult> DeletePage(
[FromRoute] string org,
[FromRoute] string app,
[FromRoute] string layoutSetId,
[FromRoute] string pageId
)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);
Page page = await layoutService.GetPageById(editingContext, layoutSetId, pageId);
if (page == null)
{
return NotFound();
}

await layoutService.DeletePage(editingContext, layoutSetId, pageId);
return Ok();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ await _fileSyncHandlerExecutor.ExecuteWithExceptionHandlingAndConditionalNotific
{
bool hasChanges = false;
string[] layoutNames = repository.GetLayoutNames(notification.LayoutSetName);
foreach (var layoutName in layoutNames)
foreach (var layoutFileName in layoutNames)
{

string layoutName = layoutFileName.Replace(".json", "");
var layout = await repository.GetLayout(notification.LayoutSetName, layoutName, cancellationToken);
if (TryChangeComponentId(layout, notification.OldComponentId, notification.NewComponentId))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ public class AltinnAppGitRepository : AltinnGitRepository
private const string SchemaFilePatternJson = "*.schema.json";
private const string SchemaFilePatternXsd = "*.xsd";

public static readonly string InitialLayoutFileName = "Side1.json";
public static readonly string InitialLayoutFileName = "Side1";

public readonly JsonNode InitialLayout = new JsonObject { ["$schema"] = LayoutSchemaUrl, ["data"] = new JsonObject { ["layout"] = new JsonArray([]) } };

public readonly JsonNode InitialLayoutSettings = new JsonObject { ["$schema"] = LayoutSettingsSchemaUrl, ["pages"] = new JsonObject { ["order"] = new JsonArray([InitialLayoutFileName.Replace(".json", "")]) } };
public readonly JsonNode InitialLayoutSettings = new JsonObject { ["$schema"] = LayoutSettingsSchemaUrl, ["pages"] = new JsonObject { ["order"] = new JsonArray([InitialLayoutFileName]) } };

private static readonly JsonSerializerOptions JsonOptions = new()
{
Expand Down Expand Up @@ -359,6 +359,16 @@ public void DeleteTexts(string languageCode)
}
}

public async Task CreatePageLayoutFile(string layoutSetId, string pageId)
{
JsonObject defaultPageLayout = new()
{
["$schema"] = LayoutSchemaUrl,
["data"] = new JsonObject { ["layout"] = new JsonArray([]) }
};
await WriteObjectByRelativePathAsync(Path.Combine([LayoutsFolderName, layoutSetId, LayoutsInSetFolderName, $"{pageId}.json"]), defaultPageLayout);
}

/// <summary>
/// Returns all the layouts for a specific layout set
/// </summary>
Expand All @@ -370,10 +380,11 @@ public async Task<Dictionary<string, JsonNode>> GetFormLayouts(string layoutSetN
cancellationToken.ThrowIfCancellationRequested();
Dictionary<string, JsonNode> formLayouts = new();
string[] layoutNames = GetLayoutNames(layoutSetName);
foreach (string layoutName in layoutNames)
foreach (string layoutFileName in layoutNames)
{
string layoutName = layoutFileName.Replace(".json", "");
JsonNode layout = await GetLayout(layoutSetName, layoutName, cancellationToken);
formLayouts[layoutName.Replace(".json", "")] = layout;
formLayouts[layoutName] = layout;
}

return formLayouts;
Expand Down Expand Up @@ -479,7 +490,7 @@ public string[] GetLayoutNames(string layoutSetName)
{
foreach (string layoutPath in GetFilesByRelativeDirectory(layoutSetPath))
{
layoutNames.Add(Path.GetFileName(layoutPath));
layoutNames.Add(Path.GetFileNameWithoutExtension(layoutPath));
}
}

Expand Down Expand Up @@ -652,11 +663,11 @@ public async Task SaveLayout(string layoutSetName, string layoutFileName, JsonNo
await WriteTextByRelativePathAsync(layoutFilePath, serializedLayout, true, cancellationToken);
}

public void UpdateFormLayoutName(string layoutSetName, string layoutFileName, string newFileName)
public void UpdateFormLayoutName(string layoutSetName, string layoutName, string newLayoutName)
{
string currentFilePath = GetPathToLayoutFile(layoutSetName, layoutFileName);
string newFilePath = GetPathToLayoutFile(layoutSetName, newFileName);
MoveFileByRelativePath(currentFilePath, newFilePath, newFileName);
string currentFilePath = GetPathToLayoutFile(layoutSetName, layoutName);
string newFilePath = GetPathToLayoutFile(layoutSetName, newLayoutName);
MoveFileByRelativePath(currentFilePath, newFilePath, newLayoutName);
}

public async Task<LayoutSets> GetLayoutSetsFile(CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -1067,11 +1078,11 @@ private static string GetPathToLayoutSet(string layoutSetName, bool excludeLayou
}

// can be null if app does not use layout set
private static string GetPathToLayoutFile(string layoutSetName, string fileName)
private static string GetPathToLayoutFile(string layoutSetName, string layoutName)
{
return string.IsNullOrEmpty(layoutSetName) ?
Path.Combine(LayoutsFolderName, LayoutsInSetFolderName, fileName) :
Path.Combine(LayoutsFolderName, layoutSetName, LayoutsInSetFolderName, fileName);
Path.Combine(LayoutsFolderName, LayoutsInSetFolderName, $"{layoutName}.json") :
Path.Combine(LayoutsFolderName, layoutSetName, LayoutsInSetFolderName, $"{layoutName}.json");
}

// can be null if app does not use layout set
Expand Down
1 change: 1 addition & 0 deletions backend/src/Designer/Infrastructure/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol
services.AddTransient<IInstanceService, InstanceService>();
services.AddTransient<IProcessModelingService, ProcessModelingService>();
services.AddTransient<IImagesService, ImagesService>();
services.AddTransient<ILayoutService, LayoutService>();
services.RegisterDatamodeling(configuration);
services.RegisterSettingsSingleton<KafkaSettings>(configuration);
services.AddTransient<IKafkaProducer, KafkaProducer>();
Expand Down
9 changes: 9 additions & 0 deletions backend/src/Designer/Models/Dto/Page.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace Altinn.Studio.Designer.Models.Dto;

public class Page
{
[JsonPropertyName("id")]
public string id { get; set; }
}
10 changes: 10 additions & 0 deletions backend/src/Designer/Models/Dto/Pages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Altinn.Studio.Designer.Models.Dto;

public class Pages
{
[JsonPropertyName("pages")]
public List<Page> pages { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ public async Task SaveFormLayout(AltinnRepoEditingContext altinnRepoEditingConte
string layoutName, JsonNode formLayout, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string layoutFileName = $"{layoutName}.json";
AltinnAppGitRepository altinnAppGitRepository =
_altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org,
altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
Expand All @@ -79,14 +78,13 @@ public async Task SaveFormLayout(AltinnRepoEditingContext altinnRepoEditingConte
"This app uses layout sets, but no layout set name was provided for this request");
}

await altinnAppGitRepository.SaveLayout(layoutSetName, layoutFileName, formLayout, cancellationToken);
await altinnAppGitRepository.SaveLayout(layoutSetName, layoutName, formLayout, cancellationToken);
}

/// <inheritdoc />
public void DeleteFormLayout(AltinnRepoEditingContext altinnRepoEditingContext, string layoutSetName,
string layoutName)
{
string layoutFileName = $"{layoutName}.json";
AltinnAppGitRepository altinnAppGitRepository =
_altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org,
altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
Expand All @@ -97,7 +95,7 @@ public void DeleteFormLayout(AltinnRepoEditingContext altinnRepoEditingContext,
"This app uses layout sets, but no layout set name was provided for this request");
}

altinnAppGitRepository.DeleteLayout(layoutSetName, layoutFileName);
altinnAppGitRepository.DeleteLayout(layoutSetName, layoutName);
}

/// <inheritdoc />
Expand All @@ -114,7 +112,7 @@ public void UpdateFormLayoutName(AltinnRepoEditingContext altinnRepoEditingConte
"This app uses layout sets, but no layout set name was provided for this request");
}

altinnAppGitRepository.UpdateFormLayoutName(layoutSetName, $"{layoutName}.json", $"{newName}.json");
altinnAppGitRepository.UpdateFormLayoutName(layoutSetName, layoutName, newName);
}

/// <inheritdoc />
Expand Down Expand Up @@ -586,7 +584,7 @@ public async Task AddComponentToLayout(
editingContext.Repo,
editingContext.Developer
);
JsonNode formLayout = await altinnAppGitRepository.GetLayout(layoutSetName, $"{layoutName}.json", cancellationToken);
JsonNode formLayout = await altinnAppGitRepository.GetLayout(layoutSetName, layoutName, cancellationToken);
if (formLayout["data"] is not JsonObject data || data["layout"] is not JsonArray layoutArray)
{
throw new InvalidOperationException("Invalid form layout structure");
Expand Down Expand Up @@ -802,7 +800,7 @@ private async Task<bool> UpdateLayoutReferences(AltinnAppGitRepository altinnApp

if (hasLayoutChanges)
{
await altinnAppGitRepository.SaveLayout(layoutSet.Id, $"{layout.Key}.json", layout.Value, cancellationToken);
await altinnAppGitRepository.SaveLayout(layoutSet.Id, layout.Key, layout.Value, cancellationToken);
hasChanges = true;
}
}
Expand Down
85 changes: 85 additions & 0 deletions backend/src/Designer/Services/Implementation/LayoutService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Infrastructure.GitRepository;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.Services.Interfaces;

namespace Altinn.Studio.Designer.Services.Implementation
{
public class LayoutService(IAltinnGitRepositoryFactory altinnGitRepositoryFactory) : ILayoutService
{
private static Pages GetPagesFromSettings(JsonNode settings)
{
Pages pages = new()
{
pages = []
};

JsonNode pagesNode = settings["pages"];
JsonArray pagesArray = pagesNode["order"] as JsonArray;
foreach (JsonNode pageNode in pagesArray)
{
string pageId = pageNode.GetValue<string>();
pages.pages.Add(new Page { id = pageId });
}

return pages;
}

public async Task<Pages> GetPagesByLayoutSetId(AltinnRepoEditingContext editingContext, string layoutSetId)
{
AltinnAppGitRepository appRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(editingContext.Org, editingContext.Repo, editingContext.Developer);
JsonNode jsonNode = await appRepository.GetLayoutSettingsAndCreateNewIfNotFound(layoutSetId);
Pages pages = GetPagesFromSettings(jsonNode);
return pages;
}

public async Task<Page> GetPageById(AltinnRepoEditingContext editingContext, string layoutSetId, string pageId)
{
AltinnAppGitRepository appRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(editingContext.Org, editingContext.Repo, editingContext.Developer);
JsonNode jsonNode = await appRepository.GetLayoutSettingsAndCreateNewIfNotFound(layoutSetId);
Pages pages = GetPagesFromSettings(jsonNode);
return pages.pages.Find(page => page.id == pageId);
}

public async Task CreatePage(AltinnRepoEditingContext editingContext, string layoutSetId, string pageId)
{
AltinnAppGitRepository appRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(editingContext.Org, editingContext.Repo, editingContext.Developer);

await appRepository.CreatePageLayoutFile(layoutSetId, pageId);

JsonNode jsonNode = await appRepository.GetLayoutSettingsAndCreateNewIfNotFound(layoutSetId);
(jsonNode["pages"]["order"] as JsonArray).Add(pageId);
await appRepository.SaveLayoutSettings(layoutSetId, jsonNode);
}

public async Task DeletePage(AltinnRepoEditingContext editingContext, string layoutSetId, string pageId)
{
AltinnAppGitRepository appRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(editingContext.Org, editingContext.Repo, editingContext.Developer);

appRepository.DeleteLayout(layoutSetId, pageId);

JsonNode jsonNode = await appRepository.GetLayoutSettingsAndCreateNewIfNotFound(layoutSetId);
JsonArray orderArray = jsonNode["pages"]["order"] as JsonArray;
JsonNode orderPage = orderArray.First(node => node.GetValue<string>().Equals(pageId));
orderArray.Remove(orderPage);
await appRepository.SaveLayoutSettings(layoutSetId, jsonNode);
}

public async Task UpdatePage(AltinnRepoEditingContext editingContext, string layoutSetId, string pageId, Page page)
{
AltinnAppGitRepository appRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(editingContext.Org, editingContext.Repo, editingContext.Developer);

appRepository.UpdateFormLayoutName(layoutSetId, pageId, page.id);

JsonNode jsonNode = await appRepository.GetLayoutSettingsAndCreateNewIfNotFound(layoutSetId);
JsonArray orderArray = jsonNode["pages"]["order"] as JsonArray;
JsonNode orderPage = orderArray.First(node => node.GetValue<string>().Equals(pageId));
int pageIndex = orderArray.IndexOf(orderPage);
orderArray[pageIndex] = page.id;
await appRepository.SaveLayoutSettings(layoutSetId, jsonNode);
}
}
}
Loading