Skip to content

Commit

Permalink
Add hook for layoutsetname validation
Browse files Browse the repository at this point in the history
  • Loading branch information
standeren committed Sep 6, 2024
1 parent 4e170a2 commit e3dc397
Show file tree
Hide file tree
Showing 16 changed files with 134 additions and 114 deletions.
10 changes: 5 additions & 5 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -646,9 +646,9 @@
"process_editor.configuration_panel_header_help_text_signing": "Du bruker oppgaven Signering (signing) når du vil at sluttbrukerne skal bekrefte med signatur.",
"process_editor.configuration_panel_header_help_text_title": "Informasjon om valgt oppgave",
"process_editor.configuration_panel_id_label": "ID:",
"process_editor.configuration_panel_layout_set_name": "Navn på sidegruppe: ",
"process_editor.configuration_panel_layout_set_name_label": "Navn på sidegruppe",
"process_editor.configuration_panel_layout_set_id_not_unique": "Navnet på sidegruppen må være unikt",
"process_editor.configuration_panel_layout_set_name": "Utforming: ",
"process_editor.configuration_panel_layout_set_name_label": "Navn på utforming",
"process_editor.configuration_panel_missing_task": "Oppgave",
"process_editor.configuration_panel_name_label": "Navn: ",
"process_editor.configuration_panel_no_data_model_to_select": "Du må ha tilgjengelige datamodeller du kan knytte til dette steget.",
Expand Down Expand Up @@ -676,9 +676,9 @@
"process_editor.not_found_diagram_heading": "Det er ingen tilgjengelige diagramdata",
"process_editor.not_found_process_error_message": "Det finnes ingen prosess definert innenfor BPMN, Du kan sjekke om prosessen finnes i BPMN-filen.",
"process_editor.not_found_process_heading": "Det er ingen tilgjengelig prosess",
"process_editor.recommended_action.new_name": "Gi oppgaven et navn",
"process_editor.recommended_action.new_name_description": "Du finner lettere igjen oppgaven på Lage-siden, hvis du gir den et eget navn. Hvis du velger Hopp over, får oppgaven en tilfeldig navn. Du kan endre navnet senere.",
"process_editor.recommended_action.new_name_label": "Navn på oppgaven",
"process_editor.recommended_action.new_name": "Gi utformingen et navn",
"process_editor.recommended_action.new_name_description": "Du finner lettere igjen utformingen til oppgaven på Lage-siden, hvis du gir den et eget navn. Hvis du velger 'Hopp over', får oppgaven et tilfeldig navn. Du kan endre navnet senere.",
"process_editor.recommended_action.new_name_label": "Navn på utforming",
"process_editor.save_bpmn_xml_error": "Noe gikk galt da prosessen skulle lagres.",
"process_editor.sequence_flow_configuration_add_new_rule": "Lag en ny logikkregel",
"process_editor.sequence_flow_configuration_panel_explanation": "Med Flytkontroll-verktøyet kan du kontrollere flyten ut av en gateway ved hjelp av uttrykk.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { StudioRecommendedNextAction } from './StudioRecommendedNextAction';
export type { StudioRecommendedNextActionProps } from './StudioRecommendedNextAction';
export { StudioRecommendedNextActionContextProvider } from './context/StudioRecommendedNextActionContext';
export { StudioRecommendedNextActionContext } from './context/StudioRecommendedNextActionContext';
export { useStudioRecommendedNextActionContext } from './context/useStudioRecommendedNextActionContext';
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const signingTasks = [
},
];

jest.mock('../../../../utils/bpmnModeler/StudioModeler', () => {
jest.mock('../../../../../utils/bpmnModeler/StudioModeler', () => {
return {
StudioModeler: jest.fn().mockImplementation(() => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { StudioToggleableTextfield } from '@studio/components';
import { KeyVerticalIcon } from '@studio/icons';
import { useBpmnContext } from '../../../../contexts/BpmnContext';
import { useBpmnApiContext } from '@altinn/process-editor/contexts/BpmnApiContext';
import { getLayoutSetIdValidationErrorKey } from 'app-shared/utils/layoutSetsUtils';
import { useBpmnApiContext } from '../../../../contexts/BpmnApiContext';
import { Paragraph } from '@digdir/designsystemet-react';
import { useValidateLayoutSetName } from 'app-shared/hooks/useValidateLayoutSetName';

interface EditLayoutSetNameProps {
existingLayoutSetName: string;
Expand All @@ -15,26 +14,19 @@ export const EditLayoutSetName = ({
}: EditLayoutSetNameProps): React.ReactElement => {
const { t } = useTranslation();
const { layoutSets, mutateLayoutSetId } = useBpmnApiContext();
const { bpmnDetails } = useBpmnContext();
const { validateLayoutSetName } = useValidateLayoutSetName();

const handleOnLayoutSetNameBlur = (event: React.ChangeEvent<HTMLInputElement>): void => {
const newName = event.target.value;
if (newName === existingLayoutSetName) return;
mutateLayoutSetId({ layoutSetIdToUpdate: existingLayoutSetName, newLayoutSetId: newName });
};

const handleValidation = (newLayoutSetId: string): string => {
const validationResult = getLayoutSetIdValidationErrorKey(
layoutSets,
bpmnDetails.element.id,
newLayoutSetId,
);
return validationResult ? t(validationResult) : undefined;
};

return (
<StudioToggleableTextfield
customValidation={handleValidation}
customValidation={(newLayoutSetName: string) =>
validateLayoutSetName(newLayoutSetName, layoutSets)
}
inputProps={{
icon: <KeyVerticalIcon />,
label: t('process_editor.configuration_panel_layout_set_name_label'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,102 +1,116 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { RecommendedActionChangeName } from './RecommendedActionChangeName';
import { useBpmnContext } from '../../../../contexts/BpmnContext';
import { useValidateBpmnTaskId } from '../../../../hooks/useValidateBpmnId';
import { textMock } from '@studio/testing/mocks/i18nMock';
import userEvent from '@testing-library/user-event';
import { StudioRecommendedNextActionContextProvider } from '@studio/components';
import { StudioRecommendedNextActionContext } from '@studio/components';
import { BpmnApiContext, type BpmnApiContextProps } from '../../../../contexts/BpmnApiContext';
import { mockBpmnApiContextValue } from '../../../../../test/mocks/bpmnContextMock';
import { useValidateLayoutSetName } from 'app-shared/hooks/useValidateLayoutSetName';

jest.mock('../../../../contexts/BpmnContext', () => ({
useBpmnContext: jest.fn(),
}));

jest.mock('../../../../hooks/useValidateBpmnId', () => ({
useValidateBpmnTaskId: jest.fn(),
jest.mock('app-shared/hooks/useValidateLayoutSetName', () => ({
useValidateLayoutSetName: jest.fn(),
}));

jest.mock('../../../../utils/bpmnModeler/StudioModeler.ts', () => ({
StudioModeler: jest.fn().mockImplementation(() => ({
updateElementProperties: jest.fn(),
})),
}));
const removeActionMock = jest.fn();
const validateLayoutSetNameMock = jest.fn();

describe('RecommendedActionChangeName', () => {
const setBpmnDetails = jest.fn();
const validateBpmnTaskId = jest.fn();
const DEFAULT_ID = 'test_id';

beforeEach(() => {
(useBpmnContext as jest.Mock).mockReturnValue({
bpmnDetails: { id: DEFAULT_ID, element: { id: 'test_id' }, metadata: {} },
setBpmnDetails: setBpmnDetails,
modelerRef: { current: { get: () => ({ updateProperties: jest.fn() }) } },
});
(useValidateBpmnTaskId as jest.Mock).mockReturnValue({
validateBpmnTaskId: validateBpmnTaskId,
(useValidateLayoutSetName as jest.Mock).mockReturnValue({
validateLayoutSetName: validateLayoutSetNameMock,
});
jest.clearAllMocks();
});

it('calls validation on name input', async () => {
const user = userEvent.setup();
renderWithContext(<RecommendedActionChangeName />);
const newLayoutSetName = 'newName';
const mutateLayoutSetIdMock = jest.fn();
renderRecommendedActionChangeName({ mutateLayoutSetId: mutateLayoutSetIdMock });
const newNameInput = screen.getByRole('textbox', {
name: textMock('process_editor.recommended_action.new_name_label'),
});
await user.type(newNameInput, 'newName');
await user.type(newNameInput, newLayoutSetName);

expect(validateBpmnTaskId).toHaveBeenCalledWith('newName');
expect(validateLayoutSetNameMock).toHaveBeenCalledTimes(newLayoutSetName.length);
expect(validateLayoutSetNameMock).toHaveBeenCalledWith(newLayoutSetName, expect.any(Object));
});

it('calls saveNewName when save button is clicked with a valid name', async () => {
it('calls mutateLayoutSetId and removeAction when save button is clicked with a valid name', async () => {
const user = userEvent.setup();
renderWithContext(<RecommendedActionChangeName />);
const newLayoutSetName = 'newName';
const mutateLayoutSetIdMock = jest.fn();
renderRecommendedActionChangeName({ mutateLayoutSetId: mutateLayoutSetIdMock });
const newNameInput = screen.getByRole('textbox', {
name: textMock('process_editor.recommended_action.new_name_label'),
});
await user.type(newNameInput, 'newName');

await user.type(newNameInput, newLayoutSetName);
const saveButton = screen.getByRole('button', { name: textMock('general.save') });
fireEvent.submit(saveButton);
await user.click(saveButton);

expect(setBpmnDetails).toHaveBeenCalledWith(
expect.objectContaining({
id: 'newName',
}),
);
expect(mutateLayoutSetIdMock).toHaveBeenCalledTimes(1);
expect(mutateLayoutSetIdMock).toHaveBeenCalledWith({
layoutSetIdToUpdate: DEFAULT_ID,
newLayoutSetId: newLayoutSetName,
});
expect(removeActionMock).toHaveBeenCalledTimes(1);
});

it('calls saveNewName when pressing enter in input field', async () => {
it('calls mutateLayoutSetId and removeAction when pressing enter in input field', async () => {
const user = userEvent.setup();
renderWithContext(<RecommendedActionChangeName />);
const newLayoutSetName = 'newName';
const mutateLayoutSetIdMock = jest.fn();
renderRecommendedActionChangeName({ mutateLayoutSetId: mutateLayoutSetIdMock });
const newNameInput = screen.getByRole('textbox', {
name: textMock('process_editor.recommended_action.new_name_label'),
});
await user.type(newNameInput, 'newName{enter}');

expect(setBpmnDetails).toHaveBeenCalledWith(
expect.objectContaining({
id: 'newName',
}),
);
await user.type(newNameInput, `${newLayoutSetName}{enter}`);
expect(mutateLayoutSetIdMock).toHaveBeenCalledTimes(1);
expect(mutateLayoutSetIdMock).toHaveBeenCalledWith({
layoutSetIdToUpdate: DEFAULT_ID,
newLayoutSetId: newLayoutSetName,
});
expect(removeActionMock).toHaveBeenCalledTimes(1);
});

it('calls cancelAction when skip button is clicked', async () => {
it('calls removeAction, but not mutateLayoutSetId, when skip button is clicked', async () => {
const user = userEvent.setup();
renderWithContext(<RecommendedActionChangeName />);
const mutateLayoutSetIdMock = jest.fn();
renderRecommendedActionChangeName({ mutateLayoutSetId: mutateLayoutSetIdMock });

const skipButton = screen.getByRole('button', { name: textMock('general.skip') });
await user.click(skipButton);

expect(setBpmnDetails).not.toHaveBeenCalled();
expect(mutateLayoutSetIdMock).not.toHaveBeenCalled();
expect(removeActionMock).toHaveBeenCalledTimes(1);
});

const renderWithContext = (children) => {
const renderRecommendedActionChangeName = (
bpmnApiContextProps: Partial<BpmnApiContextProps> = {},
) => {
render(
<StudioRecommendedNextActionContextProvider>
{children}
</StudioRecommendedNextActionContextProvider>,
<BpmnApiContext.Provider value={{ ...mockBpmnApiContextValue, ...bpmnApiContextProps }}>
<StudioRecommendedNextActionContext.Provider
value={{
removeAction: removeActionMock,
shouldDisplayAction: jest.fn(),
addAction: jest.fn(),
}}
>
<RecommendedActionChangeName />
</StudioRecommendedNextActionContext.Provider>
</BpmnApiContext.Provider>,
);
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,19 @@ import {
import { KeyVerticalIcon } from '@studio/icons';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getLayoutSetIdValidationErrorKey } from 'app-shared/utils/layoutSetsUtils';
import { useBpmnApiContext } from '@altinn/process-editor/contexts/BpmnApiContext';
import { useValidateLayoutSetName } from 'app-shared/hooks/useValidateLayoutSetName';

export const RecommendedActionChangeName = (): React.ReactElement => {
const { bpmnDetails } = useBpmnContext();
const { layoutSets, mutateLayoutSetId } = useBpmnApiContext();
const { validateLayoutSetName } = useValidateLayoutSetName();
const { t } = useTranslation();
const { removeAction } = useStudioRecommendedNextActionContext();

const [newName, setNewName] = useState('');
const [newNameError, setNewNameError] = useState('');

const handleValidation = (newLayoutSetId: string): string => {
const validationResult = getLayoutSetIdValidationErrorKey(
layoutSets,
bpmnDetails.element.id,
newLayoutSetId,
);
return validationResult ? t(validationResult) : undefined;
};

const saveNewName = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (newNameError || newName === '') {
Expand Down Expand Up @@ -58,7 +50,7 @@ export const RecommendedActionChangeName = (): React.ReactElement => {
label={t('process_editor.recommended_action.new_name_label')}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewName(event.target.value);
setNewNameError(handleValidation(event.target.value));
setNewNameError(validateLayoutSetName(event.target.value, layoutSets));
}}
value={newName}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('EditTaskId', () => {
await user.tab();

expect(metadataFormRefMock.current).toEqual(
expect.objectContaining({ taskIdChange: { newId: newId, oldId: 'testId' } }),
expect.objectContaining({ taskIdChange: { newId: newId, oldId: mockBpmnDetails.id } }),
);
expect(setBpmnDetailsMock).toHaveBeenCalledTimes(1);
});
Expand Down Expand Up @@ -177,7 +177,7 @@ describe('EditTaskId', () => {
);

await user.clear(input);
await user.type(input, 'testId');
await user.type(input, mockBpmnDetails.id);
await user.tab();

expect(metadataFormRefMock.current).toBeUndefined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {
mockBpmnApiContextValue,
mockBpmnContextValue,
} from '../../../../../test/mocks/bpmnContextMock';
import { getMockBpmnElementForTask, mockBpmnDetails } from '../../../../../test/mocks/bpmnDetailsMock';
import {
getMockBpmnElementForTask,
mockBpmnDetails,
} from '../../../../../test/mocks/bpmnDetailsMock';

const existingDataTypes = [
{ id: 'dataType1', name: 'Name 1' },
Expand Down Expand Up @@ -63,7 +66,7 @@ const signingTasks = [
},
];

jest.mock('../../../utils/bpmnModeler/StudioModeler', () => {
jest.mock('../../../../utils/bpmnModeler/StudioModeler', () => {
return {
StudioModeler: jest.fn().mockImplementation(() => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const signingTasks = [
},
];

jest.mock('../../../../utils/bpmnModeler/StudioModeler', () => {
jest.mock('../../../../../utils/bpmnModeler/StudioModeler', () => {
return {
StudioModeler: jest.fn().mockImplementation(() => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('CreateCustomReceiptForm', () => {
renderCreateCustomReceiptForm();

const layoutSetInput = screen.getByLabelText(
textMock('process_editor.configuration_panel_layout_set_name_label'),
textMock('process_editor.configuration_panel_custom_receipt_textfield_label'),
);
const newId: string = 'newLayoutSetId';
await user.type(layoutSetInput, newId);
Expand Down Expand Up @@ -114,7 +114,7 @@ describe('CreateCustomReceiptForm', () => {
});

const inputField = screen.getByLabelText(
textMock('process_editor.configuration_panel_layout_set_name_label'),
textMock('process_editor.configuration_panel_custom_receipt_textfield_label'),
);

await user.type(inputField, 'a');
Expand Down Expand Up @@ -160,7 +160,7 @@ describe('CreateCustomReceiptForm', () => {
const invalidFormatLayoutSetName: string = 'Receipt/';

const inputField = screen.getByLabelText(
textMock('process_editor.configuration_panel_layout_set_name_label'),
textMock('process_editor.configuration_panel_custom_receipt_textfield_label'),
);

await user.type(inputField, invalidFormatLayoutSetName);
Expand All @@ -182,7 +182,7 @@ describe('CreateCustomReceiptForm', () => {
});

const layoutSetInput = screen.getByLabelText(
textMock('process_editor.configuration_panel_layout_set_name_label'),
textMock('process_editor.configuration_panel_custom_receipt_textfield_label'),
);
await user.type(layoutSetInput, 'newLayoutSetId');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { type CustomReceiptType } from '../../../../../types/CustomReceiptType';
import { PROTECTED_TASK_NAME_CUSTOM_RECEIPT } from 'app-shared/constants';
import { type LayoutSetConfig } from 'app-shared/types/api/LayoutSetsResponse';
import { SelectCustomReceiptDataModelId } from './SelectCustomReceiptDataModelId';
import { getLayoutSetIdValidationErrorKey } from 'app-shared/utils/layoutSetsUtils';
import { useValidateLayoutSetName } from 'app-shared/hooks/useValidateLayoutSetName';

export type CreateCustomReceiptFormProps = {
onCloseForm: () => void;
Expand All @@ -19,6 +19,7 @@ export const CreateCustomReceiptForm = ({
const { t } = useTranslation();
const { allDataModelIds, layoutSets, existingCustomReceiptLayoutSetId, addLayoutSet } =
useBpmnApiContext();
const { validateLayoutSetName } = useValidateLayoutSetName();

const allDataModelIdsEmpty: boolean = allDataModelIds.length === 0;

Expand Down Expand Up @@ -73,12 +74,7 @@ export const CreateCustomReceiptForm = ({
};

const handleValidateLayoutSetId = (event: React.ChangeEvent<HTMLInputElement>) => {
const validationResult = getLayoutSetIdValidationErrorKey(
layoutSets,
existingCustomReceiptLayoutSetId,
event.target.value,
);
setLayoutSetError(validationResult ? t(validationResult) : null);
setLayoutSetError(validateLayoutSetName(event.target.value, layoutSets));
};

return (
Expand Down
Loading

0 comments on commit e3dc397

Please sign in to comment.