Skip to content

Commit 9ec0e3b

Browse files
feat: 13614 nextrecommendedaction in config column when adding a subform to select a subform from list (#13827)
Co-authored-by: Jonas Dyrlie <[email protected]>
1 parent 1f22a8a commit 9ec0e3b

File tree

10 files changed

+243
-117
lines changed

10 files changed

+243
-117
lines changed

frontend/language/src/nb.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1292,8 +1292,10 @@
12921292
"ux_editor.component_properties.style": "Stil",
12931293
"ux_editor.component_properties.subdomains": "Subdomener (kommaseparert)",
12941294
"ux_editor.component_properties.subform": "Sidegruppe for underskjema",
1295-
"ux_editor.component_properties.subform.choose_layout_set": "Velg sidegruppe...",
1296-
"ux_editor.component_properties.subform.choose_layout_set_label": "Velg sidegruppe å knytte til underskjema",
1295+
"ux_editor.component_properties.subform.choose_layout_set": "Velg et underskjema...",
1296+
"ux_editor.component_properties.subform.choose_layout_set_description": " Før du kan bruke komponenten Tabell for underskjema, må du velge hvilket underskjema du skal bruke den med. Deretter kan du velge hvilke egenskaper komponenten skal ha.",
1297+
"ux_editor.component_properties.subform.choose_layout_set_header": "Velg underskjemaet du vil bruke",
1298+
"ux_editor.component_properties.subform.choose_layout_set_label": "Velg et underskjema",
12971299
"ux_editor.component_properties.subform.go_to_layout_set": "Gå til utforming av underskjemaet",
12981300
"ux_editor.component_properties.subform.no_layout_sets_acting_as_subform": "Det finnes ingen sidegrupper i løsningen som kan brukes som et underskjema",
12991301
"ux_editor.component_properties.subform.selected_layout_set_label": "Underskjema",

frontend/libs/studio-components/src/components/StudioRecommendedNextAction/StudioRecommendedNextAction.tsx

+11-7
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import { StudioParagraph } from '../StudioParagraph';
66
import { Heading } from '@digdir/designsystemet-react';
77

88
export type StudioRecommendedNextActionProps = {
9-
onSave: React.FormEventHandler<HTMLFormElement>;
10-
saveButtonText: string;
11-
onSkip: React.MouseEventHandler<HTMLButtonElement>;
12-
skipButtonText: string;
9+
onSave?: React.FormEventHandler<HTMLFormElement>;
10+
saveButtonText?: string;
11+
onSkip?: React.MouseEventHandler<HTMLButtonElement>;
12+
skipButtonText?: string;
1313
title: string;
1414
description: string;
1515
hideSaveButton?: boolean;
16+
hideSkipButton?: boolean;
1617
children: React.ReactNode;
1718
};
1819

@@ -24,6 +25,7 @@ export const StudioRecommendedNextAction = ({
2425
title,
2526
description,
2627
hideSaveButton = false,
28+
hideSkipButton,
2729
children,
2830
}: StudioRecommendedNextActionProps): React.ReactElement => {
2931
const formName = useId();
@@ -44,9 +46,11 @@ export const StudioRecommendedNextAction = ({
4446
{saveButtonText}
4547
</StudioButton>
4648
)}
47-
<StudioButton onClick={onSkip} variant='tertiary'>
48-
{skipButtonText}
49-
</StudioButton>
49+
{!hideSkipButton && (
50+
<StudioButton onClick={onSkip} variant='tertiary'>
51+
{skipButtonText}
52+
</StudioButton>
53+
)}
5054
</div>
5155
</StudioCard.Content>
5256
</StudioCard>

frontend/packages/ux-editor/src/classes/SubFormUtils.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export class SubFormUtilsImpl implements SubFormUtils {
2121
}
2222

2323
private get getSubformLayoutSets(): Array<SubFormLayoutSet> {
24-
return this.layoutSets.filter((set) => set.type === 'subform') as Array<SubFormLayoutSet>;
24+
return (this.layoutSets || []).filter(
25+
(set) => set.type === 'subform',
26+
) as Array<SubFormLayoutSet>;
2527
}
2628
}

frontend/packages/ux-editor/src/components/Properties/Properties.test.tsx

+24
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,28 @@ describe('Properties', () => {
243243
screen.getByText(textMock('right_menu.rules_calculations_deprecated_info_title')),
244244
).toBeInTheDocument();
245245
});
246+
247+
it('renders properties when formItem is not a Subform component', () => {
248+
renderProperties({ formItem: componentMocks[ComponentType.Input] });
249+
expect(screen.getByText(textMock('right_menu.text'))).toBeInTheDocument();
250+
expect(screen.getByText(textMock('right_menu.data_model_bindings'))).toBeInTheDocument();
251+
expect(screen.getByText(textMock('right_menu.content'))).toBeInTheDocument();
252+
expect(screen.getByText(textMock('right_menu.dynamics'))).toBeInTheDocument();
253+
expect(screen.getByText(textMock('right_menu.calculations'))).toBeInTheDocument();
254+
});
255+
256+
it('render properties accordions for a subform component when it is linked to a subform layoutSet', () => {
257+
editFormComponentSpy.mockReturnValue(<input data-testid={editFormComponentTestId}></input>);
258+
renderProperties({
259+
formItem: { ...componentMocks[ComponentType.SubForm], layoutSet: layoutSetName },
260+
formItemId: componentMocks[ComponentType.SubForm].id,
261+
});
262+
expect(screen.getByText(textMock('right_menu.text'))).toBeInTheDocument();
263+
expect(screen.getByText(textMock('right_menu.data_model_bindings'))).toBeInTheDocument();
264+
expect(screen.getByText(textMock('right_menu.content'))).toBeInTheDocument();
265+
expect(screen.getByText(textMock('right_menu.dynamics'))).toBeInTheDocument();
266+
expect(screen.getByText(textMock('right_menu.calculations'))).toBeInTheDocument();
267+
});
246268
});
247269

248270
const getComponent = (
@@ -268,7 +290,9 @@ const renderProperties = (
268290
},
269291
) => {
270292
const queryClientMock = createQueryClientMock();
293+
271294
queryClientMock.setQueryData([QueryKey.FormLayouts, org, app, layoutSetName], layouts);
295+
queryClientMock.setQueryData([QueryKey.LayoutSets, org, app], layoutSet1NameMock);
272296

273297
return renderWithProviders(getComponent(formItemContextProps), {
274298
queryClient: queryClientMock,

frontend/packages/ux-editor/src/components/Properties/Properties.tsx

+70-64
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export const Properties = () => {
1616
const { formItemId, formItem, handleUpdate, debounceSave } = useFormItemContext();
1717
const [openList, setOpenList] = React.useState<string[]>([]);
1818

19+
if (!formItem) {
20+
return (
21+
<div className={classes.root} key={formItemId}>
22+
<PageConfigPanel />
23+
</div>
24+
);
25+
}
26+
1927
const toggleOpen = (id: string) => {
2028
if (openList.includes(id)) {
2129
setOpenList(openList.filter((item) => item !== id));
@@ -24,72 +32,70 @@ export const Properties = () => {
2432
}
2533
};
2634

35+
const isNotSubformOrHasLayoutSet = formItem.type !== 'SubForm' || !!formItem.layoutSet;
36+
2737
return (
2838
<div className={classes.root} key={formItemId}>
29-
{!formItem ? (
30-
<PageConfigPanel />
31-
) : (
32-
<>
33-
<PropertiesHeader
34-
formItem={formItem}
35-
handleComponentUpdate={async (updatedComponent) => {
36-
handleUpdate(updatedComponent);
37-
debounceSave(formItemId, updatedComponent);
38-
}}
39-
/>
40-
<Accordion color='subtle'>
41-
<Accordion.Item open={openList.includes('text')}>
42-
<Accordion.Header
43-
aria-label={t('right_menu.text_label')}
44-
onHeaderClick={() => toggleOpen('text')}
45-
>
46-
{t(formItem.type === 'Image' ? 'right_menu.text_and_image' : 'right_menu.text')}
47-
</Accordion.Header>
48-
<Accordion.Content className={classes.texts}>
49-
<Text />
50-
</Accordion.Content>
51-
</Accordion.Item>
52-
<Accordion.Item open={openList.includes('dataModel')}>
53-
<Accordion.Header onHeaderClick={() => toggleOpen('dataModel')}>
54-
{t('right_menu.data_model_bindings')}
55-
</Accordion.Header>
56-
<Accordion.Content className={classes.dataModelBindings}>
57-
<DataModelBindings />
58-
</Accordion.Content>
59-
</Accordion.Item>
60-
<Accordion.Item open={openList.includes('content')}>
61-
<Accordion.Header onHeaderClick={() => toggleOpen('content')}>
62-
{t('right_menu.content')}
63-
</Accordion.Header>
64-
<Accordion.Content>
65-
<EditFormComponent
66-
editFormId={formItemId}
67-
component={formItem}
68-
handleComponentUpdate={async (updatedComponent, mutateOptions) => {
69-
handleUpdate(updatedComponent);
70-
debounceSave(formItemId, updatedComponent, mutateOptions);
71-
}}
72-
/>
73-
</Accordion.Content>
74-
</Accordion.Item>
75-
<Accordion.Item open={openList.includes('dynamics')}>
76-
<Accordion.Header onHeaderClick={() => toggleOpen('dynamics')}>
77-
{t('right_menu.dynamics')}
78-
</Accordion.Header>
79-
<Accordion.Content>
80-
<Dynamics />
81-
</Accordion.Content>
82-
</Accordion.Item>
83-
<Accordion.Item open={openList.includes('calculations')}>
84-
<Accordion.Header onHeaderClick={(e) => toggleOpen('calculations')}>
85-
{t('right_menu.calculations')}
86-
</Accordion.Header>
87-
<Accordion.Content>
88-
<DeprecatedCalculationsInfo />
89-
</Accordion.Content>
90-
</Accordion.Item>
91-
</Accordion>
92-
</>
39+
<PropertiesHeader
40+
formItem={formItem}
41+
handleComponentUpdate={async (updatedComponent) => {
42+
handleUpdate(updatedComponent);
43+
debounceSave(formItemId, updatedComponent);
44+
}}
45+
/>
46+
{isNotSubformOrHasLayoutSet && (
47+
<Accordion color='subtle'>
48+
<Accordion.Item open={openList.includes('text')}>
49+
<Accordion.Header
50+
aria-label={t('right_menu.text_label')}
51+
onHeaderClick={() => toggleOpen('text')}
52+
>
53+
{t(formItem.type === 'Image' ? 'right_menu.text_and_image' : 'right_menu.text')}
54+
</Accordion.Header>
55+
<Accordion.Content className={classes.texts}>
56+
<Text />
57+
</Accordion.Content>
58+
</Accordion.Item>
59+
<Accordion.Item open={openList.includes('dataModel')}>
60+
<Accordion.Header onHeaderClick={() => toggleOpen('dataModel')}>
61+
{t('right_menu.data_model_bindings')}
62+
</Accordion.Header>
63+
<Accordion.Content className={classes.dataModelBindings}>
64+
<DataModelBindings />
65+
</Accordion.Content>
66+
</Accordion.Item>
67+
<Accordion.Item open={openList.includes('content')}>
68+
<Accordion.Header onHeaderClick={() => toggleOpen('content')}>
69+
{t('right_menu.content')}
70+
</Accordion.Header>
71+
<Accordion.Content>
72+
<EditFormComponent
73+
editFormId={formItemId}
74+
component={formItem}
75+
handleComponentUpdate={async (updatedComponent, mutateOptions) => {
76+
handleUpdate(updatedComponent);
77+
debounceSave(formItemId, updatedComponent, mutateOptions);
78+
}}
79+
/>
80+
</Accordion.Content>
81+
</Accordion.Item>
82+
<Accordion.Item open={openList.includes('dynamics')}>
83+
<Accordion.Header onHeaderClick={() => toggleOpen('dynamics')}>
84+
{t('right_menu.dynamics')}
85+
</Accordion.Header>
86+
<Accordion.Content>
87+
<Dynamics />
88+
</Accordion.Content>
89+
</Accordion.Item>
90+
<Accordion.Item open={openList.includes('calculations')}>
91+
<Accordion.Header onHeaderClick={(e) => toggleOpen('calculations')}>
92+
{t('right_menu.calculations')}
93+
</Accordion.Header>
94+
<Accordion.Content>
95+
<DeprecatedCalculationsInfo />
96+
</Accordion.Content>
97+
</Accordion.Item>
98+
</Accordion>
9399
)}
94100
</div>
95101
);

frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React, { useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { DefinedLayoutSet } from './DefinedLayoutSet/DefinedLayoutSet';
4-
import { UndefinedLayoutSet } from './UndefinedLayoutSet/UndefinedLayoutSet';
54
import { SelectLayoutSet } from './SelectLayoutSet/SelectLayoutSet';
5+
import { StudioRecommendedNextAction } from '@studio/components';
66

77
type EditLayoutSetProps = {
88
existingLayoutSetForSubform: string;
@@ -22,17 +22,27 @@ export const EditLayoutSet = ({
2222
existingLayoutSetForSubForm={existingLayoutSetForSubform}
2323
onUpdateLayoutSet={onUpdateLayoutSet}
2424
onSetLayoutSetSelectorVisible={setIsLayoutSetSelectorVisible}
25+
showButtons={true}
2526
/>
2627
);
2728
}
2829

2930
const layoutSetIsUndefined = !existingLayoutSetForSubform;
3031
if (layoutSetIsUndefined) {
3132
return (
32-
<UndefinedLayoutSet
33-
label={t('ux_editor.component_properties.subform.selected_layout_set_label')}
34-
onClick={() => setIsLayoutSetSelectorVisible(true)}
35-
/>
33+
<StudioRecommendedNextAction
34+
title={t('ux_editor.component_properties.subform.choose_layout_set_header')}
35+
description={t('ux_editor.component_properties.subform.choose_layout_set_description')}
36+
hideSaveButton={true}
37+
hideSkipButton={true}
38+
>
39+
<SelectLayoutSet
40+
existingLayoutSetForSubForm={existingLayoutSetForSubform}
41+
onUpdateLayoutSet={onUpdateLayoutSet}
42+
onSetLayoutSetSelectorVisible={setIsLayoutSetSelectorVisible}
43+
showButtons={false}
44+
/>
45+
</StudioRecommendedNextAction>
3646
);
3747
}
3848

Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
.selectLayoutSet {
2-
display: flex;
2+
display: grid;
33
flex-direction: column;
44
gap: var(--fds-spacing-2);
5+
}
6+
7+
.selectLayoutSetwithPadding {
58
padding: 0 var(--fds-spacing-5);
69
}
10+
11+
.layoutSetsOption {
12+
text-overflow: ellipsis;
13+
width: 100%;
14+
}

frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ import { EditLayoutSetButtons } from './EditLayoutSetButtons/EditLayoutSetButton
66
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
77
import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery';
88
import { SubFormUtilsImpl } from '../../../../../../classes/SubFormUtils';
9+
import cn from 'classnames';
910

1011
type SelectLayoutSetProps = {
1112
existingLayoutSetForSubForm: string;
1213
onUpdateLayoutSet: (layoutSetId: string) => void;
1314
onSetLayoutSetSelectorVisible: (visible: boolean) => void;
15+
showButtons?: boolean;
1416
};
1517

1618
export const SelectLayoutSet = ({
1719
existingLayoutSetForSubForm,
1820
onUpdateLayoutSet,
1921
onSetLayoutSetSelectorVisible,
22+
showButtons,
2023
}: SelectLayoutSetProps) => {
2124
const { t } = useTranslation();
2225
const { org, app } = useStudioEnvironmentParams();
@@ -48,8 +51,13 @@ export const SelectLayoutSet = ({
4851
};
4952

5053
return (
51-
<div className={classes.selectLayoutSet}>
54+
<div
55+
className={cn(classes.selectLayoutSet, {
56+
[classes.selectLayoutSetwithPadding]: existingLayoutSetForSubForm,
57+
})}
58+
>
5259
<StudioNativeSelect
60+
className={classes.layoutSetsOption}
5361
size='small'
5462
onChange={handleLayoutSetChange}
5563
label={t('ux_editor.component_properties.subform.choose_layout_set_label')}
@@ -63,7 +71,9 @@ export const SelectLayoutSet = ({
6371
</option>
6472
))}
6573
</StudioNativeSelect>
66-
<EditLayoutSetButtons onClose={closeLayoutSetSelector} onDelete={deleteLinkToLayoutSet} />
74+
{showButtons && (
75+
<EditLayoutSetButtons onClose={closeLayoutSetSelector} onDelete={deleteLinkToLayoutSet} />
76+
)}
6777
</div>
6878
);
6979
};

0 commit comments

Comments
 (0)