Skip to content

Commit

Permalink
12180 default section for component config (#12232)
Browse files Browse the repository at this point in the history
* initial commit

* adding header stuff

* test

* Moving datamodel button

* Addig component for handling edit id

* Moving the content

* deleting console.log

* removing broken tests

* adding one test

* adding properties header tests

* writing more tests

* fixing feecback from PR
  • Loading branch information
WilliamThorenfeldt authored Feb 9, 2024
1 parent 97fef76 commit e6b8382
Show file tree
Hide file tree
Showing 22 changed files with 401 additions and 128 deletions.
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,
}}
/>
);
})}
</>
)
);
};
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

0 comments on commit e6b8382

Please sign in to comment.