From 002b6e4868decdd02a15ce71eb73620fdc16efcf Mon Sep 17 00:00:00 2001 From: Lars <74791975+lassopicasso@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:45:49 +0100 Subject: [PATCH] fix: 404 error when creating subform (#13992) Co-authored-by: Nina Kylstad --- .../Subform/CreateSubformWrapper.test.tsx | 32 ++++++++++- .../Elements/Subform/CreateSubformWrapper.tsx | 34 +++++++++--- .../CreateNewSubformLayoutSet.test.tsx | 55 ++++++++++++------- .../CreateNewSubformLayoutSet.tsx | 22 +++++--- .../src/hooks/useCreateSubform.test.ts | 20 ++++--- .../ux-editor/src/hooks/useCreateSubform.ts | 38 +++++++++---- 6 files changed, 146 insertions(+), 55 deletions(-) diff --git a/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.test.tsx b/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.test.tsx index 79568f610c4..28a621369f5 100644 --- a/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.test.tsx +++ b/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.test.tsx @@ -81,9 +81,39 @@ describe('CreateSubformWrapper', () => { }); await user.click(confirmButton); expect(addLayoutSet).toHaveBeenCalledWith(org, app, subformName, { - layoutSetConfig: { id: subformName, type: 'subform' }, + layoutSetConfig: { id: subformName, type: 'subform', dataType: '' }, + taskType: undefined, }); }); + + it('should show spinner when adding subform', async () => { + const user = userEvent.setup(); + + const addLayoutSetMock = jest.fn().mockImplementation( + () => + new Promise((resolve) => { + setTimeout(resolve, 100); + }), + ); + + renderCreateSubformWrapper(undefined, { addLayoutSet: addLayoutSetMock }); + + const createSubformButton = screen.getByRole('button', { + name: textMock('ux_editor.create.subform'), + }); + await user.click(createSubformButton); + + const input = screen.getByRole('textbox'); + await user.type(input, subformName); + + const confirmButton = screen.getByRole('button', { + name: textMock('ux_editor.create.subform.confirm_button'), + }); + await user.click(confirmButton); + + const spinner = await screen.findByText(textMock('general.loading')); + expect(spinner).toBeInTheDocument(); + }); }); const renderCreateSubformWrapper = ( diff --git a/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.tsx b/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.tsx index 44ff25007ef..aee50d5ad6d 100644 --- a/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.tsx +++ b/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { StudioButton, StudioPopover, StudioTextfield } from '@studio/components'; +import { StudioButton, StudioPopover, StudioSpinner, StudioTextfield } from '@studio/components'; import { PlusIcon } from '@studio/icons'; import { useTranslation } from 'react-i18next'; import { useValidateLayoutSetName } from 'app-shared/hooks/useValidateLayoutSetName'; @@ -21,21 +21,38 @@ export const CreateSubformWrapper = ({ const [nameError, setNameError] = useState(''); const { t } = useTranslation(); const { validateLayoutSetName } = useValidateLayoutSetName(); - const { createSubform } = useCreateSubform(); + const { createSubform, isPendingLayoutSetMutation } = useCreateSubform(); const onCreateConfirmClick = () => { setCreateNewOpen(false); - createSubform({ layoutSetName: newSubformName, onSubformCreated }); + onSubformCreated(newSubformName); }; - const onNameChange = (subformName: string) => { + const handleNameChange = (subformName: string) => { const subformNameValidation = validateLayoutSetName(subformName, layoutSets); setNameError(subformNameValidation); setNewSubformName(subformName); }; + const handleCreateSubform = () => { + createSubform({ + layoutSetName: newSubformName, + onSubformCreated: onCreateConfirmClick, + //setting datatype to empty string as this createSubform area is only temporary and will be removed in a later PR + dataType: '', + }); + }; + + const createSubformButtonContent = isPendingLayoutSetMutation ? ( + + ) : ( + t('ux_editor.create.subform.confirm_button') + ); return ( - + } @@ -50,16 +67,17 @@ export const CreateSubformWrapper = ({ label={t('ux_editor.create.subform.label')} size='small' value={newSubformName} - onChange={(e) => onNameChange(e.target.value)} + onChange={(e) => handleNameChange(e.target.value)} error={nameError} + disabled={isPendingLayoutSetMutation} /> - {t('ux_editor.create.subform.confirm_button')} + {createSubformButtonContent} diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.test.tsx index dc85b96c26d..10e59cb5bb4 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.test.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.test.tsx @@ -1,18 +1,15 @@ import React from 'react'; import { renderWithProviders } from '../../../../../../testing/mocks'; import { CreateNewSubformLayoutSet } from './CreateNewSubformLayoutSet'; -import type { ComponentType } from 'app-shared/types/ComponentType'; import { textMock } from '@studio/testing/mocks/i18nMock'; -import { screen, waitFor } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; -import { app, org } from '@studio/testing/testids'; -import { QueryKey } from 'app-shared/types/QueryKey'; import { layoutSets } from 'app-shared/mocks/mocks'; -import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse'; import userEvent from '@testing-library/user-event'; -import type { FormComponent } from '../../../../../../types/FormComponent'; import { AppContext } from '../../../../../../AppContext'; import { appContextMock } from '../../../../../../testing/appContextMock'; +import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; const onSubformCreatedMock = jest.fn(); const selectedOptionDataType = 'moped'; @@ -70,10 +67,36 @@ describe('CreateNewSubformLayoutSet ', () => { await user.selectOptions(dataModelSelect, ['moped']); const saveButton = screen.getByRole('button', { name: textMock('general.close') }); await user.click(saveButton); - await waitFor(() => expect(onSubformCreatedMock).toHaveBeenCalledTimes(1)); + expect(onSubformCreatedMock).toHaveBeenCalledTimes(1); expect(onSubformCreatedMock).toHaveBeenCalledWith('NewSubform'); }); + it('displays loading spinner when save button is clicked', async () => { + const user = userEvent.setup(); + + const addLayoutSetMock = jest.fn().mockImplementation( + () => + new Promise((resolve) => { + setTimeout(resolve, 100); + }), + ); + renderCreateNewSubformLayoutSet({ + addLayoutSet: addLayoutSetMock, + }); + + const input = screen.getByRole('textbox'); + await user.type(input, 'NewSubform'); + + const dataModelSelect = screen.getByRole('combobox'); + await user.selectOptions(dataModelSelect, ['moped']); + + const saveButton = screen.getByRole('button', { name: textMock('general.close') }); + await user.click(saveButton); + + const spinner = await screen.findByText(textMock('general.loading')); + expect(spinner).toBeInTheDocument(); + }); + it('disables the save button when input is invalid', async () => { const user = userEvent.setup(); renderCreateNewSubformLayoutSet(); @@ -124,20 +147,14 @@ describe('CreateNewSubformLayoutSet ', () => { }); }); -const renderCreateNewSubformLayoutSet = ( - layoutSetsMock: LayoutSets = layoutSets, - componentProps: Partial> = {}, -) => { - const queryClient = createQueryClientMock(); - queryClient.setQueryData([QueryKey.LayoutSets, org, app], layoutSetsMock); +const renderCreateNewSubformLayoutSet = (queries?: Partial) => { return renderWithProviders( - + , - { queryClient }, + { + queries: { ...queriesMock, ...queries }, + queryClient: createQueryClientMock(), + }, ); }; diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.tsx index 896e8172cd6..21b2a4e5d67 100644 --- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.tsx +++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { StudioButton, StudioCard, StudioTextfield } from '@studio/components'; +import { StudioButton, StudioCard, StudioSpinner, StudioTextfield } from '@studio/components'; import { ClipboardIcon, CheckmarkIcon } from '@studio/icons'; import classes from './CreateNewSubformLayoutSet.module.css'; import { SubformDataModelSelect } from './SubformDataModelSelect'; @@ -21,19 +21,25 @@ export const CreateNewSubformLayoutSet = ({ const [newSubform, setNewSubform] = useState(''); const [selectedDataType, setSelectedDataType] = useState(); const { validateLayoutSetName } = useValidateLayoutSetName(); - const { createSubform } = useCreateSubform(); + const { createSubform, isPendingLayoutSetMutation } = useCreateSubform(); const [nameError, setNameError] = useState(''); - function handleChange(e: React.ChangeEvent) { - const subformNameValidation = validateLayoutSetName(e.target.value, layoutSets); + function handleChange(subformName: string) { + const subformNameValidation = validateLayoutSetName(subformName, layoutSets); setNameError(subformNameValidation); - setNewSubform(e.target.value); + setNewSubform(subformName); } function handleCreateSubform() { createSubform({ layoutSetName: newSubform, onSubformCreated, dataType: selectedDataType }); } + const saveIcon = isPendingLayoutSetMutation ? ( + + ) : ( + + ); + return ( @@ -43,8 +49,8 @@ export const CreateNewSubformLayoutSet = ({ handleChange(e.target.value)} error={nameError} /> } + icon={saveIcon} onClick={handleCreateSubform} title={t('general.close')} disabled={!newSubform || !!nameError || !selectedDataType} diff --git a/frontend/packages/ux-editor/src/hooks/useCreateSubform.test.ts b/frontend/packages/ux-editor/src/hooks/useCreateSubform.test.ts index ece8a38b609..3f5f5be7a08 100644 --- a/frontend/packages/ux-editor/src/hooks/useCreateSubform.test.ts +++ b/frontend/packages/ux-editor/src/hooks/useCreateSubform.test.ts @@ -19,14 +19,20 @@ describe('useCreateSubform', () => { const subformName = 'underskjema'; const onSubformCreated = jest.fn(); - createSubform({ layoutSetName: subformName, onSubformCreated }); + createSubform({ layoutSetName: subformName, onSubformCreated, dataType: 'dataModel1' }); - expect(addLayoutSetMock).toHaveBeenCalledWith({ - layoutSetIdToUpdate: subformName, - layoutSetConfig: { - id: subformName, - type: 'subform', + expect(addLayoutSetMock).toHaveBeenCalledWith( + { + layoutSetIdToUpdate: subformName, + layoutSetConfig: { + id: subformName, + type: 'subform', + dataType: 'dataModel1', + }, }, - }); + { + onSuccess: expect.any(Function), + }, + ); }); }); diff --git a/frontend/packages/ux-editor/src/hooks/useCreateSubform.ts b/frontend/packages/ux-editor/src/hooks/useCreateSubform.ts index 80631609018..19d33225104 100644 --- a/frontend/packages/ux-editor/src/hooks/useCreateSubform.ts +++ b/frontend/packages/ux-editor/src/hooks/useCreateSubform.ts @@ -4,24 +4,38 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen type CreateSubformProps = { layoutSetName: string; onSubformCreated: (layoutSetName: string) => void; - dataType?: string; + dataType: string; }; -export const useCreateSubform = () => { +type UseCreateSubformReturn = { + createSubform: (props: CreateSubformProps) => void; + isPendingLayoutSetMutation: boolean; +}; + +export const useCreateSubform = (): UseCreateSubformReturn => { const { org, app } = useStudioEnvironmentParams(); - const { mutate: addLayoutSet } = useAddLayoutSetMutation(org, app); + const { mutate: addLayoutSet, isPending: isPendingLayoutSetMutation } = useAddLayoutSetMutation( + org, + app, + ); const createSubform = ({ layoutSetName, onSubformCreated, dataType }: CreateSubformProps) => { - addLayoutSet({ - layoutSetIdToUpdate: layoutSetName, - layoutSetConfig: { - id: layoutSetName, - type: 'subform', - dataType, + addLayoutSet( + { + layoutSetIdToUpdate: layoutSetName, + layoutSetConfig: { + id: layoutSetName, + type: 'subform', + dataType, + }, + }, + { + onSuccess: () => { + onSubformCreated(layoutSetName); + }, }, - }); - onSubformCreated(layoutSetName); + ); }; - return { createSubform }; + return { createSubform, isPendingLayoutSetMutation }; };