diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 760a9ae779f..a53c582ec7b 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1292,8 +1292,10 @@ "ux_editor.component_properties.style": "Stil", "ux_editor.component_properties.subdomains": "Subdomener (kommaseparert)", "ux_editor.component_properties.subform": "Sidegruppe for underskjema", - "ux_editor.component_properties.subform.choose_layout_set": "Velg sidegruppe...", - "ux_editor.component_properties.subform.choose_layout_set_label": "Velg sidegruppe å knytte til underskjema", + "ux_editor.component_properties.subform.choose_layout_set": "Velg et underskjema...", + "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.", + "ux_editor.component_properties.subform.choose_layout_set_header": "Velg underskjemaet du vil bruke", + "ux_editor.component_properties.subform.choose_layout_set_label": "Velg et underskjema", "ux_editor.component_properties.subform.go_to_layout_set": "Gå til utforming av underskjemaet", "ux_editor.component_properties.subform.no_layout_sets_acting_as_subform": "Det finnes ingen sidegrupper i løsningen som kan brukes som et underskjema", "ux_editor.component_properties.subform.selected_layout_set_label": "Underskjema", diff --git a/frontend/libs/studio-components/src/components/StudioRecommendedNextAction/StudioRecommendedNextAction.tsx b/frontend/libs/studio-components/src/components/StudioRecommendedNextAction/StudioRecommendedNextAction.tsx index 3e71fdde962..12313cb3cfa 100644 --- a/frontend/libs/studio-components/src/components/StudioRecommendedNextAction/StudioRecommendedNextAction.tsx +++ b/frontend/libs/studio-components/src/components/StudioRecommendedNextAction/StudioRecommendedNextAction.tsx @@ -6,13 +6,14 @@ import { StudioParagraph } from '../StudioParagraph'; import { Heading } from '@digdir/designsystemet-react'; export type StudioRecommendedNextActionProps = { - onSave: React.FormEventHandler; - saveButtonText: string; - onSkip: React.MouseEventHandler; - skipButtonText: string; + onSave?: React.FormEventHandler; + saveButtonText?: string; + onSkip?: React.MouseEventHandler; + skipButtonText?: string; title: string; description: string; hideSaveButton?: boolean; + hideSkipButton?: boolean; children: React.ReactNode; }; @@ -24,6 +25,7 @@ export const StudioRecommendedNextAction = ({ title, description, hideSaveButton = false, + hideSkipButton, children, }: StudioRecommendedNextActionProps): React.ReactElement => { const formName = useId(); @@ -44,9 +46,11 @@ export const StudioRecommendedNextAction = ({ {saveButtonText} )} - - {skipButtonText} - + {!hideSkipButton && ( + + {skipButtonText} + + )} diff --git a/frontend/packages/ux-editor/src/classes/SubFormUtils.ts b/frontend/packages/ux-editor/src/classes/SubFormUtils.ts index c916dcbb7cb..1cda8bd7507 100644 --- a/frontend/packages/ux-editor/src/classes/SubFormUtils.ts +++ b/frontend/packages/ux-editor/src/classes/SubFormUtils.ts @@ -21,6 +21,8 @@ export class SubFormUtilsImpl implements SubFormUtils { } private get getSubformLayoutSets(): Array { - return this.layoutSets.filter((set) => set.type === 'subform') as Array; + return (this.layoutSets || []).filter( + (set) => set.type === 'subform', + ) as Array; } } diff --git a/frontend/packages/ux-editor/src/components/Properties/Properties.test.tsx b/frontend/packages/ux-editor/src/components/Properties/Properties.test.tsx index dcf1327e760..17d526e4de0 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Properties.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/Properties.test.tsx @@ -243,6 +243,28 @@ describe('Properties', () => { screen.getByText(textMock('right_menu.rules_calculations_deprecated_info_title')), ).toBeInTheDocument(); }); + + it('renders properties when formItem is not a Subform component', () => { + renderProperties({ formItem: componentMocks[ComponentType.Input] }); + expect(screen.getByText(textMock('right_menu.text'))).toBeInTheDocument(); + expect(screen.getByText(textMock('right_menu.data_model_bindings'))).toBeInTheDocument(); + expect(screen.getByText(textMock('right_menu.content'))).toBeInTheDocument(); + expect(screen.getByText(textMock('right_menu.dynamics'))).toBeInTheDocument(); + expect(screen.getByText(textMock('right_menu.calculations'))).toBeInTheDocument(); + }); + + it('render properties accordions for a subform component when it is linked to a subform layoutSet', () => { + editFormComponentSpy.mockReturnValue(); + renderProperties({ + formItem: { ...componentMocks[ComponentType.SubForm], layoutSet: layoutSetName }, + formItemId: componentMocks[ComponentType.SubForm].id, + }); + expect(screen.getByText(textMock('right_menu.text'))).toBeInTheDocument(); + expect(screen.getByText(textMock('right_menu.data_model_bindings'))).toBeInTheDocument(); + expect(screen.getByText(textMock('right_menu.content'))).toBeInTheDocument(); + expect(screen.getByText(textMock('right_menu.dynamics'))).toBeInTheDocument(); + expect(screen.getByText(textMock('right_menu.calculations'))).toBeInTheDocument(); + }); }); const getComponent = ( @@ -268,7 +290,9 @@ const renderProperties = ( }, ) => { const queryClientMock = createQueryClientMock(); + queryClientMock.setQueryData([QueryKey.FormLayouts, org, app, layoutSetName], layouts); + queryClientMock.setQueryData([QueryKey.LayoutSets, org, app], layoutSet1NameMock); return renderWithProviders(getComponent(formItemContextProps), { queryClient: queryClientMock, diff --git a/frontend/packages/ux-editor/src/components/Properties/Properties.tsx b/frontend/packages/ux-editor/src/components/Properties/Properties.tsx index d58f3daa399..ff03e5b6a69 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Properties.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/Properties.tsx @@ -16,6 +16,14 @@ export const Properties = () => { const { formItemId, formItem, handleUpdate, debounceSave } = useFormItemContext(); const [openList, setOpenList] = React.useState([]); + if (!formItem) { + return ( +
+ +
+ ); + } + const toggleOpen = (id: string) => { if (openList.includes(id)) { setOpenList(openList.filter((item) => item !== id)); @@ -24,72 +32,70 @@ export const Properties = () => { } }; + const isNotSubformOrHasLayoutSet = formItem.type !== 'SubForm' || !!formItem.layoutSet; + return (
- {!formItem ? ( - - ) : ( - <> - { - handleUpdate(updatedComponent); - debounceSave(formItemId, updatedComponent); - }} - /> - - - toggleOpen('text')} - > - {t(formItem.type === 'Image' ? 'right_menu.text_and_image' : 'right_menu.text')} - - - - - - - toggleOpen('dataModel')}> - {t('right_menu.data_model_bindings')} - - - - - - - toggleOpen('content')}> - {t('right_menu.content')} - - - { - handleUpdate(updatedComponent); - debounceSave(formItemId, updatedComponent, mutateOptions); - }} - /> - - - - toggleOpen('dynamics')}> - {t('right_menu.dynamics')} - - - - - - - toggleOpen('calculations')}> - {t('right_menu.calculations')} - - - - - - - + { + handleUpdate(updatedComponent); + debounceSave(formItemId, updatedComponent); + }} + /> + {isNotSubformOrHasLayoutSet && ( + + + toggleOpen('text')} + > + {t(formItem.type === 'Image' ? 'right_menu.text_and_image' : 'right_menu.text')} + + + + + + + toggleOpen('dataModel')}> + {t('right_menu.data_model_bindings')} + + + + + + + toggleOpen('content')}> + {t('right_menu.content')} + + + { + handleUpdate(updatedComponent); + debounceSave(formItemId, updatedComponent, mutateOptions); + }} + /> + + + + toggleOpen('dynamics')}> + {t('right_menu.dynamics')} + + + + + + + toggleOpen('calculations')}> + {t('right_menu.calculations')} + + + + + + )}
); diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx index 4bd920fbe96..eff8e7bcd9c 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DefinedLayoutSet } from './DefinedLayoutSet/DefinedLayoutSet'; -import { UndefinedLayoutSet } from './UndefinedLayoutSet/UndefinedLayoutSet'; import { SelectLayoutSet } from './SelectLayoutSet/SelectLayoutSet'; +import { StudioRecommendedNextAction } from '@studio/components'; type EditLayoutSetProps = { existingLayoutSetForSubform: string; @@ -22,6 +22,7 @@ export const EditLayoutSet = ({ existingLayoutSetForSubForm={existingLayoutSetForSubform} onUpdateLayoutSet={onUpdateLayoutSet} onSetLayoutSetSelectorVisible={setIsLayoutSetSelectorVisible} + showButtons={true} /> ); } @@ -29,10 +30,19 @@ export const EditLayoutSet = ({ const layoutSetIsUndefined = !existingLayoutSetForSubform; if (layoutSetIsUndefined) { return ( - setIsLayoutSetSelectorVisible(true)} - /> + + + ); } diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.module.css b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.module.css index d3243791006..9542531cb5a 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.module.css +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.module.css @@ -1,6 +1,14 @@ .selectLayoutSet { - display: flex; + display: grid; flex-direction: column; gap: var(--fds-spacing-2); +} + +.selectLayoutSetwithPadding { padding: 0 var(--fds-spacing-5); } + +.layoutSetsOption { + text-overflow: ellipsis; + width: 100%; +} diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx index 3c4a190e817..d0eff68e556 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx @@ -6,17 +6,20 @@ import { EditLayoutSetButtons } from './EditLayoutSetButtons/EditLayoutSetButton import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery'; import { SubFormUtilsImpl } from '../../../../../../classes/SubFormUtils'; +import cn from 'classnames'; type SelectLayoutSetProps = { existingLayoutSetForSubForm: string; onUpdateLayoutSet: (layoutSetId: string) => void; onSetLayoutSetSelectorVisible: (visible: boolean) => void; + showButtons?: boolean; }; export const SelectLayoutSet = ({ existingLayoutSetForSubForm, onUpdateLayoutSet, onSetLayoutSetSelectorVisible, + showButtons, }: SelectLayoutSetProps) => { const { t } = useTranslation(); const { org, app } = useStudioEnvironmentParams(); @@ -48,8 +51,13 @@ export const SelectLayoutSet = ({ }; return ( -
+
))} - + {showButtons && ( + + )}
); }; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx index cbbbcac4006..0140a74acfc 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx @@ -10,7 +10,6 @@ import { app, org } from '@studio/testing/testids'; import { QueryKey } from 'app-shared/types/QueryKey'; import { layoutSets } from 'app-shared/mocks/mocks'; import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse'; -import type { UserEvent } from '@testing-library/user-event'; import userEvent from '@testing-library/user-event'; import type { FormComponent } from '../../../../types/FormComponent'; import { AppContext } from '../../../../AppContext'; @@ -30,20 +29,27 @@ describe('EditLayoutSetForSubForm', () => { expect(noExistingSubFormForLayoutSet).toBeInTheDocument(); }); - it('displays a button to set subform if subform layout sets exists', () => { + it('displays the headers for recommendNextAction if subform layout sets exists', () => { const subformLayoutSetId = 'subformLayoutSetId'; renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); - const setLayoutSetButton = screen.getByRole('button', { - name: textMock('ux_editor.component_properties.subform.selected_layout_set_label'), + const setLayoutSetButton = screen.getByRole('heading', { + name: textMock('ux_editor.component_properties.subform.choose_layout_set_header'), }); expect(setLayoutSetButton).toBeInTheDocument(); }); - it('displays a select to choose a layout set for the subform when clicking button to set', async () => { - const user = userEvent.setup(); + it('displays the description for recommendNextAction if subform layout sets exists', () => { + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); + const setLayoutSetButton = screen.getByText( + textMock('ux_editor.component_properties.subform.choose_layout_set_description'), + ); + expect(setLayoutSetButton).toBeInTheDocument(); + }); + + it('displays a select to choose a layout set for the subform', async () => { const subformLayoutSetId = 'subformLayoutSetId'; renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); - await openEditMode(user); const selectLayoutSet = getSelectForLayoutSet(); const options = within(selectLayoutSet).getAllByRole('option'); expect(options).toHaveLength(2); @@ -57,7 +63,6 @@ describe('EditLayoutSetForSubForm', () => { const user = userEvent.setup(); const subformLayoutSetId = 'subformLayoutSetId'; renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); - await openEditMode(user); const selectLayoutSet = getSelectForLayoutSet(); await user.selectOptions(selectLayoutSet, subformLayoutSetId); expect(handleComponentChangeMock).toHaveBeenCalledTimes(1); @@ -68,11 +73,33 @@ describe('EditLayoutSetForSubForm', () => { ); }); + it('should display the selected layout set in document after the user choose it', async () => { + const user = userEvent.setup(); + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); + const selectLayoutSet = getSelectForLayoutSet(); + await user.selectOptions(selectLayoutSet, subformLayoutSetId); + expect(screen.getByText(subformLayoutSetId)).toBeInTheDocument(); + }); + + it('should display the select again with its buttons when the user clicks on the seleced layoutset', async () => { + const user = userEvent.setup(); + const subformLayoutSetId = 'subformLayoutSetId'; + renderEditLayoutSetForSubForm( + { sets: [{ id: subformLayoutSetId, type: 'subform' }] }, + { layoutSet: subformLayoutSetId }, + ); + await user.click(screen.getByText(subformLayoutSetId)); + const selectLayoutSet = getSelectForLayoutSet(); + expect(selectLayoutSet).toBeInTheDocument(); + expect(screen.getByRole('button', { name: textMock('general.close') })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: textMock('general.delete') })).toBeInTheDocument(); + }); + it('calls handleComponentChange with no layout set for component if selecting the empty option', async () => { const user = userEvent.setup(); const subformLayoutSetId = 'subformLayoutSetId'; renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); - await openEditMode(user); const selectLayoutSet = getSelectForLayoutSet(); const emptyOptionText = textMock('ux_editor.component_properties.subform.choose_layout_set'); await user.selectOptions(selectLayoutSet, emptyOptionText); @@ -87,23 +114,29 @@ describe('EditLayoutSetForSubForm', () => { it('closes the view mode when clicking close button after selecting a layout set', async () => { const user = userEvent.setup(); const subformLayoutSetId = 'subformLayoutSetId'; - renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); - await openEditMode(user); - const closeSetLayoutSetButton = screen.getByRole('button', { - name: textMock('general.close'), - }); - await user.click(closeSetLayoutSetButton); - const setLayoutSetButtonAfterClose = screen.getByRole('button', { - name: textMock('ux_editor.component_properties.subform.selected_layout_set_label'), - }); - expect(setLayoutSetButtonAfterClose).toBeInTheDocument(); + renderEditLayoutSetForSubForm( + { sets: [{ id: subformLayoutSetId, type: 'subform' }] }, + { layoutSet: subformLayoutSetId }, + ); + await user.click(screen.getByText(subformLayoutSetId)); + const closeButton = screen.getByRole('button', { name: textMock('general.close') }); + await user.click(closeButton); + expect( + screen.queryByRole('button', { name: textMock('general.close') }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: textMock('general.delete') }), + ).not.toBeInTheDocument(); }); it('calls handleComponentChange with no layout set for component when clicking delete button', async () => { const user = userEvent.setup(); const subformLayoutSetId = 'subformLayoutSetId'; - renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] }); - await openEditMode(user); + renderEditLayoutSetForSubForm( + { sets: [{ id: subformLayoutSetId, type: 'subform' }] }, + { layoutSet: subformLayoutSetId }, + ); + await user.click(screen.getByText(subformLayoutSetId)); const deleteLayoutSetConnectionButton = screen.getByRole('button', { name: textMock('general.delete'), }); @@ -130,7 +163,7 @@ describe('EditLayoutSetForSubForm', () => { expect(existingLayoutSetButton).toBeInTheDocument(); }); - it('opens view mode when clicking the button when a layout set for the subform if set', async () => { + it('opens view mode when a layout set for the subform is set', async () => { const user = userEvent.setup(); const subformLayoutSetId = 'subformLayoutSetId'; renderEditLayoutSetForSubForm( @@ -148,13 +181,6 @@ describe('EditLayoutSetForSubForm', () => { }); }); -const openEditMode = async (user: UserEvent) => { - const setLayoutSetButton = screen.getByRole('button', { - name: textMock('ux_editor.component_properties.subform.selected_layout_set_label'), - }); - await user.click(setLayoutSetButton); -}; - const getSelectForLayoutSet = () => screen.getByRole('combobox', { name: textMock('ux_editor.component_properties.subform.choose_layout_set_label'), diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx index a353ea9d863..20babe7477d 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx @@ -27,7 +27,6 @@ const defaultProps: PropertiesHeaderProps = { formItem: component1Mock, handleComponentUpdate: mockHandleComponentUpdate, }; -const user = userEvent.setup(); describe('PropertiesHeader', () => { afterEach(jest.clearAllMocks); @@ -43,6 +42,7 @@ describe('PropertiesHeader', () => { }); it('displays the help text when the help text button is clicked', async () => { + const user = userEvent.setup(); renderPropertiesHeader(); const helpTextButton = screen.getByRole('button', { @@ -61,6 +61,7 @@ describe('PropertiesHeader', () => { }); it('should invoke "handleComponentUpdate" when id field blurs', async () => { + const user = userEvent.setup(); renderPropertiesHeader(); const editComponentIdButton = screen.getByRole('button', { @@ -78,6 +79,7 @@ describe('PropertiesHeader', () => { }); it('should not invoke "handleComponentUpdateMock" when input field has error', async () => { + const user = userEvent.setup(); renderPropertiesHeader(); const editComponentIdButton = screen.getByRole('button', { @@ -98,14 +100,46 @@ describe('PropertiesHeader', () => { expect(mockHandleComponentUpdate).toHaveBeenCalledTimes(0); }); - it('should render subform config when component is subform', () => { + it('should not render recommendedNextAction when component is subform and has layoutset ', () => { + const subformLayoutSetId = 'subformLayoutSetId'; renderPropertiesHeader({ - formItem: { id: 'subformComponentId', type: ComponentType.SubForm, itemType: 'COMPONENT' }, + formItem: { + ...component1Mock, + type: ComponentType.SubForm, + layoutSet: layoutSetName, + id: subformLayoutSetId, + }, }); - const setLayoutSetButton = screen.getByRole('button', { - name: textMock('ux_editor.component_properties.subform.selected_layout_set_label'), + expect(subformLayoutSetId).toBe('subformLayoutSetId'); + expect( + screen.queryByText( + textMock('ux_editor.component_properties.subform.choose_layout_set_header'), + ), + ).not.toBeInTheDocument(); + }); + + it('should render recommendedNextAction when component is subform and has no layoutset ', () => { + renderPropertiesHeader({ + formItem: { + ...component1Mock, + type: ComponentType.SubForm, + }, + }); + expect( + screen.getByText(textMock('ux_editor.component_properties.subform.choose_layout_set_header')), + ).toBeInTheDocument(); + }); + + it('should not render other accordions config when component type is subform and has no layoutset', () => { + renderPropertiesHeader({ + formItem: { + ...component1Mock, + type: ComponentType.SubForm, + }, }); - expect(setLayoutSetButton).toBeInTheDocument(); + expect(screen.queryByText(textMock('right_menu.text'))).not.toBeInTheDocument(); + expect(screen.queryByText(textMock('right_menu.data_model_bindings'))).not.toBeInTheDocument(); + expect(screen.queryByText(textMock('right_menu.content'))).not.toBeInTheDocument(); }); it('should not render subform config when component is not subform', () => {