Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

12180 default section for component config #12232

Merged
20 commits merged into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,7 @@
"ux_editor.component_help_text.Summary": "Komponent som viser informasjon som oppsummering. Brukes ofte på slutten av et skjema for å vise brukeren hva som er fylt ut.",
"ux_editor.component_help_text.TextArea": "Stort tekstfelt benyttes når brukeren skal fylle inn en lengre beskrivelse.",
"ux_editor.component_help_text.default": "Mer informasjon om denne komponenten vil komme på et senere tidspunkt.",
"ux_editor.component_help_text_general_title": "Åpne hjelpetekst for komponenten",
"ux_editor.component_properties.action": "Aksjon",
"ux_editor.component_properties.align": "Plassering*",
"ux_editor.component_properties.attribution": "Opphav",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import { act, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { textMock } from '../../../../../testing/mocks/i18nMock';
import { FormContext } from '../../containers/FormContext';
import {
component1IdMock,
component1Mock,
container1IdMock,
layoutMock,
} from '../../testing/layoutMock';
import { container1IdMock, layoutMock } from '../../testing/layoutMock';
import type { IAppDataState } from '../../features/appData/appDataReducers';
import type { ITextResourcesState } from '../../features/appData/textResources/textResourcesSlice';
import { renderWithMockStore, renderHookWithMockStore } from '../../testing/mocks';
Expand Down Expand Up @@ -60,33 +55,6 @@ describe('ContentTab', () => {
expect(formContextProviderMock.debounceSave).toHaveBeenCalledTimes(4);
});
});

describe('when editing a component', () => {
const props = {
formId: component1IdMock,
form: { ...component1Mock, dataModelBindings: {} },
};

it('should render the component', async () => {
jest.spyOn(console, 'error').mockImplementation(); // Silence error from Select component
await render({ props });
expect(
screen.getByText(textMock('ux_editor.modal_properties_component_change_id')),
).toBeInTheDocument();
});

it('should auto-save when updating a field', async () => {
await render({ props });

const idInput = screen.getByLabelText(
textMock('ux_editor.modal_properties_component_change_id'),
);
await act(() => user.type(idInput, 'test'));

expect(formContextProviderMock.handleUpdate).toHaveBeenCalledTimes(4);
expect(formContextProviderMock.debounceSave).toHaveBeenCalledTimes(4);
});
});
});

const waitForData = async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import { Properties } from './Properties';
import { render as rtlRender, act, screen, waitFor } from '@testing-library/react';
import { act, screen, waitFor } from '@testing-library/react';
import { mockUseTranslation } from '../../../../../testing/mocks/i18nMock';
import { FormContext } from '../../containers/FormContext';
import userEvent from '@testing-library/user-event';
import { formContextProviderMock } from '../../testing/formContextMocks';

const user = userEvent.setup();
import { component1Mock, component1IdMock } from '../../testing/layoutMock';
import { renderWithProviders } from '../../testing/mocks';

// Test data:
const contentText = 'Innhold';
Expand Down Expand Up @@ -39,15 +39,44 @@ jest.mock('./Calculations', () => ({
jest.mock('react-i18next', () => ({ useTranslation: () => mockUseTranslation(texts) }));

describe('Properties', () => {
describe('Default config', () => {
it('hides the properties header when the form is undefined', () => {
renderProperties({ form: undefined });

const heading = screen.queryByRole('heading', { level: 2 });
expect(heading).not.toBeInTheDocument();
});

it('saves the component when changes are made in the properties header', async () => {
const user = userEvent.setup();
renderProperties({ form: component1Mock, formId: component1IdMock });

const heading = screen.getByRole('heading', {
name: component1Mock.type,
level: 2,
});
expect(heading).toBeInTheDocument();

const textbox = screen.getByRole('textbox', {
name: 'ux_editor.modal_properties_component_change_id',
});

await act(() => user.type(textbox, '2'));
expect(formContextProviderMock.handleUpdate).toHaveBeenCalledTimes(1);
expect(formContextProviderMock.debounceSave).toHaveBeenCalledTimes(1);
});
});

describe('Content', () => {
it('Closes content on load', () => {
render();
renderProperties();
const button = screen.queryByRole('button', { name: contentText });
expect(button).toHaveAttribute('aria-expanded', 'false');
});

it('Toggles content when clicked', async () => {
render();
const user = userEvent.setup();
renderProperties();
const button = screen.queryByRole('button', { name: contentText });
await act(() => user.click(button));
expect(button).toHaveAttribute('aria-expanded', 'true');
Expand All @@ -56,7 +85,7 @@ describe('Properties', () => {
});

it('Opens content when a component is selected', async () => {
const { rerender } = render();
const { rerender } = renderProperties();
rerender(getComponent({ formId: 'test' }));
const button = screen.queryByRole('button', { name: contentText });
await waitFor(() => expect(button).toHaveAttribute('aria-expanded', 'true'));
Expand All @@ -65,13 +94,14 @@ describe('Properties', () => {

describe('Dynamics', () => {
it('Closes dynamics on load', () => {
render();
renderProperties();
const button = screen.queryByRole('button', { name: dynamicsText });
expect(button).toHaveAttribute('aria-expanded', 'false');
});

it('Toggles dynamics when clicked', async () => {
render();
const user = userEvent.setup();
renderProperties();
const button = screen.queryByRole('button', { name: dynamicsText });
await act(() => user.click(button));
expect(button).toHaveAttribute('aria-expanded', 'true');
Expand All @@ -80,7 +110,8 @@ describe('Properties', () => {
});

it('Shows new dynamics by default', async () => {
const { rerender } = render();
const user = userEvent.setup();
const { rerender } = renderProperties();
rerender(getComponent({ formId: 'test' }));
const dynamicsButton = screen.queryByRole('button', { name: dynamicsText });
await act(() => user.click(dynamicsButton));
Expand All @@ -91,13 +122,14 @@ describe('Properties', () => {

describe('Calculations', () => {
it('Closes calculations on load', () => {
render();
renderProperties();
const button = screen.queryByRole('button', { name: calculationsText });
expect(button).toHaveAttribute('aria-expanded', 'false');
});

it('Toggles calculations when clicked', async () => {
render();
const user = userEvent.setup();
renderProperties();
const button = screen.queryByRole('button', { name: calculationsText });
await act(() => user.click(button));
expect(button).toHaveAttribute('aria-expanded', 'true');
Expand All @@ -108,7 +140,7 @@ describe('Properties', () => {

it('Renders accordion', () => {
const formIdMock = 'test-id';
render({ formId: formIdMock });
renderProperties({ formId: formIdMock });
expect(screen.getByText(contentText)).toBeInTheDocument();
expect(screen.getByText(dynamicsText)).toBeInTheDocument();
expect(screen.getByText(calculationsText)).toBeInTheDocument();
Expand All @@ -129,5 +161,5 @@ const getComponent = (formContextProps: Partial<FormContext> = {}) => (
</FormContext.Provider>
);

const render = (formContextProps: Partial<FormContext> = {}) =>
rtlRender(getComponent(formContextProps));
const renderProperties = (formContextProps: Partial<FormContext> = {}) =>
renderWithProviders(getComponent(formContextProps));
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { Accordion } from '@digdir/design-system-react';
import { useFormContext } from '../../containers/FormContext';
import classes from './Properties.module.css';
import { Dynamics } from './Dynamics';
import { PropertiesHeader } from './PropertiesHeader';
import { isContainer } from '../../utils/formItemUtils';

export const Properties = () => {
const { t } = useTranslation();
const { formId } = useFormContext();
const { formId, form, handleUpdate, debounceSave } = useFormContext();
const formIdRef = React.useRef(formId);

const [openList, setOpenList] = React.useState<string[]>([]);
Expand All @@ -31,6 +33,16 @@ export const Properties = () => {

return (
<div className={classes.root}>
{form && !isContainer(form) && (
<PropertiesHeader
form={form}
formId={formId}
handleComponentUpdate={async (updatedComponent) => {
handleUpdate(updatedComponent);
debounceSave(formId, updatedComponent);
}}
/>
)}
<Accordion color='subtle'>
<Accordion.Item open={openList.includes('content')}>
<Accordion.Header onHeaderClick={() => toggleOpen('content')}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from 'react';
import { act, screen } from '@testing-library/react';
import { DataModelBindingRow, type DataModelBindingRowProps } from './DataModelBindingRow';
import { FormContext } from '../../../../containers/FormContext';
import userEvent from '@testing-library/user-event';
import { formContextProviderMock } from '../../../../testing/formContextMocks';
import { component1Mock, component1IdMock } from '../../../../testing/layoutMock';
import { renderWithProviders } from '../../../../testing/mocks';
import { textMock } from '../../../../../../../testing/mocks/i18nMock';

const mockSchema = {
properties: {
dataModelBindings: {
properties: {
simpleBinding: {
description: 'Description for simpleBinding',
},
},
},
},
};

const mockSchemaUndefined = {
properties: {
dataModelBindings: {
properties: undefined,
},
},
};

const mockHandleComponentUpdate = jest.fn();

const defaultProps: DataModelBindingRowProps = {
schema: mockSchema,
component: component1Mock,
formId: component1IdMock,
handleComponentUpdate: mockHandleComponentUpdate,
};

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

it('renders EditDataModelBindings component when schema is present', () => {
renderProperties({ form: component1Mock, formId: component1IdMock });

const datamodelButton = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_data_model_link'),
});
expect(datamodelButton).toBeInTheDocument();
});

it('does not render EditDataModelBindings component when schema.properties is undefined', () => {
renderProperties(
{ form: component1Mock, formId: component1IdMock },
{ schema: mockSchemaUndefined },
);

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

it('calls handleComponentUpdate when EditDataModelBindings component is updated', async () => {
const user = userEvent.setup();
renderProperties({ form: component1Mock, formId: component1IdMock });

const datamodelButton = screen.getByRole('button', {
name: textMock('ux_editor.modal_properties_data_model_link'),
});
expect(
screen.queryByRole('button', { name: textMock('general.delete') }),
).not.toBeInTheDocument();

await act(() => user.click(datamodelButton));

const deleteButton = screen.getByRole('button', {
name: textMock('general.delete'),
});
expect(deleteButton).toBeInTheDocument();

await act(() => user.click(deleteButton));
expect(mockHandleComponentUpdate).toHaveBeenCalledTimes(1);
});
});

const renderProperties = (
formContextProps: Partial<FormContext> = {},
props: Partial<DataModelBindingRowProps> = {},
) => {
return renderWithProviders(
<FormContext.Provider
value={{
...formContextProviderMock,
...formContextProps,
}}
>
<DataModelBindingRow {...defaultProps} {...props} />
</FormContext.Provider>,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { EditDataModelBindings } from '../../../config/editModal/EditDataModelBindings';
import type { FormComponent } from '../../../../types/FormComponent';

export type DataModelBindingRowProps = {
schema: any;
component: FormComponent;
formId: string;
handleComponentUpdate: (component: FormComponent) => void;
};

export const DataModelBindingRow = ({
schema,
component,
formId,
handleComponentUpdate,
}: DataModelBindingRowProps): React.JSX.Element => {
const { dataModelBindings } = schema.properties;

return (
dataModelBindings?.properties && (
<>
{Object.keys(dataModelBindings?.properties).map((propertyKey: string) => {
return (
<EditDataModelBindings
key={`${component.id}-datamodel-${propertyKey}`}
component={component}
handleComponentChange={handleComponentUpdate}
editFormId={formId}
helpText={dataModelBindings?.properties[propertyKey]?.description}
renderOptions={{
key: propertyKey,
label: propertyKey !== 'simpleBinding' ? propertyKey : undefined,
TomasEng marked this conversation as resolved.
Show resolved Hide resolved
}}
/>
);
})}
</>
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DataModelBindingRow } from './DataModelBindingRow';
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import React from 'react';
import { idExists } from '../../../utils/formLayoutUtils';
import { idExists } from '../../../../utils/formLayoutUtils';
import { useTranslation } from 'react-i18next';
import { useSelectedFormLayout } from '../../../hooks';
import type { FormComponent } from '../../../types/FormComponent';
import { FormField } from '../../FormField';
import { useSelectedFormLayout } from '../../../../hooks';
import type { FormComponent } from '../../../../types/FormComponent';
import { FormField } from '../../../FormField';
import { Textfield } from '@digdir/design-system-react';

export interface IEditComponentId {
export interface EditComponentIdRowProps {
handleComponentUpdate: (component: FormComponent) => void;
component: FormComponent;
helpText?: string;
}
export const EditComponentId = ({
export const EditComponentIdRow = ({
component,
handleComponentUpdate,
helpText,
}: IEditComponentId) => {
}: EditComponentIdRowProps) => {
const { components, containers } = useSelectedFormLayout();
const { t } = useTranslation();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EditComponentIdRow } from './EditComponentIdRow';
Loading
Loading