From db0fbcde053d5f3f1f17b49360127c1290d22454 Mon Sep 17 00:00:00 2001 From: JamalAlabdullah <90609090+JamalAlabdullah@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:48:05 +0100 Subject: [PATCH] feat: added validation when creating new datamodel in subform (#14233) --- .../CreateNewSubformSection.test.tsx | 50 +++++++++++++++++++ .../CreateNewSubformSection.tsx | 41 +++++++++++---- .../SubformDataModel.test.tsx | 9 ++-- .../SubformDataModel.tsx | 23 +++++---- 4 files changed, 100 insertions(+), 23 deletions(-) diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.test.tsx index 98e31569c4c..1f0c5df9a78 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.test.tsx @@ -125,6 +125,31 @@ describe('CreateNewSubformLayoutSet ', () => { expect(saveButton).toBeDisabled(); }); + it('Toggles the save button disabling based on data model input validation', async () => { + const user = userEvent.setup(); + renderCreateNewSubformLayoutSet({}); + + const input = screen.getByRole('textbox'); + await user.type(input, 'NewSubform'); + + const saveButton = screen.getByRole('button', { name: textMock('general.save') }); + + const displayDataModelInput = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.create_new_data_model'), + }); + await user.click(displayDataModelInput); + + const dataModelInput = screen.getByRole('textbox', { + name: textMock('ux_editor.component_properties.subform.create_new_data_model_label'), + }); + await user.type(dataModelInput, 'æøå'); + expect(saveButton).toBeDisabled(); + + await user.clear(dataModelInput); + await user.type(dataModelInput, 'datamodel'); + expect(saveButton).not.toBeDisabled(); + }); + it('enables save button when both input and data model is valid', async () => { const user = userEvent.setup(); renderCreateNewSubformLayoutSet({}); @@ -159,6 +184,31 @@ describe('CreateNewSubformLayoutSet ', () => { await user.type(dataModelInput, 'datamodel'); expect(saveButton).not.toBeDisabled(); }); + + it('Should toggle ErrorMessage visibility based on input validity', async () => { + const user = userEvent.setup(); + renderCreateNewSubformLayoutSet({}); + + const input = screen.getByRole('textbox'); + await user.type(input, 'NewSubform'); + + const displayDataModelInput = screen.getByRole('button', { + name: textMock('ux_editor.component_properties.subform.create_new_data_model'), + }); + await user.click(displayDataModelInput); + + const dataModelInput = screen.getByRole('textbox', { + name: textMock('ux_editor.component_properties.subform.create_new_data_model_label'), + }); + + await user.type(dataModelInput, 'new'); + const errorMessage = screen.getByText(textMock('schema_editor.error_reserved_keyword')); + expect(errorMessage).toBeInTheDocument(); + + await user.clear(dataModelInput); + await user.type(dataModelInput, 'datamodel'); + expect(errorMessage).not.toBeInTheDocument(); + }); }); type RenderCreateNewSubformLayoutSetProps = { diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.tsx index c6efe918455..6a3699740e5 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/CreateNewSubformSection.tsx @@ -12,6 +12,11 @@ import { SubformDataModel } from './SubformDataModel'; import { CreateNewSubformButtons } from './CreateNewSubformButtons'; import { SubformInstructions } from './SubformInstructions'; import { useCreateSubform } from '@altinn/ux-editor/hooks/useCreateSubform'; +import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; +import { useAppMetadataModelIdsQuery } from 'app-shared/hooks/queries/useAppMetadataModelIdsQuery'; +import { useAppMetadataQuery } from 'app-shared/hooks/queries'; +import { extractDataTypeNamesFromAppMetadata } from 'app-development/features/dataModelling/SchemaEditorWithToolbar/TopToolbar/utils/validationUtils'; +import { useValidateSchemaName } from 'app-shared/hooks/useValidateSchemaName'; type CreateNewSubformSectionProps = { layoutSets: LayoutSets; @@ -33,26 +38,36 @@ export const CreateNewSubformSection = ({ }: CreateNewSubformSectionProps): React.ReactElement => { const { t } = useTranslation(); const { validateLayoutSetName } = useValidateLayoutSetName(); - const [nameError, setNameError] = useState(); - const [newDataModel, setNewDataModel] = useState(''); + const [newSubformNameError, setNewSubformNameError] = useState(); const [selectedDataModel, setSelectedDataModel] = useState(''); const [displayDataModelInput, setDisplayDataModelInput] = useState(false); const { createSubform, isPendingNewSubformMutation } = useCreateSubform(); + const [isNewDataModelFieldEmpty, setIsNewDataModelFieldEmpty] = useState(true); + + const { org, app } = useStudioEnvironmentParams(); + const { data: dataModelIds } = useAppMetadataModelIdsQuery(org, app, false); + const { data: appMetadata } = useAppMetadataQuery(org, app); + const dataTypeNames = extractDataTypeNamesFromAppMetadata(appMetadata); + const { + validateName, + nameError: dataModelNameError, + setNameError: setDataModelNameError, + } = useValidateSchemaName(dataModelIds, dataTypeNames); const handleSubformName = (subformName: string) => { const subformNameValidation = validateLayoutSetName(subformName, layoutSets); - setNameError(subformNameValidation); + setNewSubformNameError(subformNameValidation); }; const handleCloseButton = () => { if (displayDataModelInput) { - setNewDataModel(''); + setDataModelNameError(''); + setIsNewDataModelFieldEmpty(true); setDisplayDataModelInput(false); } else { setShowCreateSubformCard(false); } }; - const handleCreateSubformSubmit = (e: React.FormEvent): void => { e.preventDefault(); const formData: FormData = new FormData(e.currentTarget); @@ -68,8 +83,11 @@ export const CreateNewSubformSection = ({ }); }; - const hasInvalidSubformName = nameError === undefined || Boolean(nameError); - const hasInvalidDataModel = displayDataModelInput ? !newDataModel : !selectedDataModel; + const hasInvalidSubformName = newSubformNameError === undefined || Boolean(newSubformNameError); + const hasInvalidDataModel = displayDataModelInput + ? Boolean(dataModelNameError) || isNewDataModelFieldEmpty + : !selectedDataModel; + const disableSaveButton = hasInvalidSubformName || hasInvalidDataModel; return ( handleSubformName(e.target.value)} - error={nameError} + error={newSubformNameError} /> diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.test.tsx index 6c9807f9bc8..5e9cb0ef9fc 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.test.tsx @@ -8,8 +8,6 @@ import { useAppMetadataModelIdsQuery } from 'app-shared/hooks/queries/useAppMeta jest.mock('app-shared/hooks/queries/useAppMetadataModelIdsQuery'); -const user = userEvent.setup(); - const mockDataModelIds = ['dataModelId1', 'dataModelId2']; (useAppMetadataModelIdsQuery as jest.Mock).mockReturnValue({ data: mockDataModelIds }); @@ -41,6 +39,7 @@ describe('SubformDataModel', () => { }); it('Calls setDataModel when selecting an option', async () => { + const user = userEvent.setup(); const setSelectedDataModel = jest.fn(); renderSubformDataModelSelect({ setSelectedDataModel }); @@ -53,6 +52,7 @@ describe('SubformDataModel', () => { }); it('Should call setDisplayDataModelInput true when clicking create new data model button', async () => { + const user = userEvent.setup(); const setDisplayDataModelInput = jest.fn(); renderSubformDataModelSelect({ setDisplayDataModelInput }); const displayDataModelInput = screen.getByRole('button', { @@ -75,9 +75,12 @@ describe('SubformDataModel', () => { const defaultProps: SubformDataModelProps = { setDisplayDataModelInput: jest.fn(), - setNewDataModel: jest.fn(), displayDataModelInput: false, setSelectedDataModel: jest.fn(), + dataModelIds: mockDataModelIds, + validateName: jest.fn(), + dataModelNameError: '', + setIsTextfieldEmpty: jest.fn(), }; const renderSubformDataModelSelect = (props: Partial = {}) => { diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.tsx index 32508217bb1..47d7212440b 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformSection/SubformDataModel.tsx @@ -2,30 +2,32 @@ import React from 'react'; import { StudioTextfield, StudioNativeSelect, StudioProperty } from '@studio/components'; import { LinkIcon } from '@studio/icons'; import { useTranslation } from 'react-i18next'; -import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; -import { useAppMetadataModelIdsQuery } from 'app-shared/hooks/queries/useAppMetadataModelIdsQuery'; import classes from './SubformDataModel.module.css'; export type SubformDataModelProps = { setDisplayDataModelInput: (setDisplayDataModelInput: boolean) => void; - setNewDataModel: (dataModelId: string) => void; displayDataModelInput: boolean; setSelectedDataModel: (dataModelId: string) => void; + dataModelIds?: string[]; + validateName: (name: string) => void; + dataModelNameError: string; + setIsTextfieldEmpty: (isEmpty: boolean) => void; }; export const SubformDataModel = ({ setDisplayDataModelInput, setSelectedDataModel, - setNewDataModel, displayDataModelInput, + dataModelIds, + validateName, + dataModelNameError, + setIsTextfieldEmpty, }: SubformDataModelProps): React.ReactElement => { const { t } = useTranslation(); - const { org, app } = useStudioEnvironmentParams(); - const { data: dataModelIds } = useAppMetadataModelIdsQuery(org, app, false); - const handleDataModel = (dataModelId: string) => { - // TODO: https://github.com/Altinn/altinn-studio/issues/14184 - setNewDataModel(dataModelId); + const handleNewDataModel = (dataModelId: string) => { + validateName(dataModelId); + setIsTextfieldEmpty(dataModelId === ''); }; const handleDisplayInput = () => { @@ -59,7 +61,8 @@ export const SubformDataModel = ({ name='newSubformDataModel' label={t('ux_editor.component_properties.subform.create_new_data_model_label')} size='sm' - onChange={(e) => handleDataModel(e.target.value)} + onChange={(e) => handleNewDataModel(e.target.value)} + error={dataModelNameError} /> ) : (