Skip to content

Commit

Permalink
Merge branch 'main' into chore/update-error-handling-in-useValidateCo…
Browse files Browse the repository at this point in the history
…mponent
  • Loading branch information
ErlingHauan authored Jan 6, 2025
2 parents ae555ef + a712b7d commit 70b657a
Show file tree
Hide file tree
Showing 54 changed files with 569 additions and 376 deletions.
31 changes: 26 additions & 5 deletions backend/src/Designer/Controllers/OptionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Exceptions.Options;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
Expand Down Expand Up @@ -57,20 +58,40 @@ public ActionResult<string[]> GetOptionsListIds(string org, string repo)
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="repo">Application identifier which is unique within an organisation.</param>
/// <returns>Dictionary of all option lists belonging to the app</returns>
/// <returns>List of <see cref="OptionListData" /> objects with all option lists belonging to the app with data
/// set if option list is valid, or hasError set if option list is invalid.</returns>
[HttpGet]
[Route("option-lists")]
public async Task<ActionResult<Dictionary<string, List<Option>>>> GetOptionLists(string org, string repo)
public async Task<ActionResult<List<OptionListData>>> GetOptionLists(string org, string repo)
{
try
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
string[] optionListIds = _optionsService.GetOptionsListIds(org, repo, developer);
Dictionary<string, List<Option>> optionLists = [];
List<OptionListData> optionLists = [];
foreach (string optionListId in optionListIds)
{
List<Option> optionList = await _optionsService.GetOptionsList(org, repo, developer, optionListId);
optionLists.Add(optionListId, optionList);
try
{
List<Option> optionList = await _optionsService.GetOptionsList(org, repo, developer, optionListId);
OptionListData optionListData = new()
{
Title = optionListId,
Data = optionList,
HasError = false
};
optionLists.Add(optionListData);
}
catch (InvalidOptionsFormatException)
{
OptionListData optionListData = new()
{
Title = optionListId,
Data = null,
HasError = true
};
optionLists.Add(optionListData);
}
}
return Ok(optionLists);
}
Expand Down
10 changes: 4 additions & 6 deletions backend/src/Designer/Controllers/ProcessModelingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,19 @@ public async Task<IActionResult> UpsertProcessDefinitionAndNotify(string org, st
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
: null;

Stream stream = content.OpenReadStream();
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer);

await using Stream stream = content.OpenReadStream();
try
{
await Guard.AssertValidXmlStreamAndRewindAsync(stream);
await _processModelingService.SaveProcessDefinitionAsync(editingContext, stream, cancellationToken);
}
catch (ArgumentException)
{
return BadRequest("BPMN file is not valid XML");
}

string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer);
await _processModelingService.SaveProcessDefinitionAsync(editingContext, stream, cancellationToken);

if (metadataObject?.TaskIdChange is not null)
{
await _mediator.Publish(new ProcessTaskIdChangedEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -904,9 +904,11 @@ public Stream GetProcessDefinitionFile()

public Definitions GetDefinitions()
{
Stream processDefinitionStream = GetProcessDefinitionFile();
using Stream processDefinitionStream = GetProcessDefinitionFile();
XmlSerializer serializer = new(typeof(Definitions));
return (Definitions)serializer.Deserialize(processDefinitionStream);
Definitions definitions = (Definitions)serializer.Deserialize(processDefinitionStream);

return definitions;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class EndpointNameSyncEvaluator : IRequestSyncEvaluator
nameof(ProcessModelingController.AddDataTypeToApplicationMetadata),
nameof(ProcessModelingController.DeleteDataTypeFromApplicationMetadata),
nameof(ProcessModelingController.UpsertProcessDefinitionAndNotify),
nameof(ProcessModelingController.ProcessDataTypesChangedNotify),
nameof(ProcessModelingController.SaveProcessDefinitionFromTemplate)
)
},
Expand Down
15 changes: 15 additions & 0 deletions backend/src/Designer/Models/Dto/OptionListData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace Altinn.Studio.Designer.Models.Dto;

public class OptionListData
{
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("data")]
[CanBeNull] public List<Option> Data { get; set; }
[JsonPropertyName("hasError")]
public bool? HasError { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Filters;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
using Designer.Tests.Controllers.ApiTests;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -56,6 +58,29 @@ public async Task GetOptionsListIds_Returns200OK_WithEmptyOptionsListIdArray_Whe
Assert.Empty(responseList);
}

[Fact]
public async Task GetOptionLists_Returns200OK_WithOptionListsData()
{
// Arrange
const string repo = "app-with-options";
string apiUrl = $"/designer/api/ttd/{repo}/options/option-lists";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, apiUrl);

// Act
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
string responseBody = await response.Content.ReadAsStringAsync();
List<OptionListData> responseList = JsonSerializer.Deserialize<List<OptionListData>>(responseBody);

// Assert
Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode);
responseList.Should().BeEquivalentTo(new List<OptionListData>
{
new () { Title = "options-with-null-fields", Data = null, HasError = true },
new () { Title = "other-options", HasError = false },
new () { Title = "test-options", HasError = false }
}, options => options.Excluding(x => x.Data));
}

[Fact]
public async Task GetSingleOptionsList_Returns200Ok_WithOptionsList()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ import { app, org } from '@studio/testing/testids';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import type { UserEvent } from '@testing-library/user-event';
import userEvent from '@testing-library/user-event';
import type { OptionsLists } from 'app-shared/types/api/OptionsLists';
import type { CodeList } from '@studio/components';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import type { OptionsListsResponse } from 'app-shared/types/api/OptionsLists';

const uploadCodeListButtonTextMock = 'Upload Code List';
const updateCodeListButtonTextMock = 'Update Code List';
const updateCodeListIdButtonTextMock = 'Update Code List Id';
const codeListNameMock = 'codeListNameMock';
const newCodeListNameMock = 'newCodeListNameMock';
const codeListMock: CodeList = [{ value: '', label: '' }];
const optionListsDataMock: OptionsListsResponse = [{ title: codeListNameMock, data: codeListMock }];
jest.mock(
'../../../libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage',
() => ({
Expand Down Expand Up @@ -46,10 +47,6 @@ jest.mock(
}),
);

const optionListsMock: OptionsLists = {
list1: [{ label: 'label', value: 'value' }],
};

describe('AppContentLibrary', () => {
afterEach(jest.clearAllMocks);

Expand All @@ -66,7 +63,7 @@ describe('AppContentLibrary', () => {
});

it('renders a spinner when waiting for option lists', () => {
renderAppContentLibrary({ optionLists: {} });
renderAppContentLibrary({ optionListsData: [] });
const spinner = screen.getByText(textMock('general.loading'));
expect(spinner).toBeInTheDocument();
});
Expand Down Expand Up @@ -123,7 +120,7 @@ describe('AppContentLibrary', () => {

it('calls onUpdateOptionListId when onUpdateCodeListId is triggered', async () => {
const user = userEvent.setup();
renderAppContentLibrary(optionListsMock);
renderAppContentLibrary();
await goToLibraryPage(user, 'code_lists');
const updateCodeListIdButton = screen.getByRole('button', {
name: updateCodeListIdButtonTextMock,
Expand All @@ -149,16 +146,16 @@ const goToLibraryPage = async (user: UserEvent, libraryPage: string) => {

type renderAppContentLibraryProps = {
queries?: Partial<ServicesContextProps>;
optionLists?: OptionsLists;
optionListsData?: OptionsListsResponse;
};

const renderAppContentLibrary = ({
queries = {},
optionLists = optionListsMock,
optionListsData = optionListsDataMock,
}: renderAppContentLibraryProps = {}) => {
const queryClientMock = createQueryClientMock();
if (Object.keys(optionLists).length) {
queryClientMock.setQueryData([QueryKey.OptionLists, org, app], optionLists);
if (optionListsData.length) {
queryClientMock.setQueryData([QueryKey.OptionLists, org, app], optionListsData);
}
renderWithProviders(queries, queryClientMock)(<AppContentLibrary />);
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { CodeListWithMetadata } from '@studio/content-library';
import { ResourceContentLibraryImpl } from '@studio/content-library';
import React from 'react';
import { useOptionListsQuery } from 'app-shared/hooks/queries/useOptionListsQuery';
import { useOptionListsQuery } from 'app-shared/hooks/queries';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { convertOptionListsToCodeLists } from './utils/convertOptionListsToCodeLists';
import { convertOptionsListsDataToCodeListsData } from './utils/convertOptionsListsDataToCodeListsData';
import { StudioPageSpinner } from '@studio/components';
import { useTranslation } from 'react-i18next';
import type { ApiError } from 'app-shared/types/api/ApiError';
Expand All @@ -19,21 +19,20 @@ import {
export function AppContentLibrary(): React.ReactElement {
const { org, app } = useStudioEnvironmentParams();
const { t } = useTranslation();
const {
data: optionLists,
isPending: optionListsPending,
isError: optionListsError,
} = useOptionListsQuery(org, app);
const { data: optionListsData, isPending: optionListsDataPending } = useOptionListsQuery(
org,
app,
);
const { mutate: uploadOptionList } = useAddOptionListMutation(org, app, {
hideDefaultError: (error: AxiosError<ApiError>) => isErrorUnknown(error),
});
const { mutate: updateOptionList } = useUpdateOptionListMutation(org, app);
const { mutate: updateOptionListId } = useUpdateOptionListIdMutation(org, app);

if (optionListsPending)
if (optionListsDataPending)
return <StudioPageSpinner spinnerTitle={t('general.loading')}></StudioPageSpinner>;

const codeLists = convertOptionListsToCodeLists(optionLists);
const codeListsData = convertOptionsListsDataToCodeListsData(optionListsData);

const handleUpdateCodeListId = (optionListId: string, newOptionListId: string) => {
updateOptionListId({ optionListId, newOptionListId });
Expand All @@ -60,11 +59,10 @@ export function AppContentLibrary(): React.ReactElement {
pages: {
codeList: {
props: {
codeLists: codeLists,
codeListsData,
onUpdateCodeListId: handleUpdateCodeListId,
onUpdateCodeList: handleUpdate,
onUploadCodeList: handleUpload,
fetchDataError: optionListsError,
},
},
images: {
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { CodeListData } from '@studio/content-library';
import { convertOptionsListsDataToCodeListsData } from './convertOptionsListsDataToCodeListsData';
import type { OptionsListsResponse } from 'app-shared/types/api/OptionsLists';

describe('convertOptionsListsDataToCodeListsData', () => {
it('converts option lists data to code lists data correctly', () => {
const optionListId: string = 'optionListId';
const optionListsData: OptionsListsResponse = [
{
title: optionListId,
data: [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
],
hasError: false,
},
];
const result: CodeListData[] = convertOptionsListsDataToCodeListsData(optionListsData);
expect(result).toEqual([
{
title: optionListId,
data: [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
],
hasError: false,
},
]);
});

it('sets hasError to true in result when optionListsResponse returns an option list with error', () => {
const optionListId: string = 'optionListId';
const optionListsData: OptionsListsResponse = [
{
title: optionListId,
data: null,
hasError: true,
},
];
const result: CodeListData[] = convertOptionsListsDataToCodeListsData(optionListsData);
expect(result).toEqual([{ title: optionListId, data: null, hasError: true }]);
});

it('returns a result with empty code list data array when the input option list data is empty', () => {
const optionListsData: OptionsListsResponse = [];
const result: CodeListData[] = convertOptionsListsDataToCodeListsData(optionListsData);
expect(result).toEqual([]);
});
});
Loading

0 comments on commit 70b657a

Please sign in to comment.