diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 09346485b3b..647c19fcbc7 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -1032,6 +1032,7 @@ "right_menu.rules_conditional_rendering_add_alt": "Legg til regel for vis/skjul felt", "right_menu.rules_empty": "Ingen regler lagt til...", "right_menu.show_old_dynamics": "Vis gammelt verktøy for dynamikk", + "right_menu.text": "Tekst", "right_menu.warning_dynamics_deprecated": "Denne funksjonaliteten utgår og vi kommer ikke til å vedlikeholde den. Vi anbefaler å bruke <0 href=\"{{expressionDocs}}\" >dynamiske uttrykk.", "schema_editor.active_languages": "Aktive språk:", "schema_editor.add": "Legg til", @@ -1480,7 +1481,7 @@ "ux_editor.component_unknown": "Ukjent komponent", "ux_editor.conditional_rendering_connection_header": "Betingede renderingstilkoblinger", "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.container_not_editable_info": "Noen egenskaper for denne komponenten er ikke redigerbare for øyeblikket. Du kan legge til underkomponenter i kolonnen til venstre og redigere tekster.", "ux_editor.edit_component.show_beta_func": "Vis ny konfigurasjon (BETA)", "ux_editor.edit_component.show_beta_func_help_text": "Vi jobber med å få på plass støtte for å redigere alle innstillinger. Ved å huke av her kan du ta i bruk den nye konfigurasjonsvisningen, som støtter flere innstillinger. Merk at denne visningen fortsatt er under utvikling, og vil kunne oppleves som noe ustabil.", "ux_editor.edit_component.unknown_component": "Komponenten {{componentName}} gjenkjennes ikke av Studio og kan derfor ikke konfigureres.", @@ -1547,6 +1548,7 @@ "ux_editor.modal_new_option": "Legg til flere", "ux_editor.modal_options": "Valg", "ux_editor.modal_properties_add_check_box_options": "Hvordan vil du legge til avkrysningsbokser?", + "ux_editor.modal_properties_add_options": "Hvordan vil du legge til alternativer?", "ux_editor.modal_properties_add_radio_button_options": "Hvordan vil du legge til radioknapper?", "ux_editor.modal_properties_button_helper": "Søk etter tekst til knappen", "ux_editor.modal_properties_button_type_ActionButton": "ActionButton", @@ -1615,6 +1617,8 @@ "ux_editor.modal_properties_read_only": "Read-only.", "ux_editor.modal_properties_read_only_description": "Om dette feltet ikke skal kunne redigeres, kan du skru på read-only.", "ux_editor.modal_properties_tag_helper": "Søk etter merkebeskrivelse", + "ux_editor.modal_properties_textResourceBindings_add_button": "'Legg til ny'-knapp", + "ux_editor.modal_properties_textResourceBindings_add_button_add": "Legg til tekst for 'Legg til ny'-knapp", "ux_editor.modal_properties_textResourceBindings_altTextImg": "Alternativ tekst for bilde", "ux_editor.modal_properties_textResourceBindings_altTextImg_add": "Legg til alternativ tekst for bilde", "ux_editor.modal_properties_textResourceBindings_back": "Tekst på tilbake-knapp", @@ -1623,16 +1627,24 @@ "ux_editor.modal_properties_textResourceBindings_body_add": "Legg til tekstinnhold", "ux_editor.modal_properties_textResourceBindings_description": "Beskrivelse", "ux_editor.modal_properties_textResourceBindings_description_add": "Legg til beskrivelse", + "ux_editor.modal_properties_textResourceBindings_edit_button_close": "'Rediger'-knapp (lukket gruppe)", + "ux_editor.modal_properties_textResourceBindings_edit_button_close_add": "Legg til tekst for 'Rediger'-knapp (lukket gruppe)", + "ux_editor.modal_properties_textResourceBindings_edit_button_open": "'Rediger'-knapp (åpen gruppe)", + "ux_editor.modal_properties_textResourceBindings_edit_button_open_add": "Legg til tekst for 'Rediger'-knapp (åpen gruppe)", "ux_editor.modal_properties_textResourceBindings_help": "Hjelpetekst", "ux_editor.modal_properties_textResourceBindings_help_add": "Legg til hjelpetekst", "ux_editor.modal_properties_textResourceBindings_next": "Tekst på neste-knapp", "ux_editor.modal_properties_textResourceBindings_next_add": "Legg til tekst på neste-knapp", + "ux_editor.modal_properties_textResourceBindings_save_and_next_button": "'Lagre og neste'-knapp", + "ux_editor.modal_properties_textResourceBindings_save_and_next_button_add": "Legg til tekst for 'Lagre og neste'-knapp", + "ux_editor.modal_properties_textResourceBindings_save_button": "'Lagre'-knapp", + "ux_editor.modal_properties_textResourceBindings_save_button_add": "'Legg til tekst for Lagre'-knapp", "ux_editor.modal_properties_textResourceBindings_shortName": "Kortnavn", "ux_editor.modal_properties_textResourceBindings_shortName_add": "Legg til kortnavn", "ux_editor.modal_properties_textResourceBindings_tableTitle": "Tittel i tabell", "ux_editor.modal_properties_textResourceBindings_tableTitle_add": "Legg til tittel i tabell", - "ux_editor.modal_properties_textResourceBindings_tag": "Merkebeskrivelse:", - "ux_editor.modal_properties_textResourceBindings_tag_add": "Legg til merkebeskrivelse", + "ux_editor.modal_properties_textResourceBindings_tagTitle": "Merkebeskrivelse", + "ux_editor.modal_properties_textResourceBindings_tagTitle_add": "Legg til merkebeskrivelse", "ux_editor.modal_properties_textResourceBindings_title": "Ledetekst", "ux_editor.modal_properties_textResourceBindings_title_add": "Legg til ledetekst", "ux_editor.modal_properties_trigger_validation_label": "Skal feltet trigge en validering?", @@ -1667,6 +1679,14 @@ "ux_editor.pages_error_length": "Navnet kan ikke være lengre enn 30 tegn.", "ux_editor.pages_error_unique": "Navnet må være unikt.", "ux_editor.preview": "Forhåndsvisning", + "ux_editor.properties_panel.options.add_options": "Legg til alternativer", + "ux_editor.properties_panel.options.codelist_switch_to_custom": "Bytt til egendefinert kodeliste", + "ux_editor.properties_panel.options.codelist_switch_to_static": "Bytt til statisk kodeliste", + "ux_editor.properties_panel.options.remove_option": "Slett alternativ", + "ux_editor.properties_panel.options.use_code_list_helptext": "Skru på denne innstillingen for å bruke en kodeliste for å styre alternativene til denne komponenten. Skru av innstillingen for å legge til alternativene manuelt.", + "ux_editor.properties_panel.options.use_code_list_label": "Bruk kodeliste", + "ux_editor.properties_panel.texts.no_properties": "Det er ingen tekster å konfigurere for denne komponenten.", + "ux_editor.properties_panel.texts.options_title": "Kodelister og alternativer", "ux_editor.radios_description_add": "Legg til beskrivelse", "ux_editor.radios_description_placeholder": "Ingen beskrivelse", "ux_editor.radios_error_DuplicateValues": "Alle verdier må være unike.", diff --git a/frontend/packages/ux-editor-v3/src/components/config/componentConfig.tsx b/frontend/packages/ux-editor-v3/src/components/config/componentConfig.tsx index d719bc4a3c8..bcd4c9887f2 100644 --- a/frontend/packages/ux-editor-v3/src/components/config/componentConfig.tsx +++ b/frontend/packages/ux-editor-v3/src/components/config/componentConfig.tsx @@ -114,8 +114,8 @@ export const configComponents: IConfigComponents = { component={component} handleComponentChange={handleComponentChange} textKey={EditSettings.TagTitle} - labelKey='ux_editor.modal_properties_textResourceBindings_tag' - placeholderKey='ux_editor.modal_properties_textResourceBindings_tag_add' + labelKey='ux_editor.modal_properties_textResourceBindings_tagTitle' + placeholderKey='ux_editor.modal_properties_textResourceBindings_tagTitle_add' /> ), [EditSettings.Options]: EditOptions, diff --git a/frontend/packages/ux-editor/src/components/Properties/Content.test.tsx b/frontend/packages/ux-editor/src/components/Properties/Content.test.tsx index e7f015c9c2f..f611ca78ec3 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Content.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/Content.test.tsx @@ -14,24 +14,9 @@ import { useLayoutSchemaQuery } from '../../hooks/queries/useLayoutSchemaQuery'; const user = userEvent.setup(); -// Test data: -const textResourceEditTestId = 'text-resource-edit'; - -// Mocks: -jest.mock('../TextResourceEdit', () => ({ - TextResourceEdit: () =>
, -})); - describe('ContentTab', () => { afterEach(jest.clearAllMocks); - describe('when editing a text resource', () => { - it('should render the component', async () => { - await render({ props: {}, editId: 'test' }); - expect(screen.getByTestId(textResourceEditTestId)).toBeInTheDocument(); - }); - }); - describe('when editing a container', () => { const props = { formId: container1IdMock, diff --git a/frontend/packages/ux-editor/src/components/Properties/Content.tsx b/frontend/packages/ux-editor/src/components/Properties/Content.tsx index 8a287f5baab..1825b36eb15 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Content.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/Content.tsx @@ -1,20 +1,11 @@ import React from 'react'; -import { TextResourceEdit } from '../TextResourceEdit'; import { EditFormComponent } from '../config/EditFormComponent'; import { EditFormContainer } from '../config/EditFormContainer'; -import { getCurrentEditId } from '../../selectors/textResourceSelectors'; -import { useSelector } from 'react-redux'; import { useFormContext } from '../../containers/FormContext'; -import { useTranslation } from 'react-i18next'; import { isContainer } from '../../utils/formItemUtils'; export const Content = () => { const { formId, form, handleUpdate, debounceSave } = useFormContext(); - const editId = useSelector(getCurrentEditId); - const { t } = useTranslation(); - - if (editId) return ; - if (!formId || !form) return t('right_menu.content_empty'); return isContainer(form) ? ( ({ + Text: () =>
, +})); jest.mock('./Content', () => ({ Content: () =>
, })); @@ -39,6 +45,24 @@ jest.mock('./Calculations', () => ({ jest.mock('react-i18next', () => ({ useTranslation: () => mockUseTranslation(texts) })); describe('Properties', () => { + describe('Text', () => { + it('Toggles text when clicked', async () => { + const user = userEvent.setup(); + renderProperties(); + const button = screen.queryByRole('button', { name: textText }); + await act(() => user.click(button)); + expect(button).toHaveAttribute('aria-expanded', 'true'); + await act(() => user.click(button)); + expect(button).toHaveAttribute('aria-expanded', 'false'); + }); + + it('Opens text when a component is selected', async () => { + const { rerender } = renderProperties(); + rerender(getComponent({ formId: 'test' })); + const button = screen.queryByRole('button', { name: textText }); + await waitFor(() => expect(button).toHaveAttribute('aria-expanded', 'true')); + }); + }); describe('Default config', () => { it('hides the properties header when the form is undefined', () => { renderProperties({ form: undefined }); @@ -83,13 +107,6 @@ describe('Properties', () => { await act(() => user.click(button)); expect(button).toHaveAttribute('aria-expanded', 'false'); }); - - it('Opens content when a component is selected', async () => { - const { rerender } = renderProperties(); - rerender(getComponent({ formId: 'test' })); - const button = screen.queryByRole('button', { name: contentText }); - await waitFor(() => expect(button).toHaveAttribute('aria-expanded', 'true')); - }); }); describe('Dynamics', () => { @@ -141,9 +158,11 @@ describe('Properties', () => { it('Renders accordion', () => { const formIdMock = 'test-id'; renderProperties({ formId: formIdMock }); + expect(screen.getByText(textText)).toBeInTheDocument(); expect(screen.getByText(contentText)).toBeInTheDocument(); expect(screen.getByText(dynamicsText)).toBeInTheDocument(); expect(screen.getByText(calculationsText)).toBeInTheDocument(); + expect(screen.getByTestId(textTestId)).toBeInTheDocument(); expect(screen.getByTestId(contentTestId)).toBeInTheDocument(); expect(screen.getByTestId(expressionsTestId)).toBeInTheDocument(); expect(screen.getByTestId(calculationsTestId)).toBeInTheDocument(); diff --git a/frontend/packages/ux-editor/src/components/Properties/Properties.tsx b/frontend/packages/ux-editor/src/components/Properties/Properties.tsx index 996cfd65c7d..03c0973eeb2 100644 --- a/frontend/packages/ux-editor/src/components/Properties/Properties.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/Properties.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { Calculations } from './Calculations'; import { Content } from './Content'; +import { Text } from './Text'; import { useTranslation } from 'react-i18next'; import { Accordion } from '@digdir/design-system-react'; import { useFormContext } from '../../containers/FormContext'; @@ -19,7 +20,7 @@ export const Properties = () => { useEffect(() => { if (formIdRef.current !== formId) { formIdRef.current = formId; - if (formId && openList.length === 0) setOpenList(['content']); + if (formId && openList.length === 0) setOpenList(['text']); } }, [formId, openList.length]); @@ -44,19 +45,27 @@ export const Properties = () => { /> )} + + toggleOpen('text')}> + {t('right_menu.text')} + + {formId ? : t('right_menu.content_empty')} + toggleOpen('content')}> {t('right_menu.content')} - + {formId ? : t('right_menu.content_empty')} toggleOpen('dynamics')}> {t('right_menu.dynamics')} - {formId && } + + {formId ? : t('right_menu.content_empty')} + toggleOpen('calculations')}> diff --git a/frontend/packages/ux-editor/src/components/Properties/Text.module.css b/frontend/packages/ux-editor/src/components/Properties/Text.module.css new file mode 100644 index 00000000000..cb072f38c6d --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/Text.module.css @@ -0,0 +1,3 @@ +.textResourceContainer { + padding-bottom: 20px; +} diff --git a/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx b/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx new file mode 100644 index 00000000000..e6df9548e60 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/Text.test.tsx @@ -0,0 +1,265 @@ +import React from 'react'; +import { Text } from './Text'; +import { screen } from '@testing-library/react'; +import { textMock } from '../../../../../testing/mocks/i18nMock'; +import { FormContext } from '../../containers/FormContext'; +import { + component1IdMock, + component1Mock, + container1IdMock, + layoutMock, +} from '../../testing/layoutMock'; +import type { IAppDataState } from '../../features/appData/appDataReducers'; +import type { ITextResourcesState } from '../../features/appData/textResources/textResourcesSlice'; +import { renderWithMockStore } from '../../testing/mocks'; +import { appDataMock, textResourcesMock } from '../../testing/stateMocks'; +import { formContextProviderMock } from '../../testing/formContextMocks'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { queryClientMock } from 'app-shared/mocks/queryClientMock'; +import { componentSchemaMocks } from '../../testing/componentSchemaMocks'; +import type { ITextResource, ITextResources } from 'app-shared/types/global'; +import { DEFAULT_LANGUAGE } from 'app-shared/constants'; + +// Test data: +const org = 'org'; +const app = 'app'; +const labelTextId = 'labelTextId'; +const descriptionTextId = 'descriptionTextId'; +const addButtonTextId = 'customAddButtonTextId'; +const labelTextValue = 'Label for component'; +const descriptionTextValue = 'Description for component'; +const addButtonTextValue = 'Custom text for add button for group'; +const titleTextResource1: ITextResource = { + id: labelTextId, + value: labelTextValue, +}; +const titleTextResource2: ITextResource = { + id: addButtonTextId, + value: addButtonTextValue, +}; +const titleTextResource3: ITextResource = { + id: descriptionTextId, + value: descriptionTextValue, +}; +const textResources: ITextResources = { + [DEFAULT_LANGUAGE]: [titleTextResource1, titleTextResource2, titleTextResource3], +}; + +const textResourceBindingsPropertiesForComponentType = (componentType: string) => + Object.keys(componentSchemaMocks[componentType].properties.textResourceBindings.properties); + +describe('TextTab', () => { + afterEach(jest.clearAllMocks); + + describe('when editing a container', () => { + const props = { + formId: container1IdMock, + form: { ...layoutMock.containers[container1IdMock] }, + }; + + it('should render the component', async () => { + await render({ props }); + expect(screen.getByRole('heading', { name: textMock('general.text') })).toBeInTheDocument(); + }); + + it('should render all available textResourceBinding properties for the group component', async () => { + await render({ props }); + textResourceBindingsPropertiesForComponentType(props.form.type).forEach((trbProperty) => { + expect( + screen.getByText( + textMock(`ux_editor.modal_properties_textResourceBindings_${trbProperty}`), + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + textMock(`ux_editor.modal_properties_textResourceBindings_${trbProperty}_add`), + ), + ).toBeInTheDocument(); + }); + }); + + it('should render already defined textResourceBinding properties for the group component when exist', async () => { + await render({ + props: { + ...props, + form: { + ...layoutMock.containers[container1IdMock], + textResourceBindings: { title: labelTextId, add_button: addButtonTextId }, + }, + }, + }); + expect(screen.getByText(labelTextValue)).toBeInTheDocument(); + expect(screen.getByText(addButtonTextValue)).toBeInTheDocument(); + }); + + it('should render editable field in nb when a text is in editMode', async () => { + await render({ + props: { + ...props, + form: { + ...layoutMock.containers[container1IdMock], + textResourceBindings: { title: labelTextId, add_button: addButtonTextId }, + }, + }, + editId: labelTextId, + }); + + expect(screen.getByText(textMock('ux_editor.edit_text_resource'))).toBeInTheDocument(); + const labelTextField = screen.getByRole('textbox', { name: textMock('language.nb') }); + expect(labelTextField).toBeInTheDocument(); + }); + }); + + describe('when editing a component', () => { + const props = { + formId: component1IdMock, + form: { ...component1Mock, dataModelBindings: {} }, + }; + + it('should render the component', async () => { + await render({ props }); + expect(screen.getByRole('heading', { name: textMock('general.text') })).toBeInTheDocument(); + }); + + it('should render all available textResourceBinding properties for the input component', async () => { + await render({ props }); + textResourceBindingsPropertiesForComponentType(props.form.type).forEach((trbProperty) => { + expect( + screen.getByText( + textMock(`ux_editor.modal_properties_textResourceBindings_${trbProperty}`), + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + textMock(`ux_editor.modal_properties_textResourceBindings_${trbProperty}_add`), + ), + ).toBeInTheDocument(); + }); + }); + + it('should render already defined textResourceBinding properties for the input component when exist', async () => { + await render({ + props: { + ...props, + form: { + ...layoutMock.components[component1IdMock], + textResourceBindings: { title: labelTextId, description: descriptionTextId }, + }, + }, + }); + expect(screen.getByText(labelTextValue)).toBeInTheDocument(); + expect(screen.getByText(descriptionTextValue)).toBeInTheDocument(); + }); + + it('should render editable field in nb when a text is in editMode', async () => { + await render({ + props: { + ...props, + form: { + ...layoutMock.components[component1IdMock], + textResourceBindings: { title: labelTextId, description: descriptionTextId }, + }, + }, + editId: labelTextId, + }); + + expect(screen.getByText(textMock('ux_editor.edit_text_resource'))).toBeInTheDocument(); + const labelTextField = screen.getByRole('textbox', { name: textMock('language.nb') }); + expect(labelTextField).toBeInTheDocument(); + }); + + it('should not render options section if component schema does not have options/optionsId property', async () => { + await render({ + props: { + ...props, + form: { + ...layoutMock.components[component1IdMock], + }, + }, + }); + + expect( + screen.queryByRole('heading', { + name: textMock('ux_editor.properties_panel.texts.options_title'), + }), + ).not.toBeInTheDocument(); + }); + + it('should render options section if component schema has options property', async () => { + await render({ + props: { + ...props, + form: { + ...layoutMock.components.ComponentWithOptionsMock, + optionsId: 'optionsId', + }, + }, + }); + + expect( + screen.getByRole('heading', { + name: textMock('ux_editor.properties_panel.texts.options_title'), + }), + ).toBeInTheDocument(); + }); + + it('should render options section with codelist view if component has optionId defined', async () => { + await render({ + props: { + ...props, + form: { + ...layoutMock.components.ComponentWithOptionsMock, + optionsId: 'optionsId', + }, + }, + }); + + expect( + screen.getByText(textMock('ux_editor.modal_properties_custom_code_list_id')), + ).toBeInTheDocument(); + }); + + it('should render options section with manual view if component has options', async () => { + await render({ + props: { + ...props, + form: { + ...layoutMock.components.ComponentWithOptionsMock, + options: [{ label: labelTextId, value: 'value' }], + }, + }, + }); + + expect( + screen.getByText(textMock('ux_editor.properties_panel.options.add_options')), + ).toBeInTheDocument(); + }); + }); +}); + +const render = async ({ props = {}, editId }: { props: Partial; editId?: string }) => { + queryClientMock.setQueryData( + [QueryKey.FormComponent, props.form.type], + componentSchemaMocks[props.form.type], + ); + queryClientMock.setQueryData([QueryKey.TextResources, org, app], textResources); + const textResourcesState: ITextResourcesState = { + ...textResourcesMock, + currentEditId: editId, + }; + const appData: IAppDataState = { + ...appDataMock, + textResources: textResourcesState, + }; + + return renderWithMockStore({ appData })( + + + , + ); +}; diff --git a/frontend/packages/ux-editor/src/components/Properties/Text.tsx b/frontend/packages/ux-editor/src/components/Properties/Text.tsx new file mode 100644 index 00000000000..d79b3d5a895 --- /dev/null +++ b/frontend/packages/ux-editor/src/components/Properties/Text.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { useFormContext } from '../../containers/FormContext'; +import { useTranslation } from 'react-i18next'; +import { Heading, Paragraph } from '@digdir/design-system-react'; +import { EditTextResourceBindings } from '../config/editModal/EditTextResourceBindings'; +import { useComponentSchemaQuery } from '../../hooks/queries/useComponentSchemaQuery'; +import { selectedLayoutNameSelector } from '../../selectors/formLayoutSelectors'; +import { StudioSpinner } from '@studio/components'; +import { getCurrentEditId } from '../../selectors/textResourceSelectors'; +import { TextResourceEdit } from '../TextResourceEdit'; +import { EditOptions } from '../config/editModal/EditOptions'; +import classes from './Text.module.css'; +import type { FormComponentBase } from '../../types/FormComponent'; +import type { ComponentType } from 'app-shared/types/ComponentType'; +import type { ComponentSpecificConfig } from 'app-shared/types/ComponentSpecificConfig'; + +export const Text = () => { + const { formId, form, handleUpdate, debounceSave } = useFormContext(); + const { t } = useTranslation(); + + const { data: schema } = useComponentSchemaQuery(form.type); + const selectedLayout = useSelector(selectedLayoutNameSelector); + const editId = useSelector(getCurrentEditId); + + if (editId) return ; + + if (!schema) { + return ; + } + + if (!schema?.properties) { + return {t('ux_editor.properties_panel.texts.no_properties')}; + } + + return ( + <> + {schema.properties.textResourceBindings?.properties && ( +
+ + {t('general.text')} + + { + handleUpdate(updatedComponent); + debounceSave(formId, updatedComponent); + }} + textResourceBindingKeys={Object.keys(schema.properties.textResourceBindings.properties)} + editFormId={formId} + layoutName={selectedLayout} + /> +
+ )} + {(schema.properties.options || schema.properties.optionsId) && ( + <> + + {t('ux_editor.properties_panel.texts.options_title')} + + & + ComponentSpecificConfig) + | (FormComponentBase & + ComponentSpecificConfig) + } + handleComponentChange={async (updatedComponent) => { + handleUpdate(updatedComponent); + debounceSave(formId, updatedComponent); + }} + editFormId={formId} + layoutName={selectedLayout} + renderOptions={{ + onlyCodeListOptions: schema.properties.optionsId && !schema.properties.options, + }} + /> + + )} + + ); +}; diff --git a/frontend/packages/ux-editor/src/components/TextResource.module.css b/frontend/packages/ux-editor/src/components/TextResource.module.css index 110d7dc5e36..2223863c517 100644 --- a/frontend/packages/ux-editor/src/components/TextResource.module.css +++ b/frontend/packages/ux-editor/src/components/TextResource.module.css @@ -1,9 +1,12 @@ .root { --frame-offset: 4px; - border-radius: 2px; + border-radius: 1rem; + padding: 1rem; display: flex; flex-direction: column; - gap: 1rem; + gap: 4px; + margin-top: 8px; + background-color: var(--fds-semantic-surface-action-first-no_fill); } .root.isEditing { @@ -35,7 +38,7 @@ } .textResource { - align-items: stretch; + align-items: center; display: inline-flex; } diff --git a/frontend/packages/ux-editor/src/components/TextResource.tsx b/frontend/packages/ux-editor/src/components/TextResource.tsx index 2cff0882d5d..4c92ebec24f 100644 --- a/frontend/packages/ux-editor/src/components/TextResource.tsx +++ b/frontend/packages/ux-editor/src/components/TextResource.tsx @@ -24,7 +24,6 @@ import { prepend } from 'app-shared/utils/arrayUtils'; import cn from 'classnames'; import type { ITextResource } from 'app-shared/types/global'; import { useTextResourcesSelector } from '../hooks'; -import { FormField } from './FormField'; import { AltinnConfirmDialog } from 'app-shared/components/AltinnConfirmDialog'; import { useTranslation } from 'react-i18next'; import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; @@ -60,7 +59,6 @@ export const TextResource = ({ handleRemoveTextResource, label, placeholder, - previewMode, textResourceId, generateIdOptions, }: TextResourceProps) => { @@ -109,7 +107,6 @@ export const TextResource = ({ ); - return previewMode ? ( - renderTextResource() - ) : ( - renderTextResource()} - /> - ); + return renderTextResource(); }; export interface TextResourceOptionProps { diff --git a/frontend/packages/ux-editor/src/components/config/EditFormComponent.test.tsx b/frontend/packages/ux-editor/src/components/config/EditFormComponent.test.tsx index 49b780258ae..ab17a1097c1 100644 --- a/frontend/packages/ux-editor/src/components/config/EditFormComponent.test.tsx +++ b/frontend/packages/ux-editor/src/components/config/EditFormComponent.test.tsx @@ -32,10 +32,6 @@ const texts = { // Mocks: jest.mock('react-i18next', () => ({ useTranslation: () => mockUseTranslation(texts) })); -const buttonSpecificContentId = 'button-specific-content'; -jest.mock('./componentSpecificContent/Button/ButtonComponent', () => ({ - ButtonComponent: () =>
, -})); const imageSpecificContentId = 'image-specific-content'; jest.mock('./componentSpecificContent/Image/ImageComponent', () => ({ ImageComponent: () =>
, @@ -158,15 +154,6 @@ describe('EditFormComponent', () => { }); }); - test('should return button specific content when type button', async () => { - await render({ - componentProps: { - type: ComponentType.Button, - }, - }); - expect(await screen.findByTestId(buttonSpecificContentId)).toBeInTheDocument(); - }); - test('should render Image component when component type is Image', async () => { await render({ componentProps: { @@ -205,7 +192,6 @@ const waitForData = async () => { { getDatamodelMetadata }, )(() => useDatamodelMetadataQuery('test-org', 'test-app')).renderHookResult.result; await waitFor(() => expect(dataModelMetadataResult.current.isSuccess).toBe(true)); - await waitFor(() => expect(layoutSchemaResult.current[0].isSuccess).toBe(true)); }; const render = async ({ diff --git a/frontend/packages/ux-editor/src/components/config/EditFormComponent.tsx b/frontend/packages/ux-editor/src/components/config/EditFormComponent.tsx index 1e38ed37e4c..623c5e3c4c3 100644 --- a/frontend/packages/ux-editor/src/components/config/EditFormComponent.tsx +++ b/frontend/packages/ux-editor/src/components/config/EditFormComponent.tsx @@ -10,7 +10,6 @@ import { selectedLayoutNameSelector } from '../../selectors/formLayoutSelectors' import { useComponentSchemaQuery } from '../../hooks/queries/useComponentSchemaQuery'; import { StudioSpinner } from '@studio/components'; import { FormComponentConfig } from './FormComponentConfig'; -import { useLayoutSchemaQuery } from '../../hooks/queries/useLayoutSchemaQuery'; import { useSelector } from 'react-redux'; import { getComponentTitleByComponentType } from '../../utils/language'; import { useTranslation } from 'react-i18next'; @@ -41,7 +40,6 @@ export const EditFormComponent = ({ ); const formItemConfig = formItemConfigs[component.type]; - useLayoutSchemaQuery(); // Ensure we load the layout schemas so that component schemas can be loaded const { data: schema, isPending } = useComponentSchemaQuery(component.type); const renderFromComponentSpecificDefinition = (configDef: EditSettings[]) => { diff --git a/frontend/packages/ux-editor/src/components/config/EditFormContainer.tsx b/frontend/packages/ux-editor/src/components/config/EditFormContainer.tsx index 2b87df6ecdb..467bedd4c43 100644 --- a/frontend/packages/ux-editor/src/components/config/EditFormContainer.tsx +++ b/frontend/packages/ux-editor/src/components/config/EditFormContainer.tsx @@ -14,7 +14,6 @@ import { Paragraph, } from '@digdir/design-system-react'; import classes from './EditFormContainer.module.css'; -import { TextResource } from '../TextResource'; import { useDatamodelMetadataQuery } from '../../hooks/queries/useDatamodelMetadataQuery'; import { useText } from '../../hooks'; import { useSelectedFormLayout, useTextResourcesSelector } from '../../hooks'; @@ -87,16 +86,6 @@ export const EditFormContainer = ({ }); }; - const handleButtonTextChange = (id: string) => { - handleContainerUpdate({ - ...container, - textResourceBindings: { - ...container.textResourceBindings, - add_button: id, - }, - }); - }; - const handleTableHeadersChange = (ids: string[]) => { const updatedContainer = { ...container }; updatedContainer.tableHeaders = [...ids]; @@ -204,12 +193,6 @@ export const EditFormContainer = ({ /> )} /> - {items?.length > 0 && ( { it('should render expected components', async () => { render({}); - ['title', 'description', 'help'].forEach(async (key) => { + + [ + 'grid', + 'readOnly', + 'required', + 'hidden', + 'renderAsSummary', + 'variant', + 'autocomplete', + 'maxLength', + 'triggers', + 'labelSettings', + 'pageBreak', + 'formatting', + ].forEach(async (propertyKey) => { expect( - screen.getByText(textMock(`ux_editor.modal_properties_textResourceBindings_${key}`)), + await screen.findByText(textMock(`ux_editor.component_properties.${propertyKey}`)), ).toBeInTheDocument(); - - await waitFor(() => { - expect( - screen.getByText(textMock('ux_editor.modal_properties_data_model_link')), - ).toBeInTheDocument(); - }); - - [ - 'grid', - 'readOnly', - 'required', - 'hidden', - 'renderAsSummary', - 'variant', - 'autocomplete', - 'maxLength', - 'triggers', - 'labelSettings', - 'pageBreak', - 'formatting', - ].forEach(async (propertyKey) => { - expect( - await screen.findByText(textMock(`ux_editor.component_properties.${propertyKey}`)), - ).toBeInTheDocument(); - }); }); }); diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx index f2433aa1b3f..400281f3934 100644 --- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx +++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx @@ -1,13 +1,9 @@ import React from 'react'; import { Alert, Heading, Paragraph } from '@digdir/design-system-react'; import type { FormComponent } from '../../types/FormComponent'; -import { selectedLayoutNameSelector } from '../../selectors/formLayoutSelectors'; -import { EditTextResourceBindings } from './editModal/EditTextResourceBindings'; import { EditBooleanValue } from './editModal/EditBooleanValue'; import { EditNumberValue } from './editModal/EditNumberValue'; -import { EditOptions } from './editModal/EditOptions'; import { EditStringValue } from './editModal/EditStringValue'; -import { useSelector } from 'react-redux'; import { useText } from '../../hooks'; import { getUnsupportedPropertyTypes } from '../../utils/component'; import { EditGrid } from './editModal/EditGrid'; @@ -30,18 +26,17 @@ export const FormComponentConfig = ({ handleComponentUpdate, hideUnsupported, }: FormComponentConfigProps) => { - const selectedLayout = useSelector(selectedLayoutNameSelector); const t = useText(); const componentPropertyLabel = useComponentPropertyLabel(); if (!schema?.properties) return null; const { - textResourceBindings, dataModelBindings, required, readOnly, id, + textResourceBindings, type, options, optionsId, @@ -59,20 +54,6 @@ export const FormComponentConfig = ({ ); return ( <> - {textResourceBindings?.properties && ( - <> - - {t('general.text')} - - - - )} {grid && (
@@ -90,13 +71,6 @@ export const FormComponentConfig = ({ {'Andre innstillinger'} )} - {options && optionsId && ( - - )} {hasCustomFileEndings && ( <> diff --git a/frontend/packages/ux-editor/src/components/config/componentConfig.tsx b/frontend/packages/ux-editor/src/components/config/componentConfig.tsx index 1692c2072cf..684b86a93c6 100644 --- a/frontend/packages/ux-editor/src/components/config/componentConfig.tsx +++ b/frontend/packages/ux-editor/src/components/config/componentConfig.tsx @@ -1,13 +1,10 @@ -import React from 'react'; import { ComponentType } from 'app-shared/types/ComponentType'; import { EditCodeList } from './editModal/EditCodeList'; import { EditHeaderSize } from './editModal/EditHeaderSize'; -import { EditOptions } from './editModal/EditOptions'; import { EditPreselectedIndex } from './editModal/EditPreselectedIndex'; import { EditReadOnly } from './editModal/EditReadOnly'; import { EditRequired } from './editModal/EditRequired'; import { EditAutoComplete } from './editModal/EditAutoComplete'; -import { EditTextResourceBinding } from './editModal/EditTextResourceBinding'; import type { FormItem } from '../../types/FormItem'; export interface IGenericEditComponent { @@ -18,10 +15,6 @@ export interface IGenericEditComponent } export enum EditSettings { - Title = 'title', - Description = 'description', - Help = 'help', - TagTitle = 'tagTitle', DataModelBindings = 'dataModelBindings', Size = 'size', ReadOnly = 'readonly', @@ -34,9 +27,6 @@ export enum EditSettings { export const editBoilerPlate = [ EditSettings.DataModelBindings, - EditSettings.Title, - EditSettings.Description, - EditSettings.Help, EditSettings.ReadOnly, EditSettings.Required, ]; @@ -50,83 +40,25 @@ interface IConfigComponents { } export const componentSpecificEditConfig: IComponentEditConfig = { - [ComponentType.Header]: [EditSettings.Title, EditSettings.Size], + [ComponentType.Header]: [EditSettings.Size], [ComponentType.Input]: [...editBoilerPlate, EditSettings.AutoComplete], [ComponentType.TextArea]: [...editBoilerPlate, EditSettings.AutoComplete], [ComponentType.Datepicker]: [...editBoilerPlate], - [ComponentType.Paragraph]: [EditSettings.Title], - [ComponentType.AttachmentList]: [EditSettings.Title], - [ComponentType.Checkboxes]: [ - ...editBoilerPlate, - EditSettings.Options, - EditSettings.PreselectedIndex, - ], - [ComponentType.RadioButtons]: [ - ...editBoilerPlate, - EditSettings.Options, - EditSettings.PreselectedIndex, - ], + [ComponentType.Checkboxes]: [...editBoilerPlate, EditSettings.PreselectedIndex], + [ComponentType.RadioButtons]: [...editBoilerPlate, EditSettings.PreselectedIndex], [ComponentType.Dropdown]: [ ...editBoilerPlate, - EditSettings.CodeList, EditSettings.PreselectedIndex, EditSettings.AutoComplete, ], - [ComponentType.AddressComponent]: [EditSettings.Title, EditSettings.Help], - [ComponentType.FileUpload]: [EditSettings.Title, EditSettings.Description, EditSettings.Help], - [ComponentType.FileUploadWithTag]: [ - EditSettings.Title, - EditSettings.Description, - EditSettings.Help, - EditSettings.TagTitle, - EditSettings.CodeList, - ], - [ComponentType.Panel]: [EditSettings.Title], [ComponentType.Map]: [...editBoilerPlate], }; export const configComponents: IConfigComponents = { [EditSettings.Size]: EditHeaderSize, - [EditSettings.Title]: ({ component, handleComponentChange }: IGenericEditComponent) => ( - - ), [EditSettings.ReadOnly]: EditReadOnly, [EditSettings.Required]: EditRequired, - [EditSettings.Description]: ({ component, handleComponentChange }: IGenericEditComponent) => ( - - ), - [EditSettings.TagTitle]: ({ component, handleComponentChange }: IGenericEditComponent) => ( - - ), - [EditSettings.Options]: EditOptions, [EditSettings.CodeList]: EditCodeList, [EditSettings.PreselectedIndex]: EditPreselectedIndex, [EditSettings.AutoComplete]: EditAutoComplete, - [EditSettings.Help]: ({ component, handleComponentChange }: IGenericEditComponent) => ( - - ), }; diff --git a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/ButtonComponent.module.css b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/ButtonComponent.module.css deleted file mode 100644 index 63eaa171d31..00000000000 --- a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/ButtonComponent.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.root > div { - display: flex; - flex-direction: column; - gap: var(--fieldset-gap); -} diff --git a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/ButtonComponent.test.tsx b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/ButtonComponent.test.tsx deleted file mode 100644 index b30eb756493..00000000000 --- a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/ButtonComponent.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { screen, waitFor } from '@testing-library/react'; -import { renderWithMockStore, renderHookWithMockStore } from '../../../../testing/mocks'; -import { useLayoutSchemaQuery } from '../../../../hooks/queries/useLayoutSchemaQuery'; -import { ButtonComponent } from './ButtonComponent'; -import { ComponentType } from 'app-shared/types/ComponentType'; -import type { FormButtonComponent } from '../../../../types/FormComponent'; -import type { IGenericEditComponent } from '../../componentConfig'; -import { textMock } from '../../../../../../../testing/mocks/i18nMock'; - -// Test data: -const component: FormButtonComponent = { - id: '1', - onClickAction: jest.fn(), - type: ComponentType.Button, - itemType: 'COMPONENT', - dataModelBindings: {}, -}; -const handleComponentChange = jest.fn(); -const defaultProps: IGenericEditComponent = { - component, - handleComponentChange, -}; - -describe('ButtonComponent', () => { - it('should render title text resource bindings for Button component', async () => { - await render(); - expect( - screen.getByText(textMock('ux_editor.modal_properties_textResourceBindings_title')), - ).toBeInTheDocument(); - }); - - it('should render next and back text resource bindings for NavigationButtons component', async () => { - await render({ - component: { - ...component, - type: ComponentType.NavigationButtons, - }, - }); - expect( - screen.getByText(textMock('ux_editor.modal_properties_textResourceBindings_next')), - ).toBeInTheDocument(); - expect( - screen.getByText(textMock('ux_editor.modal_properties_textResourceBindings_back')), - ).toBeInTheDocument(); - }); -}); - -const waitForData = async () => { - const layoutSchemaResult = renderHookWithMockStore()(() => useLayoutSchemaQuery()) - .renderHookResult.result; - await waitFor(() => expect(layoutSchemaResult.current[0].isSuccess).toBe(true)); -}; - -const render = async (props?: Partial) => { - await waitForData(); - - renderWithMockStore()(); -}; diff --git a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/ButtonComponent.tsx b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/ButtonComponent.tsx deleted file mode 100644 index 920ff8cd359..00000000000 --- a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/ButtonComponent.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { Fieldset } from '@digdir/design-system-react'; -import classes from './ButtonComponent.module.css'; -import type { IGenericEditComponent } from '../../componentConfig'; -import { EditSettings } from '../../componentConfig'; -import { ComponentType } from 'app-shared/types/ComponentType'; -import { EditTextResourceBinding } from '../../editModal/EditTextResourceBinding'; -import { EditTextResourceBindings } from '../../editModal/EditTextResourceBindings'; -import { useTranslation } from 'react-i18next'; - -export const ButtonComponent = ({ component, handleComponentChange }: IGenericEditComponent) => { - const { t } = useTranslation(); - return ( -
- {component.type === ComponentType.Button && ( - - )} - {component.type === ComponentType.NavigationButtons && ( - - )} -
- ); -}; diff --git a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/index.ts b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/index.ts deleted file mode 100644 index 9ea38501c7f..00000000000 --- a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/Button/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ButtonComponent } from './ButtonComponent'; diff --git a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/ComponentSpecificContent.tsx b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/ComponentSpecificContent.tsx index 1b66aa81008..48296a18e81 100644 --- a/frontend/packages/ux-editor/src/components/config/componentSpecificContent/ComponentSpecificContent.tsx +++ b/frontend/packages/ux-editor/src/components/config/componentSpecificContent/ComponentSpecificContent.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { ImageComponent } from './Image'; import { PanelComponent } from './Panel'; -import { ButtonComponent } from './Button'; import { AddressComponent } from './Address'; import { FileUploadComponent } from './FileUpload'; import type { IGenericEditComponent } from '../componentConfig'; @@ -14,16 +13,6 @@ export function ComponentSpecificContent({ layoutName, }: IGenericEditComponent) { switch (component.type) { - case ComponentType.NavigationButtons: - case ComponentType.Button: - return ( - - ); - case ComponentType.AddressComponent: return ( { - handleComponentChange({ - ...component, - textResourceBindings: { - ...component.textResourceBindings, - altTextImg: altTextImg, - }, - }); - }; - const handleSourceChange = (src: any) => { const updatedComponent = { ...component }; updatedComponent.image.src = src; @@ -82,17 +71,6 @@ export const ImageComponent = ({ )} /> - -
- { it('should render the component', async () => { @@ -19,7 +20,11 @@ describe('EditCodeList', () => { .mockImplementation(() => Promise.resolve(optionListIdsMock)), }, }); - expect(await screen.findByText('Bytt til egendefinert kodeliste')).toBeInTheDocument(); + expect( + await screen.findByText( + textMock('ux_editor.properties_panel.options.codelist_switch_to_custom'), + ), + ).toBeInTheDocument(); }); it('should render the component when optionListIds is undefined', async () => { @@ -31,7 +36,11 @@ describe('EditCodeList', () => { }, }); - expect(await screen.findByText('Bytt til egendefinert kodeliste')).toBeInTheDocument(); + expect( + await screen.findByText( + textMock('ux_editor.properties_panel.options.codelist_switch_to_custom'), + ), + ).toBeInTheDocument(); }); it('should call onChange when option list changes', async () => { diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditCodeList.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditCodeList/EditCodeList.tsx similarity index 74% rename from frontend/packages/ux-editor/src/components/config/editModal/EditCodeList.tsx rename to frontend/packages/ux-editor/src/components/config/editModal/EditCodeList/EditCodeList.tsx index 3a034f064bf..120a100b75a 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditCodeList.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditCodeList/EditCodeList.tsx @@ -1,13 +1,14 @@ -import React, { useState } from 'react'; -import { LegacySelect, Textfield } from '@digdir/design-system-react'; -import type { IGenericEditComponent } from '../componentConfig'; -import { useOptionListIdsQuery } from '../../../hooks/queries/useOptionListIdsQuery'; +import React, { useEffect, useState } from 'react'; +import { Alert, LegacySelect, Textfield } from '@digdir/design-system-react'; +import type { IGenericEditComponent } from '../../componentConfig'; +import { useOptionListIdsQuery } from '../../../../hooks/queries/useOptionListIdsQuery'; import { useTranslation, Trans } from 'react-i18next'; import { StudioButton, StudioSpinner } from '@studio/components'; import { ErrorMessage } from '@digdir/design-system-react'; import { altinnDocsUrl } from 'app-shared/ext-urls'; -import { FormField } from '../../FormField'; +import { FormField } from '../../../FormField'; import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams'; +import classes from './EditCodeList.module.css'; export function EditCodeList({ component, handleComponentChange }: IGenericEditComponent) { const { t } = useTranslation(); @@ -22,6 +23,11 @@ export function EditCodeList({ component, handleComponentChange }: IGenericEditC }); }; + useEffect(() => { + if (!optionListIds) return; + setUseCustomCodeList(optionListIds?.length === 0); + }, [optionListIds]); + return (
{isPending ? ( @@ -31,7 +37,7 @@ export function EditCodeList({ component, handleComponentChange }: IGenericEditC {error instanceof Error ? error.message : t('ux_editor.modal_properties_error_message')} ) : optionListIds?.length === 0 ? ( - {t('ux_editor.modal_properties_no_options_found_message')} + {t('ux_editor.modal_properties_no_options_found_message')} ) : ( <>

@@ -39,9 +45,14 @@ export function EditCodeList({ component, handleComponentChange }: IGenericEditC variant='tertiary' onClick={() => setUseCustomCodeList(!useCustomCodeList)} size='small' + className={classes.customOrStaticButton} > - {optionListIds?.length > 0 && useCustomCodeList && <>Bytt til statisk kodeliste} - {!useCustomCodeList && <>Bytt til egendefinert kodeliste} + {optionListIds?.length > 0 && useCustomCodeList && ( + <>{t('ux_editor.properties_panel.options.codelist_switch_to_static')} + )} + {!useCustomCodeList && ( + <>{t('ux_editor.properties_panel.options.codelist_switch_to_custom')} + )}

diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditCodeList/index.ts b/frontend/packages/ux-editor/src/components/config/editModal/EditCodeList/index.ts new file mode 100644 index 00000000000..951b645ee4a --- /dev/null +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditCodeList/index.ts @@ -0,0 +1 @@ +export { EditCodeList } from './EditCodeList'; diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.module.css b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.module.css index 4935b9a81bd..f7b28d29607 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.module.css +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.module.css @@ -15,3 +15,10 @@ flex-direction: column; gap: 24px; } + +.codeListSwitchWrapper { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.test.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.test.tsx index 172b3ddb0f3..9f27fcf3020 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.test.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { screen } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import { EditOptions } from './EditOptions'; import { renderWithMockStore } from '../../../testing/mocks'; @@ -27,13 +27,15 @@ describe('EditOptions', () => { it('should render', () => { renderEditOptions(); expect( - screen.getByText(textMock('ux_editor.modal_properties_add_radio_button_options')), + screen.getByText(textMock('ux_editor.properties_panel.options.use_code_list_label')), ).toBeInTheDocument(); }); it('should show code list input by default when neither options nor optionId are set', async () => { renderEditOptions(); - expect(screen.getByText(textMock('ux_editor.modal_add_options_codelist'))).toBeInTheDocument(); + expect( + screen.getByText(textMock('ux_editor.modal_properties_custom_code_list_id')), + ).toBeInTheDocument(); }); it('should show manual input when component has options defined', async () => { @@ -41,9 +43,15 @@ describe('EditOptions', () => { component: { ...mockComponent, options: [{ label: 'option1', value: 'option1' }], + optionsId: undefined, }, }); - expect(screen.getByText(textMock('ux_editor.modal_add_options_manual'))).toBeInTheDocument(); + expect( + screen.getByText(textMock('ux_editor.properties_panel.options.add_options')), + ).toBeInTheDocument(); + expect( + screen.getByText(textMock('ux_editor.modal_radio_button_increment') + ' 1'), + ).toBeInTheDocument(); }); it('should show code list input when component has optionsId defined', async () => { @@ -53,6 +61,69 @@ describe('EditOptions', () => { optionsId: 'optionsId', }, }); - expect(screen.getByText(textMock('ux_editor.modal_add_options_manual'))).toBeInTheDocument(); + expect( + screen.getByText(textMock('ux_editor.modal_properties_custom_code_list_id')), + ).toBeInTheDocument(); + }); + + it('should switch to manual input when toggling codelist switch off', async () => { + const handleComponentChange = jest.fn(); + renderEditOptions({ handleComponentChange }); + const switchElement = screen.getByRole('checkbox'); + await act(() => switchElement.click()); + expect(handleComponentChange).toHaveBeenCalledWith({ ...mockComponent, options: [] }); + }); + + it('should switch to codelist input when toggling codelist switch on', async () => { + const handleComponentChange = jest.fn(); + renderEditOptions({ + handleComponentChange, + component: { + ...mockComponent, + options: [{ label: 'option1', value: 'option1' }], + optionsId: undefined, + }, + }); + const switchElement = screen.getByRole('checkbox'); + await act(() => switchElement.click()); + expect(handleComponentChange).toHaveBeenCalledWith({ ...mockComponent, optionsId: '' }); + }); + + it('should update component options when adding new option', async () => { + const handleComponentChange = jest.fn(); + renderEditOptions({ + handleComponentChange, + component: { + ...mockComponent, + options: [{ label: 'option1', value: 'option1' }], + }, + }); + const addOptionButton = screen.getByRole('button', { + name: textMock('ux_editor.modal_new_option'), + }); + await act(() => addOptionButton.click()); + expect(handleComponentChange).toHaveBeenCalledWith({ + ...mockComponent, + options: expect.arrayContaining([ + { label: 'option1', value: 'option1' }, + expect.objectContaining({ label: '' }), + ]), + }); + }); + + it('should update component options when removing option', async () => { + const handleComponentChange = jest.fn(); + renderEditOptions({ + handleComponentChange, + component: { + ...mockComponent, + options: [{ label: 'option1', value: 'option1' }], + }, + }); + const removeOptionButton = screen.getByRole('button', { + name: textMock('ux_editor.properties_panel.options.remove_option'), + }); + await act(() => removeOptionButton.click()); + expect(handleComponentChange).toHaveBeenCalledWith({ ...mockComponent, options: [] }); }); }); diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.tsx index f95120d0fd0..0f1be15fe13 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions.tsx @@ -1,10 +1,17 @@ import React, { useEffect, useRef, useState } from 'react'; import type { IOption } from '../../../types/global'; -import { Fieldset, Radio, Textfield } from '@digdir/design-system-react'; +import { + Fieldset, + Heading, + HelpText, + Paragraph, + Switch, + Textfield, +} from '@digdir/design-system-react'; import classes from './EditOptions.module.css'; import type { IGenericEditComponent } from '../componentConfig'; import { EditCodeList } from './EditCodeList'; -import { PlusIcon, XMarkIcon } from '@navikt/aksel-icons'; +import { PlusIcon, TrashIcon } from '@navikt/aksel-icons'; import { TextResource } from '../../TextResource'; import { useText, useComponentErrorMessage } from '../../../hooks'; import { addOptionToComponent, generateRandomOption } from '../../../utils/component'; @@ -28,6 +35,17 @@ export enum SelectedOptionsType { Unknown = '', } +const optionsTypeMap = { + [SelectedOptionsType.CodeList]: { + propertyName: 'optionsId', + defaultValue: '', + }, + [SelectedOptionsType.Manual]: { + propertyName: 'options', + defaultValue: [], + }, +}; + const getSelectedOptionsType = (codeListId: string, options: IOption[]): SelectedOptionsType => { if (options?.length) { return SelectedOptionsType.Manual; @@ -39,6 +57,7 @@ export function EditOptions({ editFormId, component, handleComponentChange, + renderOptions, }: ISelectionEditComponentProvidedProps) { const previousEditFormId = useRef(editFormId); const initialSelectedOptionType = getSelectedOptionsType(component.optionsId, component.options); @@ -54,22 +73,19 @@ export function EditOptions({ } }, [editFormId, initialSelectedOptionType]); - const handleOptionsTypeChange = (value: SelectedOptionsType) => { - setSelectedOptionsType(value); - if (value === SelectedOptionsType.CodeList) { - delete component.options; - handleComponentChange({ - ...component, - optionsId: '', - }); - } - if (value === SelectedOptionsType.Manual) { - delete component.optionsId; - handleComponentChange({ - ...component, - options: [], - }); - } + const handleOptionsTypeChange = (oldOptionsType: SelectedOptionsType) => { + const newOptionsType = + oldOptionsType === SelectedOptionsType.CodeList + ? SelectedOptionsType.Manual + : SelectedOptionsType.CodeList; + + setSelectedOptionsType(newOptionsType); + delete component[optionsTypeMap[oldOptionsType].propertyName]; + + handleComponentChange({ + ...component, + [optionsTypeMap[newOptionsType].propertyName]: optionsTypeMap[newOptionsType].defaultValue, + }); }; const handleUpdateOptionLabel = (index: number) => (id: string) => { @@ -99,84 +115,93 @@ export function EditOptions({ }); }; - const handleAddOption = () => + const handleAddOption = () => { handleComponentChange(addOptionToComponent(component, generateRandomOption())); + }; + + if (renderOptions?.onlyCodeListOptions) { + return ; + } return ( <> - - - {t('ux_editor.modal_add_options_codelist')} - - {t('ux_editor.modal_add_options_manual')} - +
+ handleOptionsTypeChange(selectedOptionsType)} + > + {t('ux_editor.properties_panel.options.use_code_list_label')} + + + {t('ux_editor.properties_panel.options.use_code_list_helptext')} + +
{selectedOptionsType === SelectedOptionsType.CodeList && ( )} {selectedOptionsType === SelectedOptionsType.Manual && ( - ( -
- {component.options?.map((option, index) => { - const updateValue = (e: any) => handleUpdateOptionValue(index, e); - const removeItem = () => handleRemoveOption(index); - const key = `${option.label}-${index}`; // Figure out a way to remove index from key. - const optionTitle = `${ - component.type === 'RadioButtons' - ? t('ux_editor.modal_radio_button_increment') - : t('ux_editor.modal_check_box_increment') - } ${index + 1}`; - return ( -
-
-
-
- -
- + + {t('ux_editor.properties_panel.options.add_options')} + + ( +
+ {component.options?.map((option, index) => { + const updateValue = (e: any) => handleUpdateOptionValue(index, e); + const removeItem = () => handleRemoveOption(index); + const key = `${option.label}-${index}`; // Figure out a way to remove index from key. + const optionTitle = `${ + component.type === 'RadioButtons' + ? t('ux_editor.modal_radio_button_increment') + : t('ux_editor.modal_check_box_increment') + } ${index + 1}`; + return ( +
+
+
+
+ +
+ +
-
-
-
-
- } - onClick={removeItem} - variant='tertiary' - size='small' - /> + +
+
+ } + onClick={removeItem} + variant='tertiary' + size='small' + title={t('ux_editor.properties_panel.options.remove_option')} + /> +
-
- ); - })} -
- )} - /> + ); + })} +
+ )} + /> + )} {selectedOptionsType === SelectedOptionsType.Manual && ( diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBinding.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBinding.tsx index fb71990e25b..454d93e83ef 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBinding.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBinding.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import type { IGenericEditComponent } from '../componentConfig'; import { TextResource } from '../../TextResource'; import type { TranslationKey } from 'language/type'; import type { IAppState } from '../../../types/global'; import { useTranslation } from 'react-i18next'; +import type { EditTextResourceBindingBase } from './EditTextResourceBindings'; -export interface EditTextResourceBindingProps extends IGenericEditComponent { +export interface EditTextResourceBindingProps extends EditTextResourceBindingBase { textKey: string; labelKey: TranslationKey; descriptionKey?: TranslationKey; diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBindings.test.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBindings.test.tsx index 91b847e864c..e36725c31f3 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBindings.test.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBindings.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import type { EditTextResourceBindingsProps } from './EditTextResourceBindings'; import { EditTextResourceBindings } from './EditTextResourceBindings'; -import { act, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { renderHookWithMockStore, renderWithMockStore, @@ -14,7 +14,6 @@ import { ComponentType } from 'app-shared/types/ComponentType'; import type { ITextResourcesWithLanguage } from 'app-shared/types/global'; import { useTextResourcesQuery } from 'app-shared/hooks/queries/useTextResourcesQuery'; import type { FormComponent } from '../../../types/FormComponent'; -import userEvent from '@testing-library/user-event'; // Test data: const org = 'org'; @@ -24,7 +23,9 @@ describe('EditTextResourceBindings component', () => { const mockComponent: FormComponent = { id: 'test-id', textResourceBindings: { - test: 'test-text', + title: 'test-title-text-id', + description: 'test-desc-text-id', + help: 'test-help-text-id', }, type: ComponentType.Input, itemType: 'COMPONENT', @@ -33,69 +34,61 @@ describe('EditTextResourceBindings component', () => { const textResources: ITextResource[] = [ { - id: 'test-text', - value: 'This is a test', + id: 'test-title-text-id', + value: 'This is a test title', + }, + { + id: 'test-desc-text-id', + value: 'This is a test description ', + }, + { + id: 'test-help-text-id', + value: 'This is a test help text', }, ]; test('that it renders with expected text resource binding keys', async () => { const textResourceBindingKeys = ['title', 'description', 'help']; await renderEditTextResourceBindingsComponent({ textResourceBindingKeys }); - const label = screen.getByText( - textMock(`ux_editor.modal_properties_textResourceBindings_test`), - ); - const text = screen.getByText('This is a test'); - expect(label).toBeInTheDocument(); - expect(text).toBeInTheDocument(); + + textResourceBindingKeys.forEach((key) => { + expect( + screen.getByText(textMock(`ux_editor.modal_properties_textResourceBindings_${key}`)), + ).toBeInTheDocument(); + }); + + const labelText = screen.getByText('This is a test title'); + expect(labelText).toBeInTheDocument(); + const descText = screen.getByText('This is a test description'); + expect(descText).toBeInTheDocument(); + const helpText = screen.getByText('This is a test help text'); + expect(helpText).toBeInTheDocument(); }); - test('that it renders no text resource bindings if none are added', async () => { + test('that it renders no text resource bindings if no keys are provided', async () => { await renderEditTextResourceBindingsComponent({ + textResourceBindingKeys: [], component: { ...mockComponent, textResourceBindings: {} }, }); - const titleLabel = screen.queryByText( - textMock(`ux_editor.modal_properties_textResourceBindings_title`), - ); - expect(titleLabel).not.toBeInTheDocument(); + expect( + screen.queryByText(textMock('ux_editor.modal_properties_textResourceBindings_title')), + ).not.toBeInTheDocument(); const searchTextButton = screen.queryByRole('button', { name: textMock('general.search') }); expect(searchTextButton).not.toBeInTheDocument(); }); - test('that it renders the combobox for selecting text resource binding keys to add', async () => { + test('that it renders empty text resource bindings if component has no text resource bindings', async () => { const textResourceBindingKeys = ['title', 'description', 'help']; - await renderEditTextResourceBindingsComponent({ textResourceBindingKeys }); - const selectTextResourcesCombobox = screen.getByRole('combobox', { - name: textMock('ux_editor.text_resource_bindings.add_label'), - }); - expect(selectTextResourcesCombobox).toBeInTheDocument(); - }); - - test('that the combobox for selecting text resource binding keys only contains keys that are not already added', async () => { - const textResourceBindingKeys = ['title', 'description', 'help']; - await renderEditTextResourceBindingsComponent({ textResourceBindingKeys }); - const selectTextResourcesCombobox = screen.getByRole('combobox', { - name: textMock('ux_editor.text_resource_bindings.add_label'), + await renderEditTextResourceBindingsComponent({ + textResourceBindingKeys, + component: { ...mockComponent, textResourceBindings: {} }, }); - await act(() => userEvent.click(selectTextResourcesCombobox)); // eslint-disable-line testing-library/no-unnecessary-act - let options = screen.getAllByRole('option'); - expect(options.length).toBe(3); - - await act(() => userEvent.click(options[0])); // eslint-disable-line testing-library/no-unnecessary-act - await act(() => userEvent.click(selectTextResourcesCombobox)); // eslint-disable-line testing-library/no-unnecessary-act - options = screen.getAllByRole('option'); - expect(options.length).toBe(2); - }); - - test('that it does not render the combobox for selecting text resource binding keys when all available keys are added', async () => { - const textResourceBindingKeys = ['test']; - await renderEditTextResourceBindingsComponent({ textResourceBindingKeys }); - const selectTextResourcesCombobox = screen.queryByRole('combobox', { - name: textMock('ux_editor.text_resource_bindings.add_label'), + textResourceBindingKeys.forEach((key) => { + expect( + screen.getByText(textMock(`ux_editor.modal_properties_textResourceBindings_${key}_add`)), + ).toBeInTheDocument(); }); - const addTextResourceButton = screen.queryByRole('button', { name: textMock('general.add') }); - expect(selectTextResourcesCombobox).not.toBeInTheDocument(); - expect(addTextResourceButton).not.toBeInTheDocument(); }); const waitForData = async () => { diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBindings.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBindings.tsx index 5a204eb86cf..7eb7314df82 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBindings.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditTextResourceBindings.tsx @@ -1,14 +1,17 @@ -import React, { useMemo } from 'react'; -import type { IGenericEditComponent } from '../componentConfig'; +import React from 'react'; import { EditTextResourceBinding } from './EditTextResourceBinding'; import classes from './EditTextResourceBindings.module.css'; -import type { TranslationKey } from 'language/type'; -import { useTranslation } from 'react-i18next'; -import { LegacySelect } from '@digdir/design-system-react'; - -export type TextResourceBindingKey = 'description' | 'title' | 'help' | 'body'; +import type { FormContainer } from '../../../types/FormContainer'; +import type { FormComponent } from '../../../types/FormComponent'; + +export interface EditTextResourceBindingBase { + editFormId?: string; + component: FormComponent | FormContainer; + handleComponentChange: (component: FormComponent | FormContainer) => void; + layoutName?: string; +} -export interface EditTextResourceBindingsProps extends IGenericEditComponent { +export interface EditTextResourceBindingsProps extends EditTextResourceBindingBase { textResourceBindingKeys: string[]; } @@ -17,57 +20,18 @@ export const EditTextResourceBindings = ({ handleComponentChange, textResourceBindingKeys, }: EditTextResourceBindingsProps) => { - const { t } = useTranslation(); - - const [keysSet, setKeysSet] = React.useState(Object.keys(component.textResourceBindings || {})); - - const keysToAdd = useMemo( - () => textResourceBindingKeys.filter((key) => !keysSet.includes(key)), - [keysSet, textResourceBindingKeys], - ); - - const handleAddKey = (key: string) => { - setKeysSet([...keysSet, key]); - handleComponentChange({ - ...component, - textResourceBindings: { - ...component.textResourceBindings, - [key]: '', - }, - }); - }; - - const handleRemoveKey = (key: string) => { - setKeysSet((prevKeysSet) => prevKeysSet.filter((k) => k !== key)); - }; - return (
- {keysSet.map((key: string) => ( + {textResourceBindingKeys.map((key: string) => ( handleRemoveKey(key)} textKey={key} - labelKey={`ux_editor.modal_properties_textResourceBindings_${key}` as TranslationKey} - placeholderKey={ - `ux_editor.modal_properties_textResourceBindings_${key}_add` as TranslationKey - } + labelKey={`ux_editor.modal_properties_textResourceBindings_${key}` as any} + placeholderKey={`ux_editor.modal_properties_textResourceBindings_${key}_add` as any} /> ))} - {keysToAdd.length > 0 && ( -
- ({ - label: t(`ux_editor.modal_properties_textResourceBindings_${key}`), - value: key, - }))} - onChange={(value) => handleAddKey(value)} - label={t('ux_editor.text_resource_bindings.add_label')} - /> -
- )}
); }; diff --git a/frontend/packages/ux-editor/src/hooks/mutations/useUpdateFormComponentOrderMutation.test.ts b/frontend/packages/ux-editor/src/hooks/mutations/useUpdateFormComponentOrderMutation.test.ts index 928e9222801..2e20a7b0342 100644 --- a/frontend/packages/ux-editor/src/hooks/mutations/useUpdateFormComponentOrderMutation.test.ts +++ b/frontend/packages/ux-editor/src/hooks/mutations/useUpdateFormComponentOrderMutation.test.ts @@ -45,6 +45,7 @@ describe('useUpdateFormComponentOrderMutation', () => { data: expect.objectContaining({ layout: [ expect.objectContaining({ id: container1IdMock }), + expect.objectContaining({ id: 'ComponentWithOptionsMock' }), expect.objectContaining({ id: component2IdMock }), expect.objectContaining({ id: component1IdMock }), ], diff --git a/frontend/packages/ux-editor/src/hooks/queries/useComponentSchemaQuery.ts b/frontend/packages/ux-editor/src/hooks/queries/useComponentSchemaQuery.ts index 81501796a6e..c750703cbfc 100644 --- a/frontend/packages/ux-editor/src/hooks/queries/useComponentSchemaQuery.ts +++ b/frontend/packages/ux-editor/src/hooks/queries/useComponentSchemaQuery.ts @@ -4,6 +4,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { addSchemas, dereferenceSchema } from 'app-shared/utils/formValidationUtils'; import { componentSchemaMocks } from '../../testing/componentSchemaMocks'; import { QueryKey } from 'app-shared/types/QueryKey'; +import { useLayoutSchemaQuery } from './useLayoutSchemaQuery'; export interface UseComponentSchemaQueryResult { queryResults: { @@ -13,9 +14,10 @@ export interface UseComponentSchemaQueryResult { // Currently use local mocks rather than fetching from CDN. This is because the CDN schemas are not ready to use, and // we also have made some modifications locally to the schemas. -// When the schemas are available on CDN, we can remove the mocks and use the querys instead. +// When the schemas are available on CDN, we can remove the mocks and use the queries instead. export const useComponentSchemaQuery = (component: string): UseQueryResult => { const queryClient = useQueryClient(); + useLayoutSchemaQuery(); // Ensure that the layout schema is fetched before the component schema return useQuery({ queryKey: [QueryKey.FormComponent, component], diff --git a/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts b/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts index ba5013341af..f7a6053c413 100644 --- a/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts +++ b/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts @@ -1,6 +1,6 @@ import AlertSchema from './schemas/json/component/Alert.schema.v1.json'; // TODO: Add schemas for the commented out components (https://github.com/Altinn/altinn-studio/issues/10868): -// import AccordionSchema from './schemas/json/component/Accordion.schema.v1.json'; +import AccordionSchema from './schemas/json/component/Accordion.schema.v1.json'; // import AccordionGroupSchema from './schemas/json/component/AccordionGroup.schema.v1.json'; import ActionButtonSchema from './schemas/json/component/ActionButton.schema.v1.json'; import AddressComponentSchema from './schemas/json/component/AddressComponent.schema.v1.json'; @@ -36,9 +36,10 @@ import SummarySchema from './schemas/json/component/Summary.schema.v1.json'; import TextAreaSchema from './schemas/json/component/TextArea.schema.v1.json'; export const componentSchemaMocks = { - Alert: AlertSchema, + Accordion: AccordionSchema, ActionButton: ActionButtonSchema, AddressComponent: AddressComponentSchema, + Alert: AlertSchema, AttachmentList: AttachmentListSchema, Button: ButtonSchema, ButtonGroup: ButtonGroupSchema, diff --git a/frontend/packages/ux-editor/src/testing/layoutMock.ts b/frontend/packages/ux-editor/src/testing/layoutMock.ts index cd412a6030d..74cfa0e93ed 100644 --- a/frontend/packages/ux-editor/src/testing/layoutMock.ts +++ b/frontend/packages/ux-editor/src/testing/layoutMock.ts @@ -30,6 +30,14 @@ export const component2Mock: FormComponent = { itemType: 'COMPONENT', pageIndex: null, }; +export const componentWithOptionsMock: FormComponent = { + id: 'ComponentWithOptionsMock', + type: ComponentType.Checkboxes, + itemType: 'COMPONENT', + pageIndex: null, + optionsId: '', + propertyPath: 'definitions/radioAndCheckboxComponents', +}; export const container1IdMock = 'Container-1'; export const customRootPropertiesMock: KeyValuePairs = { someCustomRootProp: 'someStringValue', @@ -43,6 +51,7 @@ export const layoutMock: IInternalLayout = { components: { [component1IdMock]: component1Mock, [component2IdMock]: component2Mock, + ComponentWithOptionsMock: componentWithOptionsMock, }, containers: { [baseContainerIdMock]: { @@ -61,7 +70,7 @@ export const layoutMock: IInternalLayout = { }, }, order: { - [baseContainerIdMock]: [container1IdMock], + [baseContainerIdMock]: [container1IdMock, 'ComponentWithOptionsMock'], [container1IdMock]: [component1IdMock, component2IdMock], }, customRootProperties: customRootPropertiesMock, @@ -85,6 +94,11 @@ export const layout1Mock: ExternalFormLayout = { id: component2IdMock, type: component2TypeMock, }, + { + id: 'ComponentWithOptionsMock', + type: ComponentType.Checkboxes, + optionsId: '', + }, ], ...customDataPropertiesMock, },