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: Add subform layoutset #13632

Merged
merged 33 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
656df7e
add elements to create sub form
lassopicasso Sep 19, 2024
78b6f45
validation in place
lassopicasso Sep 20, 2024
1c32e11
Merge branch 'main' into add-subform-layoutset
lassopicasso Sep 20, 2024
ce6b559
prepare to add new subform to backend
lassopicasso Sep 20, 2024
7c51e28
Merge branch 'main' into add-subform-layoutset
lassopicasso Sep 23, 2024
6eb1047
adjust types etc
lassopicasso Sep 23, 2024
1f5103a
set conditions in backend that tasks can be null
lassopicasso Sep 23, 2024
00f83e8
small f in subform
lassopicasso Sep 23, 2024
30c6dad
move file
lassopicasso Sep 24, 2024
fb4d1b8
rename file
lassopicasso Sep 24, 2024
4ca11fc
delete function
lassopicasso Sep 24, 2024
cf55123
subFormWrapper + combobox (description)
lassopicasso Sep 24, 2024
75df916
update and create tests for subForm rendring
lassopicasso Sep 24, 2024
7b5fb77
Add tests to createSubFormWrapper
lassopicasso Sep 24, 2024
f694cdb
reuse StudioDeleteButton
lassopicasso Sep 25, 2024
613dcf8
create tests for deleteSubFormWrapper
lassopicasso Sep 25, 2024
77f44f0
fix backend format
lassopicasso Sep 25, 2024
77340f4
fix so that user can also type in combobox
lassopicasso Sep 25, 2024
eac7b13
small fix to handle both new and existing layoutset
lassopicasso Sep 25, 2024
4c5da8a
fix backend test - ignore type (tasks or type) if not set in frontend
lassopicasso Sep 25, 2024
69ffebc
some cleanup
lassopicasso Sep 25, 2024
1e172f9
Merge branch 'main' into add-subform-layoutset
lassopicasso Sep 25, 2024
c3eb0d6
PR feedback
framitdavid Sep 27, 2024
7f88c15
added a why-comment
framitdavid Sep 27, 2024
6e5972a
avoid duplicate confirm
framitdavid Sep 27, 2024
6a6a130
Merge branch 'main' into add-subform-layoutset
framitdavid Sep 27, 2024
0e6ef68
PR feedbacks
framitdavid Sep 27, 2024
f88bb81
Merge branch 'add-subform-layoutset' of https://github.com/Altinn/alt…
framitdavid Sep 27, 2024
b98ed6b
PR feedback
framitdavid Sep 27, 2024
80c7235
PR feedback
framitdavid Sep 27, 2024
955db61
Merge branch 'main' into add-subform-layoutset
framitdavid Sep 27, 2024
2bb9a21
Merge branch 'main' into add-subform-layoutset
framitdavid Sep 30, 2024
b864a3f
PR feedback
framitdavid Sep 30, 2024
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
4 changes: 2 additions & 2 deletions backend/src/Designer/Controllers/PreviewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ private static ApplicationMetadata SetMockDataTypeIfMissing(ApplicationMetadata
LayoutSets layoutSetsWithMockedDataTypesIfMissing = AddDataTypesToReturnedLayoutSetsIfMissing(layoutSets);
layoutSetsWithMockedDataTypesIfMissing.Sets.ForEach(set =>
{
if (set.Tasks[0] == Constants.General.CustomReceiptId)
if (set.Tasks?[0] == Constants.General.CustomReceiptId)
{
return;
}
Expand All @@ -1043,7 +1043,7 @@ private static ApplicationMetadata SetMockDataTypeIfMissing(ApplicationMetadata
{
ClassRef = $"Altinn.App.Models.model.{set.DataType}"
},
TaskId = set.Tasks[0]
TaskId = set.Tasks?[0]
});
}
});
Expand Down
5 changes: 5 additions & 0 deletions backend/src/Designer/Models/LayoutSets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@
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 dotnet build and test (windows-latest)

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

[JsonPropertyName("dataType")]
[CanBeNull] public string DataType { get; set; }

[JsonPropertyName("tasks")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<string> Tasks { get; set; }

[JsonPropertyName("type")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Type { get; set; }

[JsonExtensionData]
public IDictionary<string, object?> UnknownProperties { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,9 @@ public async Task<LayoutSets> AddLayoutSet(AltinnRepoEditingContext altinnRepoEd
{
throw new NonUniqueLayoutSetIdException($"Layout set name, {newLayoutSet.Id}, already exists.");
}
if (layoutSets.Sets.Exists(set => set.Tasks[0] == newLayoutSet.Tasks[0]))
if (newLayoutSet.Tasks != null && layoutSets.Sets.Exists(set => set.Tasks?[0] == newLayoutSet.Tasks?[0]))
{
throw new NonUniqueTaskForLayoutSetException($"Layout set with task, {newLayoutSet.Tasks[0]}, already exists.");
throw new NonUniqueTaskForLayoutSetException($"Layout set with task, {newLayoutSet.Tasks?[0]}, already exists.");
standeren marked this conversation as resolved.
Show resolved Hide resolved
}

return await AddNewLayoutSet(altinnAppGitRepository, layoutSets, newLayoutSet, layoutIsInitialForPaymentTask);
Expand Down
12 changes: 7 additions & 5 deletions backend/src/Designer/Services/Implementation/PreviewService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,16 @@ public async Task<List<string>> GetTasksForAllLayoutSets(string org, string app,
List<string> tasks = [];
if (layoutSets?.Sets is { Count: > 0 })
{
foreach (LayoutSetConfig layoutSet in layoutSets.Sets.Where(ls => !tasks.Contains(ls.Tasks[0])))
foreach (LayoutSetConfig layoutSet in layoutSets.Sets.Where(ls => !tasks.Contains(ls.Tasks?[0])))
{
if (layoutSet.Tasks[0] == Constants.General.CustomReceiptId)
if (layoutSet.Tasks?[0] == Constants.General.CustomReceiptId)
{
continue;
}

tasks.Add(layoutSet.Tasks[0]);
if (layoutSet.Tasks != null)
{
tasks.Add(layoutSet.Tasks[0]);
}
}
}
return tasks;
Expand All @@ -121,7 +123,7 @@ public async Task<string> GetTaskForLayoutSetName(string org, string app, string
if (!string.IsNullOrEmpty(layoutSetName))
{
LayoutSets layoutSets = await altinnAppGitRepository.GetLayoutSetsFile(cancellationToken);
task = layoutSets?.Sets?.Find(element => element.Id == layoutSetName).Tasks[0];
task = layoutSets?.Sets?.Find(element => element.Id == layoutSetName).Tasks?[0];
}
return task;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public async Task<string> GetTaskTypeFromProcessDefinition(AltinnRepoEditingCont
XmlSerializer serializer = new XmlSerializer(typeof(Definitions));
Definitions? definitions = (Definitions?)serializer.Deserialize(processDefinitionStream);
LayoutSetConfig layoutSet = await _appDevelopmentService.GetLayoutSetConfig(altinnRepoEditingContext, layoutSetId);
string taskId = layoutSet.Tasks.First();
string taskId = layoutSet.Tasks?.First();
ProcessTask? task = definitions?.Process.Tasks.FirstOrDefault(task => task.Id == taskId);
return task?.ExtensionElements?.TaskExtension?.TaskType ?? string.Empty;
}
Expand Down
20 changes: 10 additions & 10 deletions frontend/app-development/hooks/mutations/useAddLayoutSetMutation.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { type UseMutateFunction, useMutation, useQueryClient } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
import type {
LayoutSetConfig,
LayoutSets,
BpmnTaskType,
} from 'app-shared/types/api/LayoutSetsResponse';
import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
import { useLocalStorage } from '@studio/components/src/hooks/useLocalStorage';
import type {
AddLayoutSetResponse,
LayoutSetsResponse,
} from 'app-shared/types/api/AddLayoutSetResponse';
import type { LayoutSetConfig, LayoutSetPayload } from 'app-shared/types/api/LayoutSetPayload';

export type AddLayoutSetMutationPayload = {
layoutSetIdToUpdate: string;
taskType?: BpmnTaskType;
layoutSetConfig: LayoutSetConfig;
};
} & LayoutSetPayload;

export type AddLayoutSetMutation = UseMutateFunction<
AddLayoutSetResponse,
{
layoutSets: AddLayoutSetResponse;
standeren marked this conversation as resolved.
Show resolved Hide resolved
layoutSetConfig: LayoutSetConfig;
},
Error,
AddLayoutSetMutationPayload
AddLayoutSetMutationPayload,
unknown
standeren marked this conversation as resolved.
Show resolved Hide resolved
>;

const isLayoutSets = (obj: LayoutSetsResponse): obj is LayoutSets => {
Expand Down
8 changes: 7 additions & 1 deletion frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@
"process_editor.configuration_panel_header_help_text_signing": "Du bruker oppgaven Signering (signing) når du vil at sluttbrukerne skal bekrefte med signatur.",
"process_editor.configuration_panel_header_help_text_title": "Informasjon om valgt oppgave",
"process_editor.configuration_panel_id_label": "ID:",
"process_editor.configuration_panel_layout_set_id_not_unique": "Navnet på sidegruppen må være unikt",
"process_editor.configuration_panel_layout_set_id_not_unique": "Navnet må være unikt",
"process_editor.configuration_panel_layout_set_name": "Navn på sidegruppe: ",
"process_editor.configuration_panel_layout_set_name_label": "Navn på sidegruppe",
"process_editor.configuration_panel_missing_task": "Oppgave",
Expand Down Expand Up @@ -1359,6 +1359,11 @@
"ux_editor.config.warning_duplicates.solution_studio_import": "Gå tilbake til Altinn Studio og velg 'Hent endringer' for å laste inn endringene du har gjort i koden.",
"ux_editor.container_empty": "Tomt, dra noe inn her...",
"ux_editor.container_not_editable_info": "Noen egenskaper for denne komponenten er ikke redigerbare for øyeblikket. Du kan legge til underkomponenter i kolonnen til venstre.",
"ux_editor.create.sub_form": "Nytt underskjema",
"ux_editor.create.sub_form.confirm_button": "Opprett underskjema",
"ux_editor.create.sub_form.label": "Navn på underskjema",
"ux_editor.delete.sub_form": "Slett underskjema",
"ux_editor.delete.sub_form.confirm": "Er du sikker på at du vil slette dette underskjemaet?",
standeren marked this conversation as resolved.
Show resolved Hide resolved
"ux_editor.edit_component.loading_schema": "Laster inn skjema",
"ux_editor.edit_component.show_beta_func": "Vis ny konfigurasjon (BETA)",
"ux_editor.edit_component.unknown_component": "Komponenten {{componentName}} gjenkjennes ikke av Studio og kan derfor ikke konfigureres.",
Expand Down Expand Up @@ -1606,6 +1611,7 @@
"ux_editor.search_text_resources_label": "Velg eksisterende tekst basert på ID",
"ux_editor.search_text_resources_none": "Ingen",
"ux_editor.show_icon": "Skal ikonet vises?",
"ux_editor.sub_form": "Underskjema",
"ux_editor.subdomains_label": "Subdomener (kommaseparert)",
"ux_editor.subdomains_placeholder": "domene1, domene2, domene3",
"ux_editor.success": "Vellykket",
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/shared/src/api/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import type { ApplicationAttachmentMetadata } from 'app-shared/types/Application
import type { CreateDeploymentPayload } from 'app-shared/types/api/CreateDeploymentPayload';
import type { CreateReleasePayload } from 'app-shared/types/api/CreateReleasePayload';
import type { CreateRepoCommitPayload } from 'app-shared/types/api/CreateRepoCommitPayload';
import type { LayoutSetPayload } from 'app-shared/types/api/LayoutSetsResponse';
import type { LayoutSetPayload } from 'app-shared/types/api/LayoutSetPayload';
import type { ILayoutSettings, ITextResourcesObjectFormat } from 'app-shared/types/global';
import type { RuleConfig } from 'app-shared/types/RuleConfig';
import type { UpdateTextIdPayload } from 'app-shared/types/api/UpdateTextIdPayload';
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/shared/src/types/BpmnTaskType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type BpmnTaskType = 'data' | 'confirmation' | 'feedback' | 'signing' | 'payment';
19 changes: 19 additions & 0 deletions frontend/packages/shared/src/types/api/LayoutSetPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { BpmnTaskType } from '../BpmnTaskType';

export interface LayoutSetPayload {
taskType?: BpmnTaskType;
layoutSetConfig: LayoutSetConfig;
}

type SubFormConfig = {
type: 'subform';
};

type RegularLayoutSetConfig = {
tasks: string[];
};

export type LayoutSetConfig = {
id: string;
dataType?: string;
} & (SubFormConfig | RegularLayoutSetConfig);
18 changes: 7 additions & 11 deletions frontend/packages/shared/src/types/api/LayoutSetsResponse.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
export type BpmnTaskType = 'data' | 'confirmation' | 'feedback' | 'signing' | 'payment';

export interface LayoutSets {
export type LayoutSets = {
sets: LayoutSetConfig[];
}
};

export interface LayoutSetConfig {
export type LayoutSet = {
id: string;
dataType?: string;
tasks: string[];
}
tasks?: string[];
type?: string;
};

export interface LayoutSetPayload {
taskType: BpmnTaskType;
framitdavid marked this conversation as resolved.
Show resolved Hide resolved
layoutSetConfig: LayoutSetConfig;
}
export type LayoutSetConfig = LayoutSet;
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
.layoutSetsDropDown {
min-width: 20vw;
}

.root {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
standeren marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LayoutSetsContainer } from './LayoutSetsContainer';
import { queryClientMock } from 'app-shared/mocks/queryClientMock';
Expand All @@ -12,35 +12,73 @@ import {
import { QueryKey } from 'app-shared/types/QueryKey';
import { appContextMock } from '../../testing/appContextMock';
import { app, org } from '@studio/testing/testids';
import {
addFeatureFlagToLocalStorage,
removeFeatureFlagFromLocalStorage,
} from 'app-shared/utils/featureToggleUtils';
import { textMock } from '@studio/testing/mocks/i18nMock';

// Test data
const layoutSetName1 = layoutSet1NameMock;
const layoutSetName2 = layoutSet2NameMock;

describe('LayoutSetsContainer', () => {
it('renders component', async () => {
const user = userEvent.setup();
render();

const combobox = screen.getByRole('combobox');
await user.click(combobox);

standeren marked this conversation as resolved.
Show resolved Hide resolved
expect(await screen.findByRole('option', { name: layoutSetName1 })).toBeInTheDocument();
expect(await screen.findByRole('option', { name: layoutSetName2 })).toBeInTheDocument();
});

it('NativeSelect should be rendered', async () => {
render();
expect(screen.getByRole('combobox')).toBeInTheDocument();
});

it('Should update selected layout set when set is clicked in native select', async () => {
it('Should update with selected layout', async () => {
render();
const user = userEvent.setup();
await user.selectOptions(screen.getByRole('combobox'), layoutSetName2);
expect(appContextMock.setSelectedFormLayoutSetName).toHaveBeenCalledTimes(1);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
await user.click(screen.getByRole('option', { name: layoutSetName2 }));

await waitFor(() =>
expect(appContextMock.setSelectedFormLayoutSetName).toHaveBeenCalledTimes(1),
);
expect(appContextMock.refetchLayouts).toHaveBeenCalledTimes(1);
expect(appContextMock.refetchLayouts).toHaveBeenCalledWith('test-layout-set-2');
expect(appContextMock.refetchLayoutSettings).toHaveBeenCalledTimes(1);
expect(appContextMock.refetchLayoutSettings).toHaveBeenCalledWith('test-layout-set-2');
expect(appContextMock.onLayoutSetNameChange).toHaveBeenCalledWith('test-layout-set-2');
});

it('should render add and delete subform buttons when feature is enabled', async () => {
standeren marked this conversation as resolved.
Show resolved Hide resolved
addFeatureFlagToLocalStorage('subForm');

render();
const createSubFormButton = screen.getByRole('button', {
name: textMock('ux_editor.create.sub_form'),
});
expect(createSubFormButton).toBeInTheDocument();

const deleteSubFormButton = screen.getByRole('button', {
name: textMock('ux_editor.delete.sub_form'),
});
expect(deleteSubFormButton).toBeInTheDocument();
removeFeatureFlagFromLocalStorage('subForm');
standeren marked this conversation as resolved.
Show resolved Hide resolved
});

it('should not render add and delete subform buttons when feature is disabled', async () => {
standeren marked this conversation as resolved.
Show resolved Hide resolved
render();
const createSubFormButton = screen.queryByRole('button', {
name: textMock('ux_editor.create.sub_form'),
});
expect(createSubFormButton).not.toBeInTheDocument();

const deleteSubFormButton = screen.queryByRole('button', {
name: textMock('ux_editor.delete.sub_form'),
});
expect(deleteSubFormButton).not.toBeInTheDocument();
});
});

const render = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React, { useEffect } from 'react';
import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery';
import { NativeSelect } from '@digdir/designsystemet-react';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { useText, useAppContext } from '../../hooks';
import classes from './LayoutSetsContainer.module.css';
import { ExportForm } from './ExportForm';
import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';
import { SubFormWrapper } from './SubForm/SubFormWrapper';
import { StudioCombobox } from '@studio/components';

export function LayoutSetsContainer() {
const { org, app } = useStudioEnvironmentParams();
const layoutSetsQuery = useLayoutSetsQuery(org, app);
const layoutSetNames = layoutSetsQuery.data?.sets?.map((set) => set.id);
const { data: layoutSetsResponse } = useLayoutSetsQuery(org, app);
const layoutSets = layoutSetsResponse?.sets;
const t = useText();
const {
selectedFormLayoutSetName,
Expand All @@ -25,37 +26,45 @@ export function LayoutSetsContainer() {
onLayoutSetNameChange(selectedFormLayoutSetName);
}, [onLayoutSetNameChange, selectedFormLayoutSetName]);

const onLayoutSetClick = async (set: string) => {
if (selectedFormLayoutSetName !== set) {
await refetchLayouts(set);
await refetchLayoutSettings(set);
if (!layoutSets) return null;

setSelectedFormLayoutSetName(set);
const handleLayoutSetChange = async (layoutSetName: string) => {
if (selectedFormLayoutSetName !== layoutSetName && layoutSetName) {
await refetchLayouts(layoutSetName);
await refetchLayoutSettings(layoutSetName);

setSelectedFormLayoutSetName(layoutSetName);
setSelectedFormLayoutName(undefined);
onLayoutSetNameChange(set);
onLayoutSetNameChange(layoutSetName);
}
};

if (!layoutSetNames) return null;

return (
<div className={classes.root}>
<NativeSelect
aria-label={t('left_menu.layout_dropdown_menu_label')}
onChange={(event) => onLayoutSetClick(event.target.value)}
value={selectedFormLayoutSetName}
className={classes.layoutSetsDropDown}
size='small'
<StudioCombobox
label={t('left_menu.layout_dropdown_menu_label')}
hideLabel
value={[selectedFormLayoutSetName]}
onValueChange={([value]) => handleLayoutSetChange(value)}
>
{layoutSetNames.map((set: string) => {
return (
<option key={set} value={set}>
{set}
</option>
);
})}
</NativeSelect>
{layoutSets.map((layoutSet) => (
<StudioCombobox.Option
value={layoutSet.id}
key={layoutSet.id}
description={layoutSet?.type === 'subform' && t('ux_editor.sub_form')}
standeren marked this conversation as resolved.
Show resolved Hide resolved
>
{layoutSet.id}
</StudioCombobox.Option>
))}
</StudioCombobox>
{shouldDisplayFeature('exportForm') && <ExportForm />}
{shouldDisplayFeature('subForm') && (
<SubFormWrapper
layoutSets={layoutSetsResponse}
onSubFormCreated={handleLayoutSetChange}
selectedLayoutSet={selectedFormLayoutSetName}
/>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.confirmCreateButton {
margin-top: var(--fds-spacing-2);
}
Loading
Loading