Migration/ Email templates (#11000)

* temp commit

* email templates form + test
This commit is contained in:
ronronscelestes 2021-09-20 14:00:53 +02:00 committed by GitHub
parent bd5a98903f
commit 5e4bc05751
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1063 additions and 496 deletions

View File

@ -59,23 +59,22 @@ export default {
},
permissions: pluginPermissions.readProviders,
},
{
intlLabel: {
id: getTrad('HeaderNav.link.emailTemplates'),
defaultMessage: 'Email templates',
},
id: 'email-templates',
to: `/settings/${pluginId}/email-templates`,
Component: async () => {
const component = await import(
/* webpackChunkName: "users-email-settings-page" */ './pages/EmailTemplates'
);
// {
// intlLabel: {
// id: getTrad('HeaderNav.link.emailTemplates'),
// defaultMessage: 'Email templates',
// },
// id: 'email-templates',
// to: `/settings/${pluginId}/email-templates`,
// Component: async () => {
// const component = await import(
// /* webpackChunkName: "users-email-settings-page" */ './pages/EmailTemplates'
// );
// return component;
// },
// permissions: pluginPermissions.readEmailTemplates,
// },
return component;
},
permissions: pluginPermissions.readEmailTemplates,
},
{
intlLabel: {
id: getTrad('HeaderNav.link.advancedSettings'),

View File

@ -1,105 +0,0 @@
/**
*
* The input is made so we can handle a custom descritption
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Error, Label, InputText } from '@buffetjs/core';
import { FormattedMessage } from 'react-intl';
import { Description, ErrorMessage } from '@buffetjs/styles';
import { getTrad } from '../../utils';
import Wrapper from './Wrapper';
const CustomTextInput = ({
description,
error: inputError,
label,
name,
onChange,
validations,
value,
...rest
}) => {
const inputId = name;
const descriptionId = `description-${inputId}`;
const errorId = `error-${inputId}`;
const link = (
<a
href="https://strapi.io/documentation/developer-docs/latest/development/plugins/users-permissions.html#templating-emails"
target="_blank"
rel="noopener noreferrer"
>
<FormattedMessage id="users-permissions.PopUpForm.Email.link.documentation" />
</a>
);
const descriptionCompo = (
<Description id={descriptionId}>
<FormattedMessage
id={getTrad('PopUpForm.Email.email_templates.inputDescription')}
values={{ link }}
/>
</Description>
);
return (
<Error inputError={inputError} name={name} type="text" validations={validations}>
{({ canCheck, onBlur, error, dispatch }) => (
<Wrapper error={error}>
<Label htmlFor={inputId}>{label}</Label>
<InputText
{...rest}
name={name}
id={inputId}
aria-invalid={error ? 'true' : 'false'}
onBlur={onBlur}
onChange={e => {
if (!canCheck) {
dispatch({
type: 'SET_CHECK',
});
}
dispatch({
type: 'SET_ERROR',
error: null,
});
onChange(e);
}}
value={value}
/>
{!error ? descriptionCompo : <ErrorMessage id={errorId}>{error}</ErrorMessage>}
</Wrapper>
)}
</Error>
);
};
CustomTextInput.defaultProps = {
description: null,
id: null,
error: null,
label: null,
onBlur: null,
onChange: () => {},
validations: {},
value: '',
};
CustomTextInput.propTypes = {
description: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
error: PropTypes.string,
id: PropTypes.string,
label: PropTypes.string,
name: PropTypes.string.isRequired,
onBlur: PropTypes.func,
onChange: () => {},
validations: PropTypes.object,
value: PropTypes.any,
};
export default CustomTextInput;

View File

@ -1,36 +0,0 @@
import styled, { css } from 'styled-components';
import { colors, sizes } from '@buffetjs/styles';
/* eslint-disable indent */
const Wrapper = styled.div`
position: relative;
padding-bottom: ${sizes.margin * 2.7}px;
label {
display: block;
margin-bottom: 1rem;
}
> p {
width: 100%;
padding-top: 10px;
font-size: 13px;
line-height: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: -8px;
}
input[type='checkbox'] {
margin-bottom: 13px;
}
${({ error }) =>
!!error &&
css`
input,
textarea,
select {
border-color: ${colors.darkOrange};
}
`}
`;
export default Wrapper;

View File

@ -0,0 +1,168 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Form, GenericInput } from '@strapi/helper-plugin';
import { Formik } from 'formik';
import { ModalLayout, ModalHeader, ModalFooter, ModalBody } from '@strapi/parts/ModalLayout';
import { Grid, GridItem } from '@strapi/parts/Grid';
import { Button } from '@strapi/parts/Button';
import { Breadcrumbs, Crumb } from '@strapi/parts/Breadcrumbs';
import { Textarea } from '@strapi/parts/Textarea';
import { getTrad } from '../../../utils';
import schema from '../utils/schema';
const EmailForm = ({ template, onToggle, onSubmit }) => {
const { formatMessage } = useIntl();
return (
<ModalLayout
onClose={onToggle}
labelledBy={`${formatMessage({
id: getTrad('PopUpForm.header.edit.email-templates'),
defaultMessage: 'Edit email template',
})}, ${formatMessage({ id: getTrad(template.display), defaultMessage: template.display })}`}
>
<ModalHeader>
<Breadcrumbs
label={`${formatMessage({
id: getTrad('PopUpForm.header.edit.email-templates'),
defaultMessage: 'Edit email template',
})}, ${formatMessage({
id: getTrad(template.display),
defaultMessage: template.display,
})}`}
>
<Crumb>
{formatMessage({
id: getTrad('PopUpForm.header.edit.email-templates'),
defaultMessage: 'Edit email template',
})}
</Crumb>
<Crumb>
{formatMessage({ id: getTrad(template.display), defaultMessage: template.display })}
</Crumb>
</Breadcrumbs>
</ModalHeader>
<Formik
onSubmit={onSubmit}
initialValues={template}
validateOnChange={false}
validationSchema={schema}
enableReinitialize
>
{({ errors, values, handleChange, isSubmitting }) => {
return (
<Form>
<ModalBody>
<Grid gap={5}>
<GridItem col={6} s={12}>
<GenericInput
intlLabel={{
id: getTrad('PopUpForm.Email.options.from.name.label'),
defaultMessage: 'Shipper name',
}}
name="options.from.name"
onChange={handleChange}
value={values.options.from.name}
error={errors?.options?.from?.name}
type="text"
/>
</GridItem>
<GridItem col={6} s={12}>
<GenericInput
intlLabel={{
id: getTrad('PopUpForm.Email.options.from.email.label'),
defaultMessage: 'Shipper email',
}}
name="options.from.email"
onChange={handleChange}
value={values.options.from.email}
error={errors?.options?.from?.email}
type="text"
/>
</GridItem>
<GridItem col={6} s={12}>
<GenericInput
intlLabel={{
id: getTrad('PopUpForm.Email.options.response_email.label'),
defaultMessage: 'Response email',
}}
name="options.response_email"
onChange={handleChange}
value={values.options.response_email}
error={errors?.options?.response_email}
type="text"
/>
</GridItem>
<GridItem col={6} s={12}>
<GenericInput
intlLabel={{
id: getTrad('PopUpForm.Email.options.object.label'),
defaultMessage: 'Subject',
}}
name="options.object"
onChange={handleChange}
value={values.options.object}
error={errors?.options?.object}
type="text"
/>
</GridItem>
<GridItem col={12} s={12}>
<Textarea
label={formatMessage({
id: getTrad('PopUpForm.Email.options.message.label'),
defaultMessage: 'Message',
})}
name="options.message"
onChange={handleChange}
value={values.options.message}
error={
errors?.options?.message &&
formatMessage({
id: errors.options.message,
defaultMessage: errors.options.message,
})
}
/>
</GridItem>
</Grid>
</ModalBody>
<ModalFooter
startActions={
<Button onClick={onToggle} variant="tertiary">
Cancel
</Button>
}
endActions={
<Button loading={isSubmitting} type="submit">
Finish
</Button>
}
/>
</Form>
);
}}
</Formik>
</ModalLayout>
);
};
EmailForm.propTypes = {
template: PropTypes.shape({
display: PropTypes.string,
icon: PropTypes.string,
options: PropTypes.shape({
from: PropTypes.shape({
name: PropTypes.string,
email: PropTypes.string,
}),
message: PropTypes.string,
object: PropTypes.string,
response_email: PropTypes.string,
}),
}).isRequired,
onSubmit: PropTypes.func.isRequired,
onToggle: PropTypes.func.isRequired,
};
export default EmailForm;

View File

@ -0,0 +1,115 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Table, Thead, Tbody, Tr, Td, Th } from '@strapi/parts/Table';
import { VisuallyHidden } from '@strapi/parts/VisuallyHidden';
import { Text, TableLabel } from '@strapi/parts/Text';
import { IconButton } from '@strapi/parts/IconButton';
import EditIcon from '@strapi/icons/EditIcon';
import Reload from '@strapi/icons/Reload';
import CheckIcon from '@strapi/icons/CheckIcon';
import { getTrad } from '../../../utils';
const EmailTable = ({ canUpdate, onEditClick }) => {
const { formatMessage } = useIntl();
return (
<Table colCount={3} rowCount={3}>
<Thead>
<Tr>
<Th width="1%">
<VisuallyHidden>
{formatMessage({
id: getTrad('Email.template.table.icon.label'),
defaultMessage: 'icon',
})}
</VisuallyHidden>
</Th>
<Th>
<TableLabel>
{formatMessage({
id: getTrad('Email.template.table.name.label'),
defaultMessage: 'name',
})}
</TableLabel>
</Th>
<Th width="1%">
<VisuallyHidden>
{formatMessage({
id: getTrad('Email.template.table.action.label'),
defaultMessage: 'action',
})}
</VisuallyHidden>
</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>
<Reload
aria-label={formatMessage({
id: getTrad('Email.template.reset_password'),
defaultMessage: 'Reset password',
})}
/>
</Td>
<Td>
<Text>
{formatMessage({
id: getTrad('Email.template.reset_password'),
defaultMessage: 'Reset password',
})}
</Text>
</Td>
<Td>
<IconButton
onClick={() => onEditClick('reset_password')}
label={formatMessage({
id: getTrad('Email.template.form.edit.label'),
defaultMessage: 'Edit a template',
})}
noBorder
icon={canUpdate && <EditIcon />}
/>
</Td>
</Tr>
<Tr>
<Td>
<CheckIcon
aria-label={formatMessage({
id: getTrad('Email.template.email_confirmation'),
defaultMessage: 'Email address confirmation',
})}
/>
</Td>
<Td>
<Text>
{formatMessage({
id: getTrad('Email.template.email_confirmation'),
defaultMessage: 'Email address confirmation',
})}
</Text>
</Td>
<Td>
<IconButton
onClick={() => onEditClick('email_confirmation')}
label={formatMessage({
id: getTrad('Email.template.form.edit.label'),
defaultMessage: 'Edit a template',
})}
noBorder
icon={canUpdate && <EditIcon />}
/>
</Td>
</Tr>
</Tbody>
</Table>
);
};
EmailTable.propTypes = {
canUpdate: PropTypes.bool.isRequired,
onEditClick: PropTypes.func.isRequired,
};
export default EmailTable;

View File

@ -1,27 +1,24 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useMemo, useRef, useState } from 'react';
import { useQuery, useMutation, useQueryClient } from 'react-query';
import { useIntl } from 'react-intl';
import { Header, List } from '@buffetjs/custom';
import { Pencil } from '@buffetjs/icons';
import { get } from 'lodash';
import {
SettingsPageTitle,
SizedInput,
useTracking,
request,
getYupInnerErrors,
useNotification,
useOverlayBlocker,
CheckPagePermissions,
useRBAC,
useFocusWhenNavigate,
LoadingIndicatorPage,
} from '@strapi/helper-plugin';
import { Row } from 'reactstrap';
import { useNotifyAT } from '@strapi/parts/LiveRegions';
import { Main } from '@strapi/parts/Main';
import { ContentLayout, HeaderLayout } from '@strapi/parts/Layout';
import pluginPermissions from '../../permissions';
import { useForm } from '../../hooks';
import ListBaselineAlignment from '../../components/ListBaselineAlignment';
import ListRow from '../../components/ListRow';
import ModalForm from '../../components/ModalForm';
import { getRequestURL, getTrad } from '../../utils';
import forms from './utils/forms';
import schema from './utils/schema';
import { getTrad } from '../../utils';
import { fetchData, putEmailTemplate } from './utils/api';
import EmailTable from './components/EmailTable';
import EmailForm from './components/EmailForm';
const ProtectedEmailTemplatesPage = () => (
<CheckPagePermissions permissions={pluginPermissions.readEmailTemplates}>
@ -32,210 +29,133 @@ const ProtectedEmailTemplatesPage = () => (
const EmailTemplatesPage = () => {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const { notifyStatus } = useNotifyAT();
const toggleNotification = useNotification();
const { lockApp, unlockApp } = useOverlayBlocker();
const trackUsageRef = useRef(trackUsage);
const buttonSubmitRef = useRef(null);
const pageTitle = formatMessage({ id: getTrad('HeaderNav.link.emailTemplates') });
const queryClient = useQueryClient();
useFocusWhenNavigate();
const [isModalOpen, setIsModalOpen] = useState(false);
const [templateToEdit, setTemplateToEdit] = useState(null);
const updatePermissions = useMemo(() => {
return { update: pluginPermissions.updateEmailTemplates };
}, []);
const [isOpen, setIsOpen] = useState(false);
const [isSubmiting, setIsSubmiting] = useState(false);
const [showForm, setShowForm] = useState(false);
const [templateToEdit, setTemplateToEdit] = useState(null);
const {
isLoading: isLoadingForPermissions,
allowedActions: { canUpdate },
dispatchResetForm,
dispatchSetFormErrors,
dispatchSubmitSucceeded,
formErrors,
handleChange,
isLoading,
isLoadingForPermissions,
modifiedData,
} = useForm('email-templates', updatePermissions);
} = useRBAC(updatePermissions);
const emailTemplates = useMemo(() => {
return Object.keys(modifiedData).reduce((acc, current) => {
const { display, icon } = modifiedData[current];
const { status: isLoadingData, data } = useQuery('email-templates', () => fetchData(), {
onSuccess: () => {
notifyStatus(
formatMessage({
id: getTrad('Email.template.data.loaded'),
defaultMessage: 'Email templates has been loaded',
})
);
},
onError: () => {
toggleNotification({
type: 'warning',
message: { id: 'notification.error', defaultMessage: 'An error occured' },
});
},
});
acc.push({
id: current,
name: formatMessage({ id: getTrad(display) }),
icon: ['fas', icon],
const isLoading = isLoadingForPermissions || isLoadingData !== 'success';
const handleToggle = () => {
setIsModalOpen(prev => !prev);
};
const handleEditClick = template => {
setTemplateToEdit(template);
handleToggle();
};
const submitMutation = useMutation(body => putEmailTemplate({ 'email-templates': body }), {
onSuccess: async () => {
await queryClient.invalidateQueries('email-templates');
toggleNotification({
type: 'success',
message: { id: 'notification.success.saved', defaultMessage: 'Saved' },
});
return acc;
}, []);
}, [modifiedData, formatMessage]);
trackUsageRef.current('didEditEmailTemplates');
const listTitle = useMemo(() => {
const count = emailTemplates.length;
return formatMessage(
{
id: getTrad(`List.title.emailTemplates.${count > 1 ? 'plural' : 'singular'}`),
},
{ number: count }
);
}, [emailTemplates.length, formatMessage]);
const handleClosed = useCallback(() => {
setTemplateToEdit(null);
setShowForm(false);
dispatchResetForm();
}, [dispatchResetForm]);
const handleToggle = useCallback(() => {
setIsOpen(prev => !prev);
}, []);
const handleClickEdit = useCallback(
template => {
setTemplateToEdit(template);
unlockApp();
handleToggle();
},
[handleToggle]
);
const handleSubmit = useCallback(
async e => {
e.preventDefault();
let errors = {};
try {
setIsSubmiting(true);
await schema.validate(modifiedData[templateToEdit.id], { abortEarly: false });
lockApp();
try {
trackUsageRef.current('willEditEmailTemplates');
await request(getRequestURL('email-templates'), {
method: 'PUT',
body: { 'email-templates': modifiedData },
});
trackUsageRef.current('didEditEmailTemplates');
toggleNotification({
type: 'success',
message: { id: getTrad('notification.success.submit') },
});
dispatchSubmitSucceeded();
handleToggle();
} catch (err) {
console.error(err);
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
}
} catch (err) {
errors = getYupInnerErrors(err);
} finally {
setIsSubmiting(false);
unlockApp();
}
dispatchSetFormErrors(errors);
onError: () => {
toggleNotification({
type: 'warning',
message: { id: 'notification.error', defaultMessage: 'An error occured' },
});
unlockApp();
},
[
dispatchSetFormErrors,
dispatchSubmitSucceeded,
modifiedData,
templateToEdit,
handleToggle,
toggleNotification,
lockApp,
unlockApp,
]
);
refetchActive: true,
});
const { isLoading: isSubmittingForm } = submitMutation;
const handleClick = useCallback(() => {
buttonSubmitRef.current.click();
}, []);
const handleSubmit = body => {
lockApp();
trackUsageRef.current('willEditEmailTemplates');
const handleOpened = useCallback(() => {
setShowForm(true);
}, []);
const editedTemplates = { ...data, [templateToEdit]: body };
submitMutation.mutate(editedTemplates);
};
if (isLoading) {
return (
<Main aria-busy="true">
<SettingsPageTitle
name={formatMessage({
id: getTrad('HeaderNav.link.emailTemplates'),
defaultMessage: 'Email templates',
})}
/>
<HeaderLayout
title={formatMessage({
id: getTrad('HeaderNav.link.emailTemplates'),
defaultMessage: 'Email templates',
})}
/>
<ContentLayout>
<LoadingIndicatorPage />
</ContentLayout>
</Main>
);
}
return (
<>
<SettingsPageTitle name={pageTitle} />
<div>
<Header title={{ label: pageTitle }} isLoading={isLoadingForPermissions || isLoading} />
<ListBaselineAlignment />
<List
title={listTitle}
items={emailTemplates}
isLoading={isLoadingForPermissions || isLoading}
customRowComponent={template => (
<ListRow
{...template}
onClick={() => {
if (canUpdate) {
handleClickEdit(template);
}
}}
links={[
{
icon: canUpdate ? <Pencil fill="#0e1622" /> : null,
onClick: e => {
e.stopPropagation();
handleClickEdit(template);
},
},
]}
/>
)}
/>
</div>
<ModalForm
isOpen={isOpen}
onOpened={handleOpened}
onToggle={handleToggle}
onClosed={handleClosed}
headerBreadcrumbs={[
getTrad('PopUpForm.header.edit.email-templates'),
get(templateToEdit, 'name', ''),
]}
onClick={handleClick}
onCancel={handleToggle}
isLoading={isSubmiting}
>
{showForm && (
<form onSubmit={handleSubmit}>
<Row>
{forms.map(input => {
const id = get(templateToEdit, 'id');
return (
<SizedInput
key={input.name}
{...input}
error={formErrors[input.name]}
name={`${id}.${input.name}`}
onChange={handleChange}
value={get(modifiedData, [id, ...input.name.split('.')], '')}
/>
);
})}
</Row>
<button type="submit" style={{ display: 'none' }} ref={buttonSubmitRef}>
hidden button to use the native form event
</button>
</form>
<Main aria-busy={isSubmittingForm}>
<SettingsPageTitle
name={formatMessage({
id: getTrad('HeaderNav.link.emailTemplates'),
defaultMessage: 'Email templates',
})}
/>
<HeaderLayout
title={formatMessage({
id: getTrad('HeaderNav.link.emailTemplates'),
defaultMessage: 'Email templates',
})}
/>
<ContentLayout>
<EmailTable onEditClick={handleEditClick} canUpdate={canUpdate} />
{isModalOpen && (
<EmailForm
template={data[templateToEdit]}
onToggle={handleToggle}
onSubmit={handleSubmit}
/>
)}
</ModalForm>
</>
</ContentLayout>
</Main>
);
};

View File

@ -1,58 +0,0 @@
import produce from 'immer';
import { set } from 'lodash';
const initialState = {
formErrors: {},
isLoading: true,
initialData: {},
modifiedData: {},
};
const reducer = (state, action) =>
// eslint-disable-next-line consistent-return
produce(state, draftState => {
switch (action.type) {
case 'GET_DATA': {
draftState.isLoading = true;
draftState.initialData = {};
draftState.modifiedData = {};
break;
}
case 'GET_DATA_SUCCEEDED': {
draftState.isLoading = false;
draftState.initialData = action.data;
draftState.modifiedData = action.data;
break;
}
case 'GET_DATA_ERROR': {
draftState.isLoading = true;
break;
}
case 'ON_CHANGE': {
set(draftState, ['modifiedData', ...action.keys.split('.')], action.value);
break;
}
case 'ON_SUBMIT_SUCCEEDED': {
draftState.initialData = state.modifiedData;
break;
}
case 'RESET_FORM': {
draftState.modifiedData = state.initialData;
draftState.formErrors = {};
break;
}
case 'SET_ERRORS': {
draftState.formErrors = action.errors;
break;
}
default: {
return draftState;
}
}
});
export default reducer;
export { initialState };

View File

@ -0,0 +1,576 @@
import React from 'react';
import { render, waitFor, screen } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ThemeProvider, lightTheme } from '@strapi/parts';
import { useRBAC } from '@strapi/helper-plugin';
import ProtectedEmailTemplatesPage from '../index';
import server from './utils/server';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useNotification: jest.fn(),
useFocusWhenNavigate: jest.fn(),
useOverlayBlocker: jest.fn(() => ({ lockApp: jest.fn, unlockApp: jest.fn() })),
useRBAC: jest.fn(),
CheckPagePermissions: ({ children }) => children,
// useTracking: jest.fn(() => ({
// trackUsage: jest.fn()
// }))
}));
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const App = (
<QueryClientProvider client={client}>
<IntlProvider messages={{ en: {} }} textComponent="span" locale="en">
<ThemeProvider theme={lightTheme}>
<ProtectedEmailTemplatesPage />
</ThemeProvider>
</IntlProvider>
</QueryClientProvider>
);
describe('ADMIN | Pages | Settings | Advanced Settings', () => {
beforeAll(() => server.listen());
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
jest.resetAllMocks();
server.close();
});
it('renders and matches the snapshot', async () => {
useRBAC.mockImplementation(() => ({
isLoading: false,
allowedActions: { canUpdate: true },
}));
const { container } = render(App);
await waitFor(() => {
expect(screen.getByText('Reset password')).toBeInTheDocument();
});
expect(container.firstChild).toMatchInlineSnapshot(`
.c24 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
cursor: pointer;
padding: 8px;
border-radius: 4px;
background: #ffffff;
border: 1px solid #dcdce4;
}
.c24 svg {
height: 12px;
width: 12px;
}
.c24 svg > g,
.c24 svg path {
fill: #ffffff;
}
.c24[aria-disabled='true'] {
pointer-events: none;
}
.c25 {
border: none;
}
.c25 svg > g,
.c25 svg path {
fill: #8e8ea9;
}
.c25:hover svg > g,
.c25:hover svg path {
fill: #666687;
}
.c25:active svg > g,
.c25:active svg path {
fill: #a5a5ba;
}
.c25[aria-disabled='true'] {
background-color: #eaeaef;
}
.c25[aria-disabled='true'] svg path {
fill: #666687;
}
.c8 {
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
}
.c9 {
background: #ffffff;
}
.c11 {
padding-right: 24px;
padding-left: 24px;
}
.c13 {
width: 100%;
white-space: nowrap;
}
.c10 {
position: relative;
border-radius: 4px 4px 0 0;
}
.c10:before {
background: linear-gradient(90deg,#000000 0%,rgba(0,0,0,0) 100%);
opacity: 0.2;
position: absolute;
height: 100%;
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
width: 8px;
left: 0;
}
.c10:after {
background: linear-gradient(270deg,#000000 0%,rgba(0,0,0,0) 100%);
opacity: 0.2;
position: absolute;
height: 100%;
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
width: 8px;
right: 0;
top: 0;
}
.c12 {
overflow-x: auto;
}
.c23 tr:last-of-type {
border-bottom: none;
}
.c14 {
border-bottom: 1px solid #eaeaef;
}
.c15 {
border-bottom: 1px solid #eaeaef;
}
.c15 td,
.c15 th {
padding: 16px;
}
.c15 td:first-of-type,
.c15 th:first-of-type {
padding: 0 4px;
}
.c17 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c16 {
vertical-align: middle;
text-align: left;
color: #666687;
outline-offset: -4px;
}
.c16 input {
vertical-align: sub;
}
.c19 svg {
height: 0.25rem;
}
.c20 {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
.c21 {
font-weight: 600;
line-height: 1.14;
}
.c22 {
font-weight: 600;
font-size: 0.6875rem;
line-height: 1.45;
text-transform: uppercase;
}
.c0 {
outline: none;
}
.c1 {
background: #f6f6f9;
padding-top: 56px;
padding-right: 56px;
padding-bottom: 56px;
padding-left: 56px;
}
.c7 {
padding-right: 56px;
padding-left: 56px;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c4 {
font-weight: 600;
font-size: 2rem;
line-height: 1.25;
color: #32324d;
}
.c5 {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #666687;
}
.c6 {
font-size: 1rem;
line-height: 1.5;
}
.c18 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
<main
aria-busy="false"
aria-labelledby="main-content-title"
class="c0"
id="main-content"
tabindex="-1"
>
<div
style="height: 0px;"
>
<div
class="c1"
data-strapi-header="true"
>
<div
class="c2"
>
<div
class="c3"
>
<h1
class="c4"
id="main-content-title"
>
Email templates
</h1>
</div>
</div>
<p
class="c5 c6"
/>
</div>
</div>
<div
class="c7"
>
<div
class="c8"
>
<div
class="c9 c10"
>
<div
class="c11 c12"
>
<table
aria-colcount="3"
aria-rowcount="3"
class="c13"
>
<thead
class="c14"
>
<tr
aria-rowindex="1"
class="c15"
>
<th
aria-colindex="1"
class="c16"
tabindex="0"
width="1%"
>
<div
class="c17"
>
<div
class="c18"
>
icon
</div>
<span
class="c19"
/>
</div>
</th>
<th
aria-colindex="2"
class="c16"
tabindex="-1"
>
<div
class="c17"
>
<span
class="c20 c21 c22"
>
name
</span>
<span
class="c19"
/>
</div>
</th>
<th
aria-colindex="3"
class="c16"
tabindex="-1"
width="1%"
>
<div
class="c17"
>
<div
class="c18"
>
action
</div>
<span
class="c19"
/>
</div>
</th>
</tr>
</thead>
<tbody
class="c23"
>
<tr
aria-rowindex="2"
class="c15"
>
<td
aria-colindex="1"
class="c16"
tabindex="-1"
>
<svg
aria-label="Reset password"
fill="none"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M15.681 2.804A9.64 9.64 0 0011.818 2C6.398 2 2 6.48 2 12c0 5.521 4.397 10 9.818 10 2.03 0 4.011-.641 5.67-1.835a9.987 9.987 0 003.589-4.831 1.117 1.117 0 00-.664-1.418 1.086 1.086 0 00-1.393.676 7.769 7.769 0 01-2.792 3.758 7.546 7.546 0 01-4.41 1.428V4.222h.002a7.492 7.492 0 013.003.625 7.61 7.61 0 012.5 1.762l.464.551-2.986 3.042a.186.186 0 00.129.316H22V3.317a.188.188 0 00-.112-.172.179.179 0 00-.199.04l-2.355 2.4-.394-.468-.02-.02a9.791 9.791 0 00-3.239-2.293zm-3.863 1.418V2v2.222zm0 0v15.556c-4.216 0-7.636-3.484-7.636-7.778s3.42-7.777 7.636-7.778z"
fill="#212134"
fill-rule="evenodd"
/>
</svg>
</td>
<td
aria-colindex="2"
class="c16"
tabindex="-1"
>
<span
class="c20"
>
Reset password
</span>
</td>
<td
aria-colindex="3"
class="c16"
>
<span>
<button
aria-disabled="false"
aria-labelledby="tooltip-1"
class="c24 c25"
tabindex="-1"
type="button"
>
<svg
fill="none"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M23.604 3.514c.528.528.528 1.36 0 1.887l-2.622 2.607-4.99-4.99L18.6.396a1.322 1.322 0 011.887 0l3.118 3.118zM0 24v-4.99l14.2-14.2 4.99 4.99L4.99 24H0z"
fill="#212134"
fill-rule="evenodd"
/>
</svg>
</button>
</span>
</td>
</tr>
<tr
aria-rowindex="3"
class="c15"
>
<td
aria-colindex="1"
class="c16"
tabindex="-1"
>
<svg
aria-label="Email address confirmation"
fill="none"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.727 2.97a.2.2 0 01.286 0l2.85 2.89a.2.2 0 010 .28L9.554 20.854a.2.2 0 01-.285 0l-9.13-9.243a.2.2 0 010-.281l2.85-2.892a.2.2 0 01.284 0l6.14 6.209L20.726 2.97z"
fill="#212134"
/>
</svg>
</td>
<td
aria-colindex="2"
class="c16"
tabindex="-1"
>
<span
class="c20"
>
Email address confirmation
</span>
</td>
<td
aria-colindex="3"
class="c16"
>
<span>
<button
aria-disabled="false"
aria-labelledby="tooltip-3"
class="c24 c25"
tabindex="-1"
type="button"
>
<svg
fill="none"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M23.604 3.514c.528.528.528 1.36 0 1.887l-2.622 2.607-4.99-4.99L18.6.396a1.322 1.322 0 011.887 0l3.118 3.118zM0 24v-4.99l14.2-14.2 4.99 4.99L4.99 24H0z"
fill="#212134"
fill-rule="evenodd"
/>
</svg>
</button>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
`);
});
});

View File

@ -0,0 +1,41 @@
import { setupServer } from 'msw/node';
import { rest } from 'msw';
const handlers = [
rest.get('*/email-templates', (req, res, ctx) => {
return res(
ctx.delay(100),
ctx.status(200),
ctx.json({
email_confirmation: {
display: 'Email.template.email_confirmation',
options: {
from: {
email: 'mochoko@strapi.io',
name: 'Administration Panel',
},
message: 'Thank you for registering. Please click on the link below.',
object: 'Account confirmation',
response_email: '',
},
},
reset_password: {
display: 'Email.template.reset_password',
options: {
from: {
email: 'mochoko@strapi.io',
name: 'Administration Panel',
},
message: 'We heard that you lost your password. Sorry about that!',
object: 'Reset password',
response_email: '',
},
},
})
);
}),
];
const server = setupServer(...handlers);
export default server;

View File

@ -0,0 +1,13 @@
import { axiosInstance, getRequestURL } from '../../../utils';
const fetchData = async () => {
const { data } = await axiosInstance.get(getRequestURL('email-templates'));
return data;
};
const putEmailTemplate = body => {
return axiosInstance.put(getRequestURL('email-templates'), body);
};
export { fetchData, putEmailTemplate };

View File

@ -1,81 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { getTrad } from '../../../utils';
import CustomTextInput from '../CustomTextInput';
const forms = [
{
autoFocus: true,
label: getTrad('PopUpForm.Email.options.from.name.label'),
name: 'options.from.name',
type: 'text',
placeholder: getTrad('PopUpForm.Email.options.from.name.placeholder'),
size: { xs: 6 },
validations: {
required: true,
},
},
{
autoFocus: false,
label: getTrad('PopUpForm.Email.options.from.email.label'),
name: 'options.from.email',
type: 'email',
placeholder: getTrad('PopUpForm.Email.options.from.email.placeholder'),
size: { xs: 6 },
validations: {
required: true,
},
},
{
autoFocus: false,
label: getTrad('PopUpForm.Email.options.response_email.label'),
name: 'options.response_email',
type: 'email',
placeholder: getTrad('PopUpForm.Email.options.response_email.placeholder'),
size: { xs: 6 },
validations: {
required: true,
},
},
{
autoFocus: false,
label: getTrad('PopUpForm.Email.options.object.label'),
name: 'options.object',
type: 'customText',
placeholder: getTrad('PopUpForm.Email.options.object.placeholder'),
customInputs: { customText: CustomTextInput },
descriptione: () => (
<FormattedMessage
id={getTrad('PopUpForm.Email.email_templates.inputDescription')}
values={{
link: (
<a
href="https://strapi.io/documentation/developer-docs/latest/development/plugins/users-permissions.html#templating-emails"
target="_blank"
rel="noopener noreferrer"
>
<FormattedMessage id={getTrad('PopUpForm.Email.link.documentation')} />
</a>
),
}}
/>
),
size: { xs: 6 },
validations: {
required: true,
},
},
{
autoFocus: false,
label: getTrad('PopUpForm.Email.options.message.label'),
name: 'options.message',
type: 'textarea',
style: { height: '15.6rem' },
size: { xs: 12 },
validations: {
required: true,
},
},
];
export default forms;

View File

@ -18,8 +18,13 @@
"EditPage.form.roles": "Role details",
"Email.template.email_confirmation": "Email address confirmation",
"Email.template.reset_password": "Reset password",
"Email.template.table.name.label": "name",
"Email.template.table.icon.label": "icon",
"Email.template.table.action.label": "action",
"Email.template.data.loaded": "Email templates has been loaded",
"Email.template.form.edit.label": "Edit a template",
"HeaderNav.link.advancedSettings": "Advanced settings",
"HeaderNav.link.emailTemplates": "Email Templates",
"HeaderNav.link.emailTemplates": "Email templates",
"HeaderNav.link.providers": "Providers",
"HeaderNav.link.roles": "Roles",
"Form.title.advancedSettings": "Settings",
@ -52,7 +57,7 @@
"PopUpForm.Providers.secret.placeholder": "TEXT",
"PopUpForm.Providers.subdomain.label": "Host URI (Subdomain)",
"PopUpForm.Providers.subdomain.placeholder": "my.subdomain.com",
"PopUpForm.header.edit.email-templates": "Edit Email Templates",
"PopUpForm.header.edit.email-templates": "Edit email template",
"PopUpForm.header.edit.providers": "Edit Provider",
"Settings.roles.deleted": "Role deleted",
"Settings.roles.edited": "Role edited",
@ -76,5 +81,15 @@
"popUpWarning.button.cancel": "Cancel",
"popUpWarning.button.confirm": "Confirm",
"popUpWarning.title": "Please confirm",
"popUpWarning.warning.cancel": "Are you sure you want to cancel your modifications?"
"popUpWarning.warning.cancel": "Are you sure you want to cancel your modifications?",
"components.Input.error.validation.email": "This is an invalid email",
"components.Input.error.validation.json": "This doesn't match the JSON format",
"components.Input.error.validation.max": "The value is too high.",
"components.Input.error.validation.maxLength": "The value is too long.",
"components.Input.error.validation.min": "The value is too low.",
"components.Input.error.validation.minLength": "The value is too short.",
"components.Input.error.validation.minSupMax": "Can't be superior",
"components.Input.error.validation.regex": "The value does not match the regex.",
"components.Input.error.validation.required": "This value is required.",
"components.Input.error.validation.unique": "This value is already used."
}