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: Proof of concept for content library on organisation level #14864

Merged
merged 63 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f20335b
Add mocks for code list endpoints (#14528)
TomasEng Jan 28, 2025
fbc8886
feat: add paths for org-level code lists (#14530)
ErlingHauan Jan 28, 2025
ce1dff3
Org text resource mocks (#14534)
TomasEng Jan 28, 2025
d179445
feat: create endpoints for codelists on org level (#14533)
Konrad-Simso Jan 28, 2025
5e62728
feat: add org content library (#14535)
standeren Jan 29, 2025
af0d8ec
feat: query for get text resources for org (#14541)
wrt95 Feb 3, 2025
af80c6f
feat: mutation for create text resource for org (#14542)
wrt95 Feb 4, 2025
a016495
feat: mutation for update text resource for org (#14543)
wrt95 Feb 5, 2025
7533d17
feat: query/mutation hooks for org code lists (#14540)
ErlingHauan Feb 6, 2025
4c6fdbc
feat: add org-library and apps menu items and additional routing path…
standeren Feb 7, 2025
fcc0f52
feat: Display info message on library when the context is not an org …
TomasEng Feb 11, 2025
846defc
refactor: codeListService (#14647)
Konrad-Simso Feb 12, 2025
80d1abe
fix: adding missing white line under active tab on dashboard (#14618)
wrt95 Feb 13, 2025
67ac199
feat: 14508/delete code list for org (#14630)
wrt95 Feb 13, 2025
8828edd
feat: 14508/upload code list for org (#14629)
wrt95 Feb 13, 2025
cde4e0f
feat: update code list for org (#14632)
wrt95 Feb 13, 2025
7fbc132
feat: code list endpoints on org level (#14650)
Konrad-Simso Feb 14, 2025
67ffd07
fix: update CodeListPageUtils handle undefined prop (#14670)
Konrad-Simso Feb 14, 2025
8901b09
fix: update org codelist controller (#14667)
Konrad-Simso Feb 14, 2025
3e9eb01
feat: 14508/get code lists for org (#14653)
wrt95 Feb 14, 2025
a963aba
feat: text resource endpoints org level (#14655)
Konrad-Simso Feb 17, 2025
bd3ab53
fix: rename app to dashboard (#14680)
wrt95 Feb 17, 2025
7ca5c54
Feat/14609/initial setup for org library playwright tests (#14690)
wrt95 Feb 17, 2025
0aab0d3
test: 14609/playwright for navigate to code lists (#14693)
wrt95 Feb 17, 2025
ad1c449
feat: Adding feature flag for org library (#14679)
wrt95 Feb 18, 2025
8c08016
fix: remove warning on dashboard console (#14700)
wrt95 Feb 18, 2025
f3d5508
Merge branch 'main' into org-library-mvp
TomasEng Feb 19, 2025
d5ea724
Merge branch 'org-library-mvp' of https://github.com/Altinn/altinn-st…
TomasEng Feb 19, 2025
c441e99
test: 14609 - Playwright test for dashboard navigation (#14713)
wrt95 Feb 20, 2025
0c0b5e2
Merge branch 'main' into org-library-mvp
wrt95 Feb 21, 2025
57ecb0e
test: helper functions and test data for org level tests (#14768)
Konrad-Simso Feb 24, 2025
b6687cf
Merge branch 'main' into org-library-mvp
Konrad-Simso Feb 24, 2025
5873d30
fix: remove trailing coma in setup (#14791)
wrt95 Feb 24, 2025
ca06723
Merge branch 'main' into org-library-mvp
wrt95 Feb 24, 2025
8848440
docs: fix documentation OrgCodeListController (#14767)
Konrad-Simso Feb 25, 2025
8389beb
fix: Add verification for local clone (#14793)
Konrad-Simso Feb 25, 2025
c50e81a
Merge branch 'main' into org-library-mvp
wrt95 Feb 25, 2025
be31e4a
test: 14609/playwright test for create codelist for org (#14794)
wrt95 Feb 25, 2025
10b2079
Merge branch 'org-library-mvp' of github.com:Altinn/altinn-studio int…
wrt95 Feb 25, 2025
94b866a
test: 14609 - playwright test for upload codelist (#14801)
wrt95 Feb 26, 2025
c088b24
test: 14609 - playwright test for delete codelist org (#14802)
wrt95 Feb 27, 2025
1b41a80
test: tests for AltinnOrgGitRepository (#14771)
Konrad-Simso Feb 27, 2025
4108446
test: tests for OrgTextController (#14809)
Konrad-Simso Feb 27, 2025
7bf6248
test: tests for OrgCodeListService (#14770)
Konrad-Simso Feb 27, 2025
312ea61
test: tests for OrgCodeListController (#14769)
Konrad-Simso Feb 27, 2025
945c7c4
test: 14609 - playwright test for adding adding a row and editing a c…
wrt95 Feb 27, 2025
0539eea
test: tests for OrgTextsService (#14834)
Konrad-Simso Mar 3, 2025
c3733ea
test: tests for AltinnOrgGitRepository, text resources and code lists…
Konrad-Simso Mar 3, 2025
af73bd9
Test/14609/playwright test for delete row in codelist (#14829)
wrt95 Mar 3, 2025
ab4ea8c
refactor: move HeaderMenuItemKey to enum folder (#14860)
wrt95 Mar 4, 2025
c3a1e12
refactor: move SelectedContextType enum to enums folder (#14861)
wrt95 Mar 4, 2025
0838f1c
refactor: Move subroute enum to enum folder (#14862)
wrt95 Mar 4, 2025
fe7ba74
Merge remote-tracking branch 'origin/main' into org-library-mvp
TomasEng Mar 4, 2025
00a5adc
Move stream into function so it can be closed after usage.
Konrad-Simso Mar 4, 2025
d190d6f
refactor: move dashboardHeaderMenuItems (#14865)
wrt95 Mar 4, 2025
63b96f3
Merge branch 'main' into org-library-mvp
TomasEng Mar 4, 2025
b186c40
Merge branch 'main' into org-library-mvp
TomasEng Mar 5, 2025
e79756b
Fix wrong unit test
TomasEng Mar 5, 2025
b9e8160
useLocation instead of location
TomasEng Mar 5, 2025
30c5653
Mock shouldDisplayFeature
TomasEng Mar 5, 2025
915033c
Remove unnecessary variables as it already exists in AltinnAppGitRepo…
Konrad-Simso Mar 5, 2025
e1f620e
Update tests
Konrad-Simso Mar 5, 2025
c8c356f
Merge branch 'main' into org-library-mvp
TomasEng Mar 6, 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
156 changes: 156 additions & 0 deletions backend/src/Designer/Controllers/Organisation/OrgCodeListController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
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 Altinn.Studio.Designer.Services.Interfaces.Organisation;
using LibGit2Sharp;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Altinn.Studio.Designer.Controllers.Organisation;

/// <summary>
/// Controller containing actions related to code lists on organisation level.
/// </summary>
[ApiController]
[Authorize]
[Route("designer/api/{org}/code-lists")]
public class OrgCodeListController : ControllerBase
{
private readonly IOrgCodeListService _orgCodeListService;
private readonly ISourceControl _sourceControl;

/// <summary>
/// Initializes a new instance of the <see cref="OrgCodeListController"/> class.
/// </summary>
/// <param name="orgCodeListService">The CodeList service for organisation level</param>
/// <param name="sourceControl">The source control service.</param>
public OrgCodeListController(IOrgCodeListService orgCodeListService, ISourceControl sourceControl)
{
_orgCodeListService = orgCodeListService;
_sourceControl = sourceControl;
}

/// <summary>
/// Fetches the contents of all the code lists belonging to the organisation.
/// </summary>
/// <param name="org">Unique identifier of the organisation.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>List of <see cref="OptionListData" /> objects with all code lists belonging to the organisation with data
/// set if code list is valid, or hasError set if code list is invalid.</returns>
[HttpGet]
public async Task<ActionResult<List<OptionListData>>> GetCodeLists(string org, CancellationToken cancellationToken = default)
{
try
{
await _sourceControl.VerifyCloneExists(org, $"{org}-content");
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

List<OptionListData> codeLists = await _orgCodeListService.GetCodeLists(org, developer, cancellationToken);

return Ok(codeLists);
}
catch (NotFoundException)
{
return NoContent();
}
}

/// <summary>
/// Creates or overwrites a code list.
/// </summary>
/// <param name="org">Unique identifier of the organisation.</param>
/// <param name="codeListId">Name of the code list.</param>
/// <param name="codeList">Contents of the code list.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
[HttpPost]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Route("{codeListId}")]
public async Task<ActionResult<List<OptionListData>>> CreateCodeList(string org, [FromRoute] string codeListId, [FromBody] List<Option> codeList, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

List<OptionListData> codeLists = await _orgCodeListService.CreateCodeList(org, developer, codeListId, codeList, cancellationToken);

return Ok(codeLists);
}

/// <summary>
/// Creates or overwrites a code list.
/// </summary>
/// <param name="org">Unique identifier of the organisation.</param>
/// <param name="codeListId">Name of the code list.</param>
/// <param name="codeList">Contents of the code list.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
[HttpPut]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Route("{codeListId}")]
public async Task<ActionResult<List<OptionListData>>> UpdateCodeList(string org, [FromRoute] string codeListId, [FromBody] List<Option> codeList, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

List<OptionListData> codeLists = await _orgCodeListService.UpdateCodeList(org, developer, codeListId, codeList, cancellationToken);

return Ok(codeLists);
}

/// <summary>
/// Create new code list.
/// </summary>
/// <param name="org">Unique identifier of the organisation.</param>
/// <param name="file">File being uploaded.</param>
/// <param name="cancellationToken"><see cref="CancellationToken"/> that observes if operation is cancelled.</param>
[HttpPost]
[Route("upload")]
public async Task<ActionResult<List<OptionListData>>> UploadCodeList(string org, [FromForm] IFormFile file, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

try
{
List<OptionListData> codeLists = await _orgCodeListService.UploadCodeList(org, developer, file, cancellationToken);
return Ok(codeLists);
}
catch (JsonException e)
{
return BadRequest(e.Message);
}
}

/// <summary>
/// Deletes a code list.
/// </summary>
/// <param name="org">Unique identifier of the organisation.</param>
/// <param name="codeListId">Name of the code list.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
[HttpDelete]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Route("{codeListId}")]
public async Task<ActionResult<List<OptionListData>>> DeleteCodeList(string org, [FromRoute] string codeListId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

bool codeListExists = await _orgCodeListService.CodeListExists(org, developer, codeListId, cancellationToken);
if (!codeListExists)
{
return NotFound($"The code list file {codeListId}.json does not exist.");
}

List<OptionListData> codeLists = await _orgCodeListService.DeleteCodeList(org, developer, codeListId, cancellationToken);
return Ok(codeLists);
}
}
117 changes: 117 additions & 0 deletions backend/src/Designer/Controllers/Organisation/OrgTextController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces.Organisation;
using LibGit2Sharp;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Altinn.Studio.Designer.Controllers.Organisation;

/// <summary>
/// Controller for text resources on organisation level
/// </summary>
[ApiController]
[Authorize]
[Route("designer/api/{org}/text")]
public class OrgTextController : ControllerBase
{
private readonly IOrgTextsService _orgTextsService;

/// <summary>
/// Initializes a new instance of the <see cref="OrgTextController"/> class.
/// </summary>
/// <param name="orgTextsService">The texts service.</param>
public OrgTextController(IOrgTextsService orgTextsService)
{
_orgTextsService = orgTextsService;
}

/// <summary>
/// Returns a JSON resource file for the given language code
/// </summary>
/// <param name="org">Unique identifier of the organisation.</param>
/// <param name="languageCode">The resource language id (for example <code>nb, en</code>)</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The JSON config</returns>
[HttpGet]
[Route("language/{languageCode}")]
public async Task<ActionResult<TextResource>> GetResources(string org, string languageCode, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
try
{
TextResource textResource = await _orgTextsService.GetText(org, developer, languageCode, cancellationToken);
return Ok(textResource);
}
catch (NotFoundException)
{
return NotFound($"Text resource, resource.{languageCode}.json, could not be found.");
}

}

/// <summary>
/// Save a resource file
/// </summary>
/// <param name="jsonData">The JSON Data</param>
/// <param name="languageCode">The resource language id (for example <code>nb, en</code> )</param>
/// <param name="org">Unique identifier of the organisation.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The updated resource file</returns>
[HttpPost]
[Route("language/{languageCode}")]
public async Task<ActionResult<TextResource>> CreateResource([FromBody] TextResource jsonData, string languageCode, string org, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

try
{
await _orgTextsService.SaveText(org, developer, jsonData, languageCode, cancellationToken);
TextResource textResource = await _orgTextsService.GetText(org, developer, languageCode, cancellationToken);
return Ok(textResource);
}
catch (ArgumentException e)
{
return BadRequest(e.Message);
}
}

/// <summary>
/// Method to update multiple texts for given keys and a given
/// language in the text resource files.
/// Non-existing keys will be added.
/// </summary>
/// <param name="org">Unique identifier of the organisation.</param>
/// <param name="keysTexts">List of Key/Value pairs that should be updated or added if not present.</param>
/// <param name="languageCode">The languageCode for the text resource file that is being edited.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The updated resource file</returns>
[HttpPut]
[Route("language/{languageCode}")]
public async Task<ActionResult<TextResource>> UpdateResource(string org, [FromBody] Dictionary<string, string> keysTexts, string languageCode, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

try
{
await _orgTextsService.UpdateTextsForKeys(org, developer, keysTexts, languageCode, cancellationToken);
TextResource textResource = await _orgTextsService.GetText(org, developer, languageCode, cancellationToken);
return Ok(textResource);
}
catch (ArgumentException exception)
{
return BadRequest(exception.Message);
}
catch (NotFoundException)
{
return BadRequest($"The text resource, resource.{languageCode}.json, could not be updated.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class InstancesController(IHttpContextAccessor httpContextAccessor,
IApplicationMetadataService applicationMetadataService
) : Controller
{
private const string OptionsFolderPath = "App/options/";

// <summary>
// Redirect requests from older versions of Studio to old controller
// </summary>
Expand Down Expand Up @@ -188,7 +190,7 @@ CancellationToken cancellationToken
// TODO: Need code to get dynamic options list based on language and source?
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
AltinnAppGitRepository altinnAppGitRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
string options = await altinnAppGitRepository.GetOptionsList(optionListId, cancellationToken);
string options = await altinnAppGitRepository.GetOptionsList(optionListId, OptionsFolderPath, cancellationToken);
return Ok(options);
}
catch (NotFoundException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class OldInstancesController(IHttpContextAccessor httpContextAccessor,
IAltinnGitRepositoryFactory altinnGitRepositoryFactory
) : Controller
{
private const string OptionsFolderPath = "App/options/";

/// <summary>
/// Get instance data
/// </summary>
Expand Down Expand Up @@ -183,7 +185,7 @@ CancellationToken cancellationToken
// TODO: Need code to get dynamic options list based on language and source?
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
AltinnAppGitRepository altinnAppGitRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
string options = await altinnAppGitRepository.GetOptionsList(optionListId, cancellationToken);
string options = await altinnAppGitRepository.GetOptionsList(optionListId, OptionsFolderPath, cancellationToken);
return Ok(options);
}
catch (NotFoundException)
Expand Down
3 changes: 2 additions & 1 deletion backend/src/Designer/Controllers/PreviewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class PreviewController(IHttpContextAccessor httpContextAccessor,

// This value will be overridden to act as the task number for apps that use layout sets
private const int PartyId = 51001;
private const string OptionsFolderPath = "App/options/";

/// <summary>
/// Default action for the preview.
Expand Down Expand Up @@ -627,7 +628,7 @@ public async Task<ActionResult<string>> GetOptions(string org, string app, strin
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
string options = await altinnAppGitRepository.GetOptionsList(optionListId, cancellationToken);
string options = await altinnAppGitRepository.GetOptionsList(optionListId, OptionsFolderPath, cancellationToken);
return Ok(options);
}
catch (NotFoundException)
Expand Down
10 changes: 10 additions & 0 deletions backend/src/Designer/Factories/AltinnGitRepositoryFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,15 @@ public string GetRepositoryPath(string org, string repository, string developer)
string[] paths = { _repositoriesRootDirectory, developer.AsFileName(), org.AsFileName(), repository.AsFileName() };
return Path.Combine(paths);
}

/// <summary>
/// Creates an instance of <see cref="AltinnOrgGitRepository"/>
/// </summary>
/// <returns><see cref="AltinnOrgGitRepository"/></returns>
public AltinnOrgGitRepository GetAltinnOrgGitRepository(string org, string repository, string developer)
{
var repositoryDirectory = GetRepositoryPath(org, repository, developer);
return new AltinnOrgGitRepository(org, repository, developer, _repositoriesRootDirectory, repositoryDirectory);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public class AltinnAppGitRepository : AltinnGitRepository
};

/// <summary>
/// Initializes a new instance of the <see cref="AltinnGitRepository"/> class.
/// Initializes a new instance of the <see cref="AltinnAppGitRepository"/> class.
/// </summary>
/// <param name="org">Organization owning the repository identified by it's short name.</param>
/// <param name="repository">Repository name to search for schema files.</param>
Expand Down Expand Up @@ -606,7 +606,7 @@ public List<RefToOptionListSpecifier> FindOptionListReferencesInLayout(
string layoutName
)
{
var optionListIds = GetOptionsListIds();
var optionListIds = GetOptionsListIds(OptionsFolderPath);
var layoutArray = layout["data"]?["layout"] as JsonArray;
if (layoutArray == null)
{
Expand Down Expand Up @@ -995,9 +995,9 @@ public async Task<string> GetAppFrontendCshtml(
/// Gets a list of file names from the Options folder representing the available options lists.
/// </summary>
/// <returns>A list of option list names.</returns>
public string[] GetOptionsListIds()
public string[] GetOptionsListIds(string optionsFolderPath)
{
string optionsFolder = Path.Combine(OptionsFolderPath);
string optionsFolder = Path.Combine(optionsFolderPath);
if (!DirectoryExistsByRelativePath(optionsFolder))
{
return [];
Expand All @@ -1012,16 +1012,18 @@ public string[] GetOptionsListIds()
/// Gets a specific options list with the provided id.
/// </summary>
/// <param name="optionsListId">The name of the options list to fetch.</param>
/// <param name="optionsFolderPath"></param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that observes if operation is cancelled.</param>
/// <returns>The options list as a string.</returns>
public async Task<string> GetOptionsList(
string optionsListId,
string optionsFolderPath,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();

string optionsFilePath = Path.Combine(OptionsFolderPath, $"{optionsListId}.json");
string optionsFilePath = Path.Combine(optionsFolderPath, $"{optionsListId}.json");
if (!FileExistsByRelativePath(optionsFilePath))
{
throw new NotFoundException($"Options file {optionsListId}.json was not found.");
Expand Down
Loading