Skip to content

Commit

Permalink
11230 editdatamodelbindings (#11333)
Browse files Browse the repository at this point in the history
* edit data model binding
  • Loading branch information
JamalAlabdullah authored Oct 17, 2023
1 parent 64d10f8 commit e3af968
Show file tree
Hide file tree
Showing 10 changed files with 519 additions and 52 deletions.
3 changes: 2 additions & 1 deletion frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1339,7 +1339,8 @@
"ux_editor.modal_properties_component_id_not_unique_error": "Komponenten må ha en unik ID",
"ux_editor.modal_properties_component_id_not_valid": "Komponent-ID kan kun bestå av engelske bokstaver, tall og '-' tegnet.",
"ux_editor.modal_properties_custom_code_list_id": "ID til egendefinert kodeliste",
"ux_editor.modal_properties_data_model_helper": "Lenke til datamodell",
"ux_editor.modal_properties_data_model_helper": "Velg en datamodell",
"ux_editor.modal_properties_data_model_link": "Legg til en datamodell",
"ux_editor.modal_properties_description": "Beskrivelse:",
"ux_editor.modal_properties_description_add": "Legg til beskrivelse",
"ux_editor.modal_properties_description_helper": "Søk etter beskrivelse",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.container {
margin-top: var(--fds-spacing-3);
display: flex;
align-items: center;
min-height: var(--fds-component-mode-height-medium);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import { act, render as rtlRender, screen } from '@testing-library/react';
import type { InputActionWrapperProps } from './InputActionWrapper';
import { InputActionWrapper } from './InputActionWrapper';
import { mockUseTranslation } from '../../../../../testing/mocks/i18nMock';
import userEvent from '@testing-library/user-event';

jest.mock('react-i18next', () => ({ useTranslation: () => mockUseTranslation() }));

const user = userEvent.setup();

const mockProps: InputActionWrapperProps = {
mode: 'editMode',
onEditClick: jest.fn(),
onDeleteClick: jest.fn(),
onSaveClick: jest.fn(),
children: <input />,
};

describe('InputActionWrapper', () => {
it('renders save and delete buttons when mode is (editMode)', async () => {
await rtlRender(<InputActionWrapper {...mockProps} mode='editMode' />);
expect(screen.getByLabelText('general.save')).toBeInTheDocument();
expect(screen.getByLabelText('general.delete')).toBeInTheDocument();
});

it('renders edit button when mode is (hoverMode)', async () => {
await rtlRender(<InputActionWrapper {...mockProps} mode='hoverMode' />);
expect(screen.getByLabelText('general.edit')).toBeInTheDocument();
});

it('renders delete button when mode is (hoverMode)', async () => {
await rtlRender(<InputActionWrapper {...mockProps} mode='hoverMode' />);
expect(screen.getByLabelText('general.delete')).toBeInTheDocument();
});

it('does not render save button when mode is (hoverMode)', async () => {
await rtlRender(<InputActionWrapper {...mockProps} mode='hoverMode' />);
expect(screen.queryByLabelText('general.save')).not.toBeInTheDocument();
});

it('does not renders edit button when mode is (editMode)', async () => {
await rtlRender(<InputActionWrapper {...mockProps} mode='editMode' />);
expect(screen.queryByLabelText('general.edit')).not.toBeInTheDocument();
});

it('renders delete button when mode is (editMode)', async () => {
await rtlRender(<InputActionWrapper {...mockProps} mode='editMode' />);
expect(screen.getByLabelText('general.delete')).toBeInTheDocument();
});

it('triggers save click on save button click', async () => {
rtlRender(<InputActionWrapper {...mockProps} />);
const saveButton = screen.getByLabelText('general.save');
await act(() => user.click(saveButton));
expect(mockProps.onSaveClick).toBeCalledTimes(1);
});

it('triggers delete click on delete button click', async () => {
rtlRender(<InputActionWrapper {...mockProps} />);
const deleteButton = screen.getByLabelText('general.delete');
await act(() => user.click(deleteButton));
expect(mockProps.onDeleteClick).toBeCalledTimes(1);
});

it('check that handleActionClick is called when edit button is clicked', async () => {
rtlRender(<InputActionWrapper {...mockProps} mode='hoverMode' />);
const editButton = screen.getByLabelText('general.edit');
await act(() => user.click(editButton));
expect(mockProps.onEditClick).toBeCalledTimes(1);
});

it('check that handleHover is called when onMouseOver is called ', async () => {
rtlRender(<InputActionWrapper {...mockProps} mode='standBy' />);
const input = screen.getByRole('textbox');
await act(() => user.hover(input));
expect(screen.getByLabelText('general.edit')).toBeInTheDocument();
});

it('check that handleBlur is called when onMouseLeave is called ', async () => {
rtlRender(<InputActionWrapper {...mockProps} mode='standBy' />);
const input = screen.getByRole('textbox');
await act(() => user.hover(input));
expect(screen.getByLabelText('general.edit')).toBeInTheDocument();
await act(() => user.unhover(input));
expect(screen.queryByLabelText('general.edit')).not.toBeInTheDocument();
});

it('check that handleFocus is called when onFocus is called ', async () => {
rtlRender(<InputActionWrapper {...mockProps} mode='standBy' />);
const input = screen.getByRole('textbox');
await act(() => user.hover(input));
expect(screen.getByLabelText('general.edit')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useState } from 'react';
import { CheckmarkIcon, TrashIcon, PencilWritingIcon } from '@altinn/icons';
import { useText } from '../../../../ux-editor/src/hooks/index';
import { Button, ButtonProps } from '@digdir/design-system-react';
import classes from './InputActionWrapper.module.css';

type AvailableAction = 'edit' | 'save' | 'delete';
export type ActionGroup = 'editMode' | 'hoverMode' | 'standBy';

const actionGroupMap: Record<ActionGroup, AvailableAction[]> = {
editMode: ['save', 'delete'],
hoverMode: ['edit', 'delete'],
standBy: [],
};

const actionToIconMap: Record<AvailableAction, React.ReactNode> = {
edit: <PencilWritingIcon />,
save: <CheckmarkIcon />,
delete: <TrashIcon />,
};

export type InputActionWrapperProps = {
children: React.ReactElement;
mode?: ActionGroup;
onEditClick: () => void;
onDeleteClick: () => void;
onSaveClick: () => void;
};

export const InputActionWrapper = ({
mode,
children,
onEditClick,
onDeleteClick,
onSaveClick,
...rest
}: InputActionWrapperProps): JSX.Element => {
const t = useText();
const [actions, setActions] = useState<AvailableAction[]>(actionGroupMap[mode || 'standBy']);

const handleFocus = (): void => {
setActions(actionGroupMap['editMode']);
};

const handleBlur = (): void => {
setActions(actionGroupMap['standBy']);
};

const handleMouseLeave = (): void => {
const isEditMode = actions.includes('save');
if (isEditMode) return;
handleBlur();
};

const handleActionClick = (action: AvailableAction): void => {
switch (action) {
case 'delete':
onDeleteClick();
break;
case 'save':
onSaveClick();
handleBlur();
break;
case 'edit':
onEditClick();
setActions(actionGroupMap['editMode']);
default:
break;
}
};

const handleHover = (): void => {
const isInStandByMode = mode === 'standBy';
if (isInStandByMode) {
setActions(actionGroupMap['hoverMode']);
}
};

const actionToAriaLabelMap: Record<AvailableAction, string> = {
edit: t('general.edit'),
delete: t('general.delete'),
save: t('general.save'),
};

const actionToColorMap: Record<AvailableAction, ButtonProps['color']> = {
edit: 'first',
save: 'success',
delete: 'danger',
};

return (
<div className={classes.container} onMouseOver={handleHover} onMouseLeave={handleMouseLeave}>
{React.cloneElement(children, {
...rest,
onFocus: handleFocus,
})}
{actions.map((action) => (
<Button
variant='quiet'
size='medium'
color={actionToColorMap[action]}
key={action}
onClick={() => handleActionClick(action)}
aria-label={actionToAriaLabelMap[action]}
>
{actionToIconMap[action]}
</Button>
))}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InputActionWrapper } from './InputActionWrapper';
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ describe('EditFormComponent', () => {
'ux_editor.modal_configure_read_only': 'checkbox',
};

const linkIcon = screen.getByText(/ux_editor.modal_properties_data_model_link/i);
await waitFor(async () => {
await userEvent.click(linkIcon);
});

Object.keys(labels).map(async (label) =>
expect(await screen.findByRole(labels[label], { name: label })),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import React from 'react';
import {
FormComponentConfig,
FormComponentConfigProps,
} from './FormComponentConfig';
import { FormComponentConfig, FormComponentConfigProps } from './FormComponentConfig';
import { renderWithMockStore } from '../../testing/mocks';
import { componentMocks } from '../../testing/componentMocks';
import InputSchema from '../../testing/schemas/json/component/Input.schema.v1.json';
import { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import { textMock } from '../../../../../testing/mocks/i18nMock';

describe('FormComponentConfig', () => {
it('should render expected components', () => {
it('should render expected components', async () => {
render({});
expect(
screen.getByText(textMock('ux_editor.modal_properties_component_change_id'))
screen.getByText(textMock('ux_editor.modal_properties_component_change_id')),
).toBeInTheDocument();
['title', 'description', 'help'].forEach((key) => {
['title', 'description', 'help'].forEach(async (key) => {
expect(
screen.getByText(textMock(`ux_editor.modal_properties_textResourceBindings_${key}`))
screen.getByText(textMock(`ux_editor.modal_properties_textResourceBindings_${key}`)),
).toBeInTheDocument();

expect(
screen.getByText(textMock('ux_editor.modal_properties_data_model_helper'))
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(textMock('ux_editor.modal_properties_data_model_link')),
).toBeInTheDocument();
});

[
'readOnly',
Expand All @@ -39,7 +38,7 @@ describe('FormComponentConfig', () => {
'formatting',
].forEach(async (propertyKey) => {
expect(
await screen.findByText(textMock(`ux_editor.component_properties.${propertyKey}`))
await screen.findByText(textMock(`ux_editor.component_properties.${propertyKey}`)),
).toBeInTheDocument();
});
});
Expand All @@ -64,7 +63,7 @@ describe('FormComponentConfig', () => {
},
});
expect(
screen.getByText(textMock('ux_editor.edit_component.unsupported_properties_message'))
screen.getByText(textMock('ux_editor.edit_component.unsupported_properties_message')),
).toBeInTheDocument();
expect(screen.getByText('unsupportedProperty')).toBeInTheDocument();
});
Expand All @@ -78,17 +77,17 @@ describe('FormComponentConfig', () => {
properties: {
...InputSchema.properties,
children: {
type: 'string'
}
type: 'string',
},
},
},
},
});
expect(
screen.getByText(textMock('ux_editor.edit_component.unsupported_properties_message'))
screen.getByText(textMock('ux_editor.edit_component.unsupported_properties_message')),
).toBeInTheDocument();
expect(screen.getByText('children')).toBeInTheDocument();
})
});

it('should not render list of unsupported properties if hideUnsupported is true', () => {
render({
Expand All @@ -109,7 +108,7 @@ describe('FormComponentConfig', () => {
},
});
expect(
screen.queryByText(textMock('ux_editor.edit_component.unsupported_properties_message'))
screen.queryByText(textMock('ux_editor.edit_component.unsupported_properties_message')),
).not.toBeInTheDocument();
expect(screen.queryByText('unsupportedProperty')).not.toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.datamodelLink {
display: flex;
align-items: center;
}

.linkIcon {
font-size: 1.5rem;
}

.SelectDataModelComponent {
margin-top: -25px;
width: 100%;
}

.linkedDatamodelContainer {
margin-top: 25px;
display: flex;
align-items: center;
gap: var(--fds-spacing-1);
}

.selectedOption {
gap: var(--fds-spacing-1);
width: 50%;
}
Loading

0 comments on commit e3af968

Please sign in to comment.