[i18n] add a locale from the ISO list (#9397)

This commit is contained in:
Marvin Frachet 2021-02-15 14:55:08 +01:00 committed by GitHub
parent 615905d120
commit cdcbb0de4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 315 additions and 19 deletions

View File

@ -0,0 +1,77 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Label } from '@buffetjs/core';
import { Inputs } from '@buffetjs/custom';
import Select from 'react-select';
import { Col, Row } from 'reactstrap';
import { useIntl } from 'react-intl';
import { useFormikContext } from 'formik';
import { getTrad } from '../../utils';
const BaseForm = ({ options, defaultOption }) => {
const { formatMessage } = useIntl();
const { values, handleChange, setFieldValue } = useFormikContext();
return (
<Row>
<Col>
<span id="locale-code">
<Label htmlFor="">
{formatMessage({
id: getTrad('Settings.locales.modal.locales.label'),
})}
</Label>
</span>
<Select
aria-labelledby="locale-code"
options={options}
defaultValue={defaultOption}
onChange={selection => {
setFieldValue('displayName', selection.value);
setFieldValue('code', selection.label);
}}
/>
</Col>
<Col>
<Inputs
label={formatMessage({
id: getTrad('Settings.locales.modal.locales.displayName'),
})}
name="displayName"
description={formatMessage({
id: getTrad('Settings.locales.modal.locales.displayName.description'),
})}
type="text"
value={values.displayName}
onChange={handleChange}
validations={{
max: 50,
}}
translatedErrors={{
max: formatMessage({
id: getTrad('Settings.locales.modal.locales.displayName.error'),
}),
}}
/>
</Col>
</Row>
);
};
BaseForm.defaultProps = {
defaultOption: undefined,
};
BaseForm.propTypes = {
options: PropTypes.arrayOf(
PropTypes.exact({ value: PropTypes.string.isRequired, label: PropTypes.string.isRequired })
).isRequired,
defaultOption: PropTypes.exact({
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
}),
};
export default BaseForm;

View File

@ -7,15 +7,40 @@ import { Formik } from 'formik';
import localeFormSchema from '../../schemas'; import localeFormSchema from '../../schemas';
import { getTrad } from '../../utils'; import { getTrad } from '../../utils';
import SettingsModal from '../SettingsModal'; import SettingsModal from '../SettingsModal';
import useDefaultLocales from '../../hooks/useDefaultLocales';
import useAddLocale from '../../hooks/useAddLocale';
import BaseForm from './BaseForm';
const ModalCreate = ({ onClose, isOpened }) => { const ModalCreate = ({ onClose, isOpened }) => {
const { defaultLocales, isLoading } = useDefaultLocales();
const { isAdding, addLocale } = useAddLocale();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
if (!isOpened) return null;
if (isLoading) {
return (
<div>
<p>
{formatMessage({ id: getTrad('Settings.locales.modal.create.defaultLocales.loading') })}
</p>
</div>
);
}
const options = (defaultLocales || []).map(locale => ({
label: locale.code,
value: locale.name,
}));
const defaultOption = options[0];
return ( return (
<Modal isOpen={isOpened} onToggle={onClose}> <Modal isOpen={isOpened} onToggle={onClose}>
<Formik <Formik
initialValues={{ displayName: '' }} initialValues={{ code: defaultOption.label, displayName: defaultOption.value }}
onSubmit={() => null} onSubmit={values =>
addLocale({ code: values.code, name: values.displayName }).then(onClose)}
validationSchema={localeFormSchema} validationSchema={localeFormSchema}
> >
{({ handleSubmit, errors }) => ( {({ handleSubmit, errors }) => (
@ -30,7 +55,9 @@ const ModalCreate = ({ onClose, isOpened }) => {
})} })}
tabsId="i18n-settings-tabs-create" tabsId="i18n-settings-tabs-create"
> >
<TabPanel>Base form</TabPanel> <TabPanel>
<BaseForm options={options} defaultOption={defaultOption} />
</TabPanel>
<TabPanel>advanced</TabPanel> <TabPanel>advanced</TabPanel>
</SettingsModal> </SettingsModal>
@ -42,7 +69,7 @@ const ModalCreate = ({ onClose, isOpened }) => {
<Button <Button
color="success" color="success"
type="submit" type="submit"
isLoading={false} isLoading={isAdding}
disabled={Object.keys(errors).length > 0} disabled={Object.keys(errors).length > 0}
> >
{formatMessage({ id: getTrad('Settings.locales.modal.create.confirmation') })} {formatMessage({ id: getTrad('Settings.locales.modal.create.confirmation') })}

View File

@ -33,11 +33,11 @@ const BaseForm = ({ options, defaultOption }) => {
<Col> <Col>
<Inputs <Inputs
label={formatMessage({ label={formatMessage({
id: getTrad('Settings.locales.modal.edit.locales.displayName'), id: getTrad('Settings.locales.modal.locales.displayName'),
})} })}
name="displayName" name="displayName"
description={formatMessage({ description={formatMessage({
id: getTrad('Settings.locales.modal.edit.locales.displayName.description'), id: getTrad('Settings.locales.modal.locales.displayName.description'),
})} })}
type="text" type="text"
value={values.displayName} value={values.displayName}

View File

@ -179,13 +179,13 @@ describe('i18n settings page', () => {
const rowUtils = within(row); const rowUtils = within(row);
fireEvent.click(rowUtils.getByLabelText('Settings.list.actions.edit')); fireEvent.click(rowUtils.getByLabelText('Settings.list.actions.edit'));
fireEvent.change(screen.getByLabelText('Settings.locales.modal.edit.locales.displayName'), { fireEvent.change(screen.getByLabelText('Settings.locales.modal.locales.displayName'), {
target: { target: {
value: value:
'a very very very very long string that has more than fifty characters in order to show a warning', 'a very very very very long string that has more than fifty characters in order to show a warning',
}, },
}); });
fireEvent.blur(screen.getByLabelText('Settings.locales.modal.edit.locales.displayName')); fireEvent.blur(screen.getByLabelText('Settings.locales.modal.locales.displayName'));
await waitFor(() => await waitFor(() =>
expect(screen.getByText('Settings.locales.modal.edit.confirmation')).toBeDisabled() expect(screen.getByText('Settings.locales.modal.edit.confirmation')).toBeDisabled()
@ -341,7 +341,7 @@ describe('i18n settings page', () => {
fireEvent.click(rowUtils.getByLabelText('Settings.list.actions.edit')); fireEvent.click(rowUtils.getByLabelText('Settings.list.actions.edit'));
fireEvent.change(screen.getByLabelText('Settings.locales.modal.edit.locales.displayName'), { fireEvent.change(screen.getByLabelText('Settings.locales.modal.locales.displayName'), {
target: { target: {
value: '', value: '',
}, },
@ -487,7 +487,17 @@ describe('i18n settings page', () => {
}); });
describe('create', () => { describe('create', () => {
// TODO: add the select verifications beforeEach(() => {
request.mockImplementation(url =>
url.includes('/i18n/locales')
? Promise.resolve([])
: Promise.resolve([
{ code: 'fr-FR', name: 'Francais' },
{ code: 'en-EN', name: 'English' },
])
);
});
it('shows the default create modal layout', async () => { it('shows the default create modal layout', async () => {
render( render(
<TestWrapper> <TestWrapper>
@ -497,7 +507,18 @@ describe('i18n settings page', () => {
fireEvent.click(screen.getByText('Settings.list.actions.add')); fireEvent.click(screen.getByText('Settings.list.actions.add'));
expect(screen.getByText(`Settings.locales.modal.create.confirmation`)).toBeVisible(); expect(
screen.getByText(`Settings.locales.modal.create.defaultLocales.loading`)
).toBeVisible();
await waitFor(() =>
expect(screen.getByText(`Settings.locales.modal.create.confirmation`)).toBeVisible()
);
expect(screen.getByText(`fr-FR`)).toBeVisible();
expect(screen.getByLabelText('Settings.locales.modal.locales.displayName')).toHaveValue(
'Francais'
);
}); });
it('closes the create modal when clicking on cancel', async () => { it('closes the create modal when clicking on cancel', async () => {
@ -508,9 +529,142 @@ describe('i18n settings page', () => {
); );
fireEvent.click(screen.getByText('Settings.list.actions.add')); fireEvent.click(screen.getByText('Settings.list.actions.add'));
await waitFor(() =>
expect(screen.getByText(`Settings.locales.modal.create.confirmation`)).toBeVisible()
);
fireEvent.click(screen.getByText('app.components.Button.cancel')); fireEvent.click(screen.getByText('app.components.Button.cancel'));
expect(screen.queryByText(`Settings.locales.modal.create.confirmation`)).toBeFalsy(); expect(screen.queryByText(`Settings.locales.modal.create.confirmation`)).toBeFalsy();
}); });
it('shows a warning and disabled the confirmation button when display name length is over 50', async () => {
render(
<TestWrapper>
<LocaleSettingsPage />
</TestWrapper>
);
fireEvent.click(screen.getByText('Settings.list.actions.add'));
await waitFor(() =>
expect(screen.getByText(`Settings.locales.modal.create.confirmation`)).toBeVisible()
);
fireEvent.change(screen.getByLabelText('Settings.locales.modal.locales.displayName'), {
target: {
value:
'a very very very very long string that has more than fifty characters in order to show a warning',
},
});
fireEvent.blur(screen.getByLabelText('Settings.locales.modal.locales.displayName'));
await waitFor(() =>
expect(screen.getByText(`Settings.locales.modal.locales.displayName.error`)).toBeVisible()
);
});
it('sync the select and the text input', async () => {
render(
<TestWrapper>
<LocaleSettingsPage />
</TestWrapper>
);
fireEvent.click(screen.getByText('Settings.list.actions.add'));
await waitFor(() =>
expect(screen.getByText(`Settings.locales.modal.create.confirmation`)).toBeVisible()
);
// Put some data in the input in order to make sure it resets well when changing locale
fireEvent.change(screen.getByLabelText('Settings.locales.modal.locales.displayName'), {
target: {
value:
'a very very very very long string that has more than fifty characters in order to show a warning',
},
});
const DOWN_ARROW = { keyCode: 40 };
fireEvent.keyDown(screen.getByLabelText('Settings.locales.modal.locales.label'), DOWN_ARROW);
fireEvent.click(screen.getByText('en-EN'));
expect(screen.getByLabelText('Settings.locales.modal.locales.displayName')).toHaveValue(
'English'
);
});
it('shows an error when something went wrong when adding a locale', async () => {
request.mockImplementation((url, opts) => {
if (opts.method === 'POST') {
return Promise.reject(new Error('Something went wrong when adding a locale'));
}
if (url.includes('/i18n/locales')) return Promise.resolve([]);
return Promise.resolve([
{ code: 'fr-FR', name: 'Francais' },
{ code: 'en-EN', name: 'English' },
]);
});
render(
<TestWrapper>
<LocaleSettingsPage />
</TestWrapper>
);
fireEvent.click(screen.getByText('Settings.list.actions.add'));
const confirmationButton = await waitFor(() =>
screen.getByText(`Settings.locales.modal.create.confirmation`)
);
fireEvent.click(confirmationButton);
await waitFor(() =>
expect(strapi.notification.toggle).toBeCalledWith({
type: 'warning',
message: { id: 'notification.error' },
})
);
});
it('shows an success toast when adding a locale is successful', async () => {
request.mockImplementation((url, opts) => {
if (opts.method === 'POST') {
return Promise.resolve({ id: 3, code: 'en-CA', name: 'Canadien' });
}
if (url.includes('/i18n/locales')) return Promise.resolve([]);
return Promise.resolve([
{ code: 'fr-FR', name: 'Francais' },
{ code: 'en-EN', name: 'English' },
]);
});
render(
<TestWrapper>
<LocaleSettingsPage />
</TestWrapper>
);
fireEvent.click(screen.getByText('Settings.list.actions.add'));
const confirmationButton = await waitFor(() =>
screen.getByText(`Settings.locales.modal.create.confirmation`)
);
fireEvent.click(confirmationButton);
await waitFor(() =>
expect(strapi.notification.toggle).toBeCalledWith({
type: 'success',
message: { id: 'Settings.locales.modal.create.success' },
})
);
});
}); });
}); });

View File

@ -0,0 +1,41 @@
import { request } from 'strapi-helper-plugin';
import { useMutation, useQueryClient } from 'react-query';
import { getTrad } from '../../utils';
const addLocale = async ({ code, name }) => {
try {
const data = await request(`/i18n/locales`, {
method: 'POST',
body: {
name,
code,
},
});
strapi.notification.toggle({
type: 'success',
message: { id: getTrad('Settings.locales.modal.create.success') },
});
return data;
} catch (e) {
strapi.notification.toggle({
type: 'warning',
message: { id: 'notification.error' },
});
return e;
}
};
const useAddLocale = () => {
const queryClient = useQueryClient();
const { isLoading, mutateAsync } = useMutation(addLocale, {
onSuccess: data => queryClient.setQueryData(['locales', { id: data.id }], data),
});
return { isAdding: isLoading, addLocale: mutateAsync };
};
export default useAddLocale;

View File

@ -1,7 +1,7 @@
import { object, string } from 'yup'; import { object, string } from 'yup';
const localeFormSchema = object().shape({ const localeFormSchema = object().shape({
displayName: string().max(50, 'Settings.locales.modal.create.locales.displayName.error'), displayName: string().max(50, 'Settings.locales.modal.locales.displayName.error'),
}); });
export default localeFormSchema; export default localeFormSchema;

View File

@ -30,18 +30,15 @@
"Settings.locales.modal.delete.success": "Locale successfully deleted", "Settings.locales.modal.delete.success": "Locale successfully deleted",
"Settings.locales.modal.edit.confirmation": "Finish", "Settings.locales.modal.edit.confirmation": "Finish",
"Settings.locales.modal.edit.success": "Locale successfully edited", "Settings.locales.modal.edit.success": "Locale successfully edited",
"Settings.locales.modal.edit.locales.label": "Locales",
"Settings.locales.modal.edit.locales.displayName": "Locale display name",
"Settings.locales.modal.edit.locales.displayName.description":"Locale will be displayed under that name in the administration panel",
"Settings.locales.modal.edit.tab.label":"Navigating between the I18N base settings and advanced settings", "Settings.locales.modal.edit.tab.label":"Navigating between the I18N base settings and advanced settings",
"Settings.locales.modal.base": "Base settings", "Settings.locales.modal.base": "Base settings",
"Settings.locales.modal.advanced": "Advanced settings", "Settings.locales.modal.advanced": "Advanced settings",
"Settings.locales.modal.create.defaultLocales.loading": "Loading the available locales...",
"Settings.locales.modal.create.confirmation": "Add locale", "Settings.locales.modal.create.confirmation": "Add locale",
"Settings.locales.modal.create.success": "Locale successfully added", "Settings.locales.modal.create.success": "Locale successfully added",
"Settings.locales.modal.create.locales.label": "Locales", "Settings.locales.modal.locales.label": "Locales",
"Settings.locales.modal.create.locales.displayName": "Locale display name", "Settings.locales.modal.locales.displayName": "Locale display name",
"Settings.locales.modal.locales.displayName.error": "The locale display name can only be less than 50 characters.", "Settings.locales.modal.locales.displayName.error": "The locale display name can only be less than 50 characters.",
"Settings.locales.modal.create.locales.displayName.description":"Locale will be displayed under that name in the administration panel", "Settings.locales.modal.locales.displayName.description":"Locale will be displayed under that name in the administration panel",
"Settings.locales.modal.create.tab.label":"Navigating between the I18N base settings and advanced settings" "Settings.locales.modal.create.tab.label":"Navigating between the I18N base settings and advanced settings"
} }