From 445209f43d3e3f8c49d9fae986aed6186ae29cf5 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 17 Jan 2025 11:39:14 -0300 Subject: [PATCH] validate form and notify --- app/react/I18N/TranslateModal.tsx | 40 ++++++++++++-- app/react/I18N/specs/TranslateModal.spec.tsx | 58 ++++++++++++++++++-- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/app/react/I18N/TranslateModal.tsx b/app/react/I18N/TranslateModal.tsx index 7b586110b7..d00dcba2ba 100644 --- a/app/react/I18N/TranslateModal.tsx +++ b/app/react/I18N/TranslateModal.tsx @@ -1,9 +1,10 @@ /* eslint-disable react/jsx-props-no-spreading */ import React from 'react'; -import { useAtom, useAtomValue } from 'jotai'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useFieldArray, useForm } from 'react-hook-form'; +import { FetchResponseError } from 'shared/JSONRequest'; import { Modal } from 'V2/Components/UI'; -import { settingsAtom, translationsAtom, inlineEditAtom } from 'V2/atoms'; +import { settingsAtom, translationsAtom, inlineEditAtom, notificationAtom } from 'V2/atoms'; import { InputField } from 'app/V2/Components/Forms'; import { Button } from 'V2/Components/UI/Button'; import { TranslationValue } from 'V2/shared/types'; @@ -13,10 +14,17 @@ import { t } from './translateFunction'; const TranslateModal = () => { const [inlineEditState, setInlineEditState] = useAtom(inlineEditAtom); const [translations] = useAtom(translationsAtom); + const setNotifications = useSetAtom(notificationAtom); const context = translations[0].contexts.find(ctx => ctx.id === inlineEditState.context)!; const { languages = [] } = useAtomValue(settingsAtom); - const { register, handleSubmit, control, reset } = useForm<{ data: TranslationValue[] }>({ + const { + register, + handleSubmit, + control, + reset, + formState: { errors, isDirty }, + } = useForm<{ data: TranslationValue[] }>({ mode: 'onSubmit', }); @@ -26,7 +34,8 @@ const TranslateModal = () => { const initialValues = translations.map(translation => { const language = languages.find(lang => lang.key === translation.locale)!; const languageContext = translation.contexts.find(c => c.id === context?.id); - const value = languageContext?.values[inlineEditState.translationKey]; + const value = + languageContext?.values[inlineEditState.translationKey] || inlineEditState.translationKey; return { language: language.key, value, @@ -41,7 +50,25 @@ const TranslateModal = () => { }; const submit = async ({ data }: { data: TranslationValue[] }) => { - await postV2(data, context); + if (isDirty) { + const response = await postV2(data, context); + if (response === 200) { + setNotifications({ + type: 'success', + text: t('System', 'Translations saved', null, false), + }); + } + if (response instanceof FetchResponseError) { + const message = response.json?.prettyMessage + ? response.json.prettyMessage + : response.message; + setNotifications({ + type: 'error', + text: t('System', 'An error occurred', null, false), + details: message, + }); + } + } closeModal(); }; @@ -67,7 +94,8 @@ const TranslateModal = () => { } id={field.id} key={field.id} - {...register(`data.${index}.value`)} + {...register(`data.${index}.value`, { required: true })} + hasErrors={errors.data && errors.data[index] !== undefined} /> ))} diff --git a/app/react/I18N/specs/TranslateModal.spec.tsx b/app/react/I18N/specs/TranslateModal.spec.tsx index ea9e96890e..6eb2d2aff7 100644 --- a/app/react/I18N/specs/TranslateModal.spec.tsx +++ b/app/react/I18N/specs/TranslateModal.spec.tsx @@ -4,8 +4,9 @@ import React, { act } from 'react'; import { fireEvent, render, RenderResult } from '@testing-library/react'; import { TestAtomStoreProvider } from 'V2/testing'; -import { settingsAtom, translationsAtom, inlineEditAtom } from 'V2/atoms'; +import { settingsAtom, translationsAtom, inlineEditAtom, notificationAtom } from 'V2/atoms'; import * as translationsAPI from 'V2/api/translations'; +import { NotificationsContainer } from 'V2/Components/UI'; import { TranslateModal } from '../TranslateModal'; import { languages, translations } from './fixtures'; @@ -13,7 +14,7 @@ describe('TranslateModal', () => { let renderResult: RenderResult; beforeAll(() => { - jest.spyOn(translationsAPI, 'postV2').mockImplementationOnce(async () => Promise.resolve([])); + jest.spyOn(translationsAPI, 'postV2').mockImplementation(async () => Promise.resolve(200)); }); afterEach(() => { @@ -27,9 +28,11 @@ describe('TranslateModal', () => { [settingsAtom, { languages }], [translationsAtom, translations], [inlineEditAtom, { inlineEdit, context, translationKey }], + [notificationAtom, {}], ]} > + ); }; @@ -72,9 +75,56 @@ describe('TranslateModal', () => { translations[0].contexts[0] ); expect(renderResult.queryByText('Translate')).not.toBeInTheDocument(); + expect(renderResult.queryByText('Translations saved')).toBeInTheDocument(); }); - it('should not allow sending empty fields', () => {}); + it('should not allow sending empty fields', async () => { + renderComponent(true, 'System', 'Search'); + const inputFields = renderResult.queryAllByRole('textbox'); + const saveButton = renderResult.getByTestId('save-button'); + + await act(() => { + fireEvent.change(inputFields[0], { target: { value: '' } }); + fireEvent.click(saveButton); + }); + + expect(translationsAPI.postV2).not.toHaveBeenCalled(); + }); - it('should use the default context key if translation does not exist', () => {}); + it('should use the default context key if translation does not exist', async () => { + renderComponent(true, 'System', 'This key is not in the database'); + const inputFields = renderResult.queryAllByRole('textbox'); + expect(inputFields[0]).toHaveValue('This key is not in the database'); + expect(inputFields[1]).toHaveValue('This key is not in the database'); + const saveButton = renderResult.getByTestId('save-button'); + + await act(() => { + fireEvent.change(inputFields[0], { target: { value: 'My new key' } }); + fireEvent.change(inputFields[1], { target: { value: 'Nueva llave' } }); + fireEvent.click(saveButton); + }); + + expect(translationsAPI.postV2).toHaveBeenCalledWith( + [ + { language: 'en', value: 'My new key', key: 'This key is not in the database' }, + { language: 'es', value: 'Nueva llave', key: 'This key is not in the database' }, + ], + translations[0].contexts[0] + ); + expect(renderResult.queryByText('Translate')).not.toBeInTheDocument(); + }); + + it('should not save if there are no changes', async () => { + renderComponent(true, 'System', 'Search'); + const saveButton = renderResult.getByTestId('save-button'); + const inputFields = renderResult.queryAllByRole('textbox'); + + await act(() => { + fireEvent.change(inputFields[1], { target: { value: 'Nueva traducción' } }); + fireEvent.change(inputFields[1], { target: { value: 'Buscar' } }); + fireEvent.click(saveButton); + }); + + expect(translationsAPI.postV2).not.toHaveBeenCalled(); + }); });