mirror of
https://github.com/strapi/strapi.git
synced 2025-08-16 04:34:40 +00:00
[Email] Migrate to typescript (#18136)
Co-authored-by: Josh <37798644+joshuaellis@users.noreply.github.com>
This commit is contained in:
parent
0b13b1b50a
commit
a9552a70bf
@ -12,7 +12,10 @@ const LazyCompo = ({ loadComponent }) => {
|
|||||||
try {
|
try {
|
||||||
const loadedCompo = await loadComponent();
|
const loadedCompo = await loadComponent();
|
||||||
|
|
||||||
setCompo(() => loadedCompo.default);
|
// TODO the loaded component provided can currently come from a default or named export
|
||||||
|
// We will move the entire codebase to use named exports only
|
||||||
|
// Until then we support both cases with priority given to the existing default exports
|
||||||
|
setCompo(() => loadedCompo?.default ?? loadedCompo);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// TODO return the error component
|
// TODO return the error component
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
@ -2,6 +2,16 @@ root = true
|
|||||||
|
|
||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = false
|
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
|
||||||
|
[{package.json,*.yml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.eslintrc.js
|
.eslintrc.js
|
||||||
index.d.ts
|
index.d.ts
|
||||||
|
dist/
|
||||||
|
@ -7,7 +7,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*'],
|
files: ['**/*'],
|
||||||
excludedFiles: ['admin/**/*'],
|
excludedFiles: ['admin/**/*', 'server/**/*'],
|
||||||
extends: ['custom/back'],
|
extends: ['custom/back'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
4
packages/core/email/admin/.eslintrc.js
Normal file
4
packages/core/email/admin/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['custom/front/typescript'],
|
||||||
|
};
|
@ -9,8 +9,12 @@ import { prefixPluginTranslations } from '@strapi/helper-plugin';
|
|||||||
|
|
||||||
import { PERMISSIONS } from './constants';
|
import { PERMISSIONS } from './constants';
|
||||||
|
|
||||||
export default {
|
import type { Plugin } from '@strapi/types';
|
||||||
register(app) {
|
|
||||||
|
const admin: Plugin.Config.AdminInput = {
|
||||||
|
// TODO typing app in strapi/types as every plugin needs it
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
register(app: any) {
|
||||||
// Create the email settings section
|
// Create the email settings section
|
||||||
app.createSettingSection(
|
app.createSettingSection(
|
||||||
{
|
{
|
||||||
@ -26,11 +30,11 @@ export default {
|
|||||||
id: 'settings',
|
id: 'settings',
|
||||||
to: `/settings/email`,
|
to: `/settings/email`,
|
||||||
async Component() {
|
async Component() {
|
||||||
const component = await import(
|
const { ProtectedSettingsPage } = await import(
|
||||||
/* webpackChunkName: "email-settings-page" */ './pages/Settings'
|
/* webpackChunkName: "email-settings-page" */ './pages/Settings'
|
||||||
);
|
);
|
||||||
|
|
||||||
return component;
|
return ProtectedSettingsPage;
|
||||||
},
|
},
|
||||||
permissions: PERMISSIONS.settings,
|
permissions: PERMISSIONS.settings,
|
||||||
},
|
},
|
||||||
@ -41,8 +45,9 @@ export default {
|
|||||||
name: 'email',
|
name: 'email',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
bootstrap() {},
|
bootstrap() {},
|
||||||
async registerTrads({ locales }) {
|
async registerTrads({ locales }: { locales: string[] }) {
|
||||||
const importedTrads = await Promise.all(
|
const importedTrads = await Promise.all(
|
||||||
locales.map((locale) => {
|
locales.map((locale) => {
|
||||||
return import(
|
return import(
|
||||||
@ -66,3 +71,6 @@ export default {
|
|||||||
return Promise.resolve(importedTrads);
|
return Promise.resolve(importedTrads);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default admin;
|
324
packages/core/email/admin/src/pages/Settings.tsx
Normal file
324
packages/core/email/admin/src/pages/Settings.tsx
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ContentLayout,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
HeaderLayout,
|
||||||
|
Main,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
TextInput,
|
||||||
|
Typography,
|
||||||
|
} from '@strapi/design-system';
|
||||||
|
import {
|
||||||
|
CheckPagePermissions,
|
||||||
|
getYupInnerErrors,
|
||||||
|
LoadingIndicatorPage,
|
||||||
|
SettingsPageTitle,
|
||||||
|
useFetchClient,
|
||||||
|
useFocusWhenNavigate,
|
||||||
|
useNotification,
|
||||||
|
useOverlayBlocker,
|
||||||
|
} from '@strapi/helper-plugin';
|
||||||
|
import { Envelop } from '@strapi/icons';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useQuery, useMutation } from 'react-query';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { ValidationError } from 'yup';
|
||||||
|
|
||||||
|
import { PERMISSIONS } from '../constants';
|
||||||
|
import { schema } from '../utils/schema';
|
||||||
|
|
||||||
|
import type { EmailSettings } from '../../../shared/types';
|
||||||
|
|
||||||
|
const DocumentationLink = styled.a`
|
||||||
|
color: ${({ theme }) => theme.colors.primary600};
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface MutationBody {
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProtectedSettingsPage = () => (
|
||||||
|
<CheckPagePermissions permissions={PERMISSIONS.settings}>
|
||||||
|
<SettingsPage />
|
||||||
|
</CheckPagePermissions>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SettingsPage = () => {
|
||||||
|
const toggleNotification = useNotification();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { lockApp, unlockApp } = useOverlayBlocker();
|
||||||
|
const { get, post } = useFetchClient();
|
||||||
|
|
||||||
|
const [testAddress, setTestAddress] = React.useState('');
|
||||||
|
const [isTestAddressValid, setIsTestAddressValid] = React.useState(false);
|
||||||
|
|
||||||
|
// TODO: I'm not sure how to type this. I think it should be Record<string, TranslationMessage> but that type is defined in the helper-plugin
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const [formErrors, setFormErrors] = React.useState<Record<string, any>>({});
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery(['email', 'settings'], async () => {
|
||||||
|
const res = await get<EmailSettings>('/email/settings');
|
||||||
|
const {
|
||||||
|
data: { config },
|
||||||
|
} = res;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutation = useMutation<void, Error, MutationBody>(
|
||||||
|
async (body) => {
|
||||||
|
await post('/email/test', body);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError() {
|
||||||
|
toggleNotification!({
|
||||||
|
type: 'warning',
|
||||||
|
message: formatMessage(
|
||||||
|
{
|
||||||
|
id: 'email.Settings.email.plugin.notification.test.error',
|
||||||
|
defaultMessage: 'Failed to send a test mail to {to}',
|
||||||
|
},
|
||||||
|
{ to: testAddress }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
toggleNotification!({
|
||||||
|
type: 'success',
|
||||||
|
message: formatMessage(
|
||||||
|
{
|
||||||
|
id: 'email.Settings.email.plugin.notification.test.success',
|
||||||
|
defaultMessage: 'Email test succeeded, check the {to} mailbox',
|
||||||
|
},
|
||||||
|
{ to: testAddress }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
retry: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useFocusWhenNavigate();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
schema
|
||||||
|
.validate({ email: testAddress }, { abortEarly: false })
|
||||||
|
.then(() => setIsTestAddressValid(true))
|
||||||
|
.catch(() => setIsTestAddressValid(false));
|
||||||
|
}, [testAddress]);
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTestAddress(() => event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await schema.validate({ email: testAddress }, { abortEarly: false });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
setFormErrors(getYupInnerErrors(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lockApp!();
|
||||||
|
|
||||||
|
mutation.mutate({ to: testAddress });
|
||||||
|
|
||||||
|
unlockApp!();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Main labelledBy="title" aria-busy={isLoading || mutation.isLoading}>
|
||||||
|
<SettingsPageTitle
|
||||||
|
name={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.title',
|
||||||
|
defaultMessage: 'Configuration',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HeaderLayout
|
||||||
|
id="title"
|
||||||
|
title={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.title',
|
||||||
|
defaultMessage: 'Configuration',
|
||||||
|
})}
|
||||||
|
subtitle={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.subTitle',
|
||||||
|
defaultMessage: 'Test the settings for the Email plugin',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ContentLayout>
|
||||||
|
{isLoading ? (
|
||||||
|
<LoadingIndicatorPage />
|
||||||
|
) : (
|
||||||
|
data && (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Flex direction="column" alignItems="stretch" gap={7}>
|
||||||
|
<Box
|
||||||
|
background="neutral0"
|
||||||
|
hasRadius
|
||||||
|
shadow="filterShadow"
|
||||||
|
paddingTop={6}
|
||||||
|
paddingBottom={6}
|
||||||
|
paddingLeft={7}
|
||||||
|
paddingRight={7}
|
||||||
|
>
|
||||||
|
<Flex direction="column" alignItems="stretch" gap={4}>
|
||||||
|
<Flex direction="column" alignItems="stretch" gap={1}>
|
||||||
|
<Typography variant="delta" as="h2">
|
||||||
|
{formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.title.config',
|
||||||
|
defaultMessage: 'Configuration',
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{formatMessage(
|
||||||
|
{
|
||||||
|
id: 'email.Settings.email.plugin.text.configuration',
|
||||||
|
defaultMessage:
|
||||||
|
'The plugin is configured through the {file} file, checkout this {link} for the documentation.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: './config/plugins.js',
|
||||||
|
link: (
|
||||||
|
<DocumentationLink
|
||||||
|
href="https://docs.strapi.io/developer-docs/latest/plugins/email.html"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'email.link',
|
||||||
|
defaultMessage: 'Link',
|
||||||
|
})}
|
||||||
|
</DocumentationLink>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Grid gap={5}>
|
||||||
|
<GridItem col={6} s={12}>
|
||||||
|
<TextInput
|
||||||
|
name="shipper-email"
|
||||||
|
label={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.label.defaultFrom',
|
||||||
|
defaultMessage: 'Default sender email',
|
||||||
|
})}
|
||||||
|
placeholder={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.placeholder.defaultFrom',
|
||||||
|
defaultMessage: "ex: Strapi No-Reply '<'no-reply@strapi.io'>'",
|
||||||
|
})}
|
||||||
|
disabled
|
||||||
|
value={data.settings.defaultFrom}
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
|
<GridItem col={6} s={12}>
|
||||||
|
<TextInput
|
||||||
|
name="response-email"
|
||||||
|
label={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.label.defaultReplyTo',
|
||||||
|
defaultMessage: 'Default response email',
|
||||||
|
})}
|
||||||
|
placeholder={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.placeholder.defaultReplyTo',
|
||||||
|
defaultMessage: `ex: Strapi '<'example@strapi.io'>'`,
|
||||||
|
})}
|
||||||
|
disabled
|
||||||
|
value={data.settings.defaultReplyTo}
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
|
<GridItem col={6} s={12}>
|
||||||
|
<Select
|
||||||
|
name="email-provider"
|
||||||
|
label={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.label.provider',
|
||||||
|
defaultMessage: 'Email provider',
|
||||||
|
})}
|
||||||
|
disabled
|
||||||
|
value={data.provider}
|
||||||
|
>
|
||||||
|
<Option value={data.provider}>{data.provider}</Option>
|
||||||
|
</Select>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
alignItems="stretch"
|
||||||
|
background="neutral0"
|
||||||
|
direction="column"
|
||||||
|
gap={4}
|
||||||
|
hasRadius
|
||||||
|
shadow="filterShadow"
|
||||||
|
paddingTop={6}
|
||||||
|
paddingBottom={6}
|
||||||
|
paddingLeft={7}
|
||||||
|
paddingRight={7}
|
||||||
|
>
|
||||||
|
<Typography variant="delta" as="h2">
|
||||||
|
{formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.title.test',
|
||||||
|
defaultMessage: 'Test email delivery',
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid gap={5}>
|
||||||
|
<GridItem col={6} s={12}>
|
||||||
|
<TextInput
|
||||||
|
id="test-address-input"
|
||||||
|
name="test-address"
|
||||||
|
onChange={handleChange}
|
||||||
|
label={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.label.testAddress',
|
||||||
|
defaultMessage: 'Recipient email',
|
||||||
|
})}
|
||||||
|
value={testAddress}
|
||||||
|
error={
|
||||||
|
formErrors.email?.id &&
|
||||||
|
formatMessage({
|
||||||
|
id: `email.${formErrors.email?.id}`,
|
||||||
|
defaultMessage: 'This is an invalid email',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder={formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.placeholder.testAddress',
|
||||||
|
defaultMessage: 'ex: developer@example.com',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem col={7} s={12}>
|
||||||
|
<Button
|
||||||
|
loading={mutation.isLoading}
|
||||||
|
disabled={!isTestAddressValid}
|
||||||
|
type="submit"
|
||||||
|
startIcon={<Envelop />}
|
||||||
|
>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'email.Settings.email.plugin.button.test-email',
|
||||||
|
defaultMessage: 'Send test email',
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ContentLayout>
|
||||||
|
</Main>
|
||||||
|
);
|
||||||
|
};
|
@ -1,314 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
ContentLayout,
|
|
||||||
Flex,
|
|
||||||
Grid,
|
|
||||||
GridItem,
|
|
||||||
HeaderLayout,
|
|
||||||
Main,
|
|
||||||
Option,
|
|
||||||
Select,
|
|
||||||
TextInput,
|
|
||||||
Typography,
|
|
||||||
} from '@strapi/design-system';
|
|
||||||
import {
|
|
||||||
CheckPagePermissions,
|
|
||||||
getYupInnerErrors,
|
|
||||||
LoadingIndicatorPage,
|
|
||||||
SettingsPageTitle,
|
|
||||||
useFetchClient,
|
|
||||||
useFocusWhenNavigate,
|
|
||||||
useNotification,
|
|
||||||
useOverlayBlocker,
|
|
||||||
} from '@strapi/helper-plugin';
|
|
||||||
import { Envelop } from '@strapi/icons';
|
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import { useQuery, useMutation } from 'react-query';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { PERMISSIONS } from '../../constants';
|
|
||||||
import schema from '../../utils/schema';
|
|
||||||
|
|
||||||
const DocumentationLink = styled.a`
|
|
||||||
color: ${({ theme }) => theme.colors.primary600};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ProtectedSettingsPage = () => (
|
|
||||||
<CheckPagePermissions permissions={PERMISSIONS.settings}>
|
|
||||||
<SettingsPage />
|
|
||||||
</CheckPagePermissions>
|
|
||||||
);
|
|
||||||
|
|
||||||
const SettingsPage = () => {
|
|
||||||
const toggleNotification = useNotification();
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const { lockApp, unlockApp } = useOverlayBlocker();
|
|
||||||
const { get, post } = useFetchClient();
|
|
||||||
const { data, isLoading } = useQuery(['email', 'settings'], async () => {
|
|
||||||
const {
|
|
||||||
data: { config },
|
|
||||||
} = await get('/email/settings');
|
|
||||||
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
const mutation = useMutation(
|
|
||||||
(body) => post('/email/test', body),
|
|
||||||
{
|
|
||||||
onError() {
|
|
||||||
toggleNotification({
|
|
||||||
type: 'warning',
|
|
||||||
message: formatMessage(
|
|
||||||
{
|
|
||||||
id: 'email.Settings.email.plugin.notification.test.error',
|
|
||||||
defaultMessage: 'Failed to send a test mail to {to}',
|
|
||||||
},
|
|
||||||
{ to: testAddress }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuccess() {
|
|
||||||
toggleNotification({
|
|
||||||
type: 'success',
|
|
||||||
message: formatMessage(
|
|
||||||
{
|
|
||||||
id: 'email.Settings.email.plugin.notification.test.success',
|
|
||||||
defaultMessage: 'Email test succeeded, check the {to} mailbox',
|
|
||||||
},
|
|
||||||
{ to: testAddress }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
retry: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
useFocusWhenNavigate();
|
|
||||||
|
|
||||||
const [formErrors, setFormErrors] = React.useState({});
|
|
||||||
const [testAddress, setTestAddress] = React.useState('');
|
|
||||||
const [isTestAddressValid, setIsTestAddressValid] = React.useState(false);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
schema
|
|
||||||
.validate({ email: testAddress }, { abortEarly: false })
|
|
||||||
.then(() => setIsTestAddressValid(true))
|
|
||||||
.catch(() => setIsTestAddressValid(false));
|
|
||||||
}, [testAddress]);
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
setTestAddress(() => e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await schema.validate({ email: testAddress }, { abortEarly: false });
|
|
||||||
} catch (error) {
|
|
||||||
setFormErrors(getYupInnerErrors(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
lockApp();
|
|
||||||
|
|
||||||
mutation.mutate({ to: testAddress });
|
|
||||||
|
|
||||||
unlockApp();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Main labelledBy="title" aria-busy={isLoading || mutation.isLoading}>
|
|
||||||
<SettingsPageTitle
|
|
||||||
name={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.title',
|
|
||||||
defaultMessage: 'Configuration',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HeaderLayout
|
|
||||||
id="title"
|
|
||||||
title={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.title',
|
|
||||||
defaultMessage: 'Configuration',
|
|
||||||
})}
|
|
||||||
subtitle={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.subTitle',
|
|
||||||
defaultMessage: 'Test the settings for the Email plugin',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ContentLayout>
|
|
||||||
{isLoading ? (
|
|
||||||
<LoadingIndicatorPage />
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<Flex direction="column" alignItems="stretch" gap={7}>
|
|
||||||
<Box
|
|
||||||
background="neutral0"
|
|
||||||
hasRadius
|
|
||||||
shadow="filterShadow"
|
|
||||||
paddingTop={6}
|
|
||||||
paddingBottom={6}
|
|
||||||
paddingLeft={7}
|
|
||||||
paddingRight={7}
|
|
||||||
>
|
|
||||||
<Flex direction="column" alignItems="stretch" gap={4}>
|
|
||||||
<Flex direction="column" alignItems="stretch" gap={1}>
|
|
||||||
<Typography variant="delta" as="h2">
|
|
||||||
{formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.title.config',
|
|
||||||
defaultMessage: 'Configuration',
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
{formatMessage(
|
|
||||||
{
|
|
||||||
id: 'email.Settings.email.plugin.text.configuration',
|
|
||||||
defaultMessage:
|
|
||||||
'The plugin is configured through the {file} file, checkout this {link} for the documentation.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: './config/plugins.js',
|
|
||||||
link: (
|
|
||||||
<DocumentationLink
|
|
||||||
href="https://docs.strapi.io/developer-docs/latest/plugins/email.html"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{formatMessage({
|
|
||||||
id: 'email.link',
|
|
||||||
defaultMessage: 'Link',
|
|
||||||
})}
|
|
||||||
</DocumentationLink>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Grid gap={5}>
|
|
||||||
<GridItem col={6} s={12}>
|
|
||||||
<TextInput
|
|
||||||
name="shipper-email"
|
|
||||||
label={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.label.defaultFrom',
|
|
||||||
defaultMessage: 'Default sender email',
|
|
||||||
})}
|
|
||||||
placeholder={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.placeholder.defaultFrom',
|
|
||||||
defaultMessage: "ex: Strapi No-Reply '<'no-reply@strapi.io'>'",
|
|
||||||
})}
|
|
||||||
disabled
|
|
||||||
onChange={() => {}}
|
|
||||||
value={data.settings.defaultFrom}
|
|
||||||
/>
|
|
||||||
</GridItem>
|
|
||||||
|
|
||||||
<GridItem col={6} s={12}>
|
|
||||||
<TextInput
|
|
||||||
name="response-email"
|
|
||||||
label={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.label.defaultReplyTo',
|
|
||||||
defaultMessage: 'Default response email',
|
|
||||||
})}
|
|
||||||
placeholder={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.placeholder.defaultReplyTo',
|
|
||||||
defaultMessage: `ex: Strapi '<'example@strapi.io'>'`,
|
|
||||||
})}
|
|
||||||
disabled
|
|
||||||
onChange={() => {}}
|
|
||||||
value={data.settings.defaultReplyTo}
|
|
||||||
/>
|
|
||||||
</GridItem>
|
|
||||||
|
|
||||||
<GridItem col={6} s={12}>
|
|
||||||
<Select
|
|
||||||
name="email-provider"
|
|
||||||
label={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.label.provider',
|
|
||||||
defaultMessage: 'Email provider',
|
|
||||||
})}
|
|
||||||
disabled
|
|
||||||
onChange={() => {}}
|
|
||||||
value={data.provider}
|
|
||||||
>
|
|
||||||
<Option value={data.provider}>{data.provider}</Option>
|
|
||||||
</Select>
|
|
||||||
</GridItem>
|
|
||||||
</Grid>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
alignItems="stretch"
|
|
||||||
background="neutral0"
|
|
||||||
direction="column"
|
|
||||||
gap={4}
|
|
||||||
hasRadius
|
|
||||||
shadow="filterShadow"
|
|
||||||
paddingTop={6}
|
|
||||||
paddingBottom={6}
|
|
||||||
paddingLeft={7}
|
|
||||||
paddingRight={7}
|
|
||||||
>
|
|
||||||
<Typography variant="delta" as="h2">
|
|
||||||
{formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.title.test',
|
|
||||||
defaultMessage: 'Test email delivery',
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Grid gap={5} alignItems="end">
|
|
||||||
<GridItem col={6} s={12}>
|
|
||||||
<TextInput
|
|
||||||
id="test-address-input"
|
|
||||||
name="test-address"
|
|
||||||
onChange={handleChange}
|
|
||||||
label={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.label.testAddress',
|
|
||||||
defaultMessage: 'Recipient email',
|
|
||||||
})}
|
|
||||||
value={testAddress}
|
|
||||||
error={
|
|
||||||
formErrors.email?.id &&
|
|
||||||
formatMessage({
|
|
||||||
id: `email.${formErrors.email?.id}`,
|
|
||||||
defaultMessage: 'This is an invalid email',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder={formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.placeholder.testAddress',
|
|
||||||
defaultMessage: 'ex: developer@example.com',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</GridItem>
|
|
||||||
<GridItem col={7} s={12}>
|
|
||||||
<Button
|
|
||||||
loading={mutation.isLoading}
|
|
||||||
disabled={!isTestAddressValid}
|
|
||||||
type="submit"
|
|
||||||
startIcon={<Envelop />}
|
|
||||||
>
|
|
||||||
{formatMessage({
|
|
||||||
id: 'email.Settings.email.plugin.button.test-email',
|
|
||||||
defaultMessage: 'Send test email',
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</GridItem>
|
|
||||||
</Grid>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</ContentLayout>
|
|
||||||
</Main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProtectedSettingsPage;
|
|
@ -1,9 +1,11 @@
|
|||||||
const translations = require('../en.json');
|
import translations from '../en.json';
|
||||||
|
|
||||||
|
const typedTranslations: Record<string, string> = translations;
|
||||||
|
|
||||||
describe('translations', () => {
|
describe('translations', () => {
|
||||||
describe('plural syntax', () => {
|
describe('plural syntax', () => {
|
||||||
it('should avoid .plural/.singular syntax', () => {
|
it('should avoid .plural/.singular syntax', () => {
|
||||||
Object.keys(translations).forEach((translationKey) => {
|
Object.keys(typedTranslations).forEach((translationKey) => {
|
||||||
const keyParts = translationKey.split('.');
|
const keyParts = translationKey.split('.');
|
||||||
const lastKeyPart = keyParts.pop();
|
const lastKeyPart = keyParts.pop();
|
||||||
|
|
||||||
@ -13,7 +15,7 @@ describe('translations', () => {
|
|||||||
keyParts.push('plural');
|
keyParts.push('plural');
|
||||||
const pluralKey = keyParts.join('.');
|
const pluralKey = keyParts.join('.');
|
||||||
|
|
||||||
expect(translations[pluralKey]).toBeUndefined();
|
expect(typedTranslations[pluralKey]).toBeUndefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -1,8 +1,6 @@
|
|||||||
import { translatedErrors } from '@strapi/helper-plugin';
|
import { translatedErrors } from '@strapi/helper-plugin';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
|
||||||
const schema = yup.object().shape({
|
export const schema = yup.object().shape({
|
||||||
email: yup.string().email(translatedErrors.email).required(translatedErrors.required),
|
email: yup.string().email(translatedErrors.email).required(translatedErrors.required),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default schema;
|
|
8
packages/core/email/admin/tsconfig.json
Normal file
8
packages/core/email/admin/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "tsconfig/client.json",
|
||||||
|
"include": ["./src", "../shared/types.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "../",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,15 +19,42 @@
|
|||||||
"url": "https://strapi.io"
|
"url": "https://strapi.io"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"exports": {
|
||||||
|
"./strapi-admin": {
|
||||||
|
"types": "./dist/admin/src/index.d.ts",
|
||||||
|
"source": "./admin/src/index.ts",
|
||||||
|
"import": "./dist/admin/index.mjs",
|
||||||
|
"require": "./dist/admin/index.js",
|
||||||
|
"default": "./dist/admin/index.js"
|
||||||
|
},
|
||||||
|
"./strapi-server": {
|
||||||
|
"types": "./dist/server/src/index.d.ts",
|
||||||
|
"source": "./server/src/index.ts",
|
||||||
|
"import": "./dist/server/index.mjs",
|
||||||
|
"require": "./dist/server/index.js",
|
||||||
|
"default": "./dist/server/index.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./dist",
|
||||||
|
"strapi-server.js"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "pack-up build",
|
||||||
|
"clean": "run -T rimraf ./dist",
|
||||||
"lint": "run -T eslint .",
|
"lint": "run -T eslint .",
|
||||||
|
"prepublishOnly": "yarn clean && yarn build",
|
||||||
"test:front": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js",
|
"test:front": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js",
|
||||||
"test:front:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js",
|
"test:front:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js",
|
||||||
"test:front:watch": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js --watchAll",
|
"test:front:watch": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js --watchAll",
|
||||||
"test:front:watch:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
|
"test:front:watch:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll",
|
||||||
|
"test:ts:front": "run -T tsc -p admin/tsconfig.json",
|
||||||
|
"watch": "pack-up watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strapi/design-system": "1.12.0",
|
"@strapi/design-system": "1.12.0",
|
||||||
|
"@strapi/helper-plugin": "4.14.4",
|
||||||
"@strapi/icons": "1.12.0",
|
"@strapi/icons": "1.12.0",
|
||||||
"@strapi/provider-email-sendmail": "4.14.4",
|
"@strapi/provider-email-sendmail": "4.14.4",
|
||||||
"@strapi/utils": "4.14.4",
|
"@strapi/utils": "4.14.4",
|
||||||
@ -38,8 +65,12 @@
|
|||||||
"yup": "0.32.9"
|
"yup": "0.32.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@strapi/helper-plugin": "4.14.4",
|
"@strapi/pack-up": "workspace:*",
|
||||||
|
"@strapi/types": "workspace:*",
|
||||||
"@testing-library/react": "14.0.0",
|
"@testing-library/react": "14.0.0",
|
||||||
|
"@types/koa": "2.13.4",
|
||||||
|
"@types/lodash": "^4.14.191",
|
||||||
|
"koa": "2.13.4",
|
||||||
"msw": "1.3.0",
|
"msw": "1.3.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@ -47,6 +78,7 @@
|
|||||||
"styled-components": "5.3.3"
|
"styled-components": "5.3.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"koa": "2.13.4",
|
||||||
"react": "^17.0.0 || ^18.0.0",
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
"react-dom": "^17.0.0 || ^18.0.0",
|
"react-dom": "^17.0.0 || ^18.0.0",
|
||||||
"react-router-dom": "5.3.4",
|
"react-router-dom": "5.3.4",
|
||||||
|
27
packages/core/email/packup.config.ts
Normal file
27
packages/core/email/packup.config.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { defineConfig } from '@strapi/pack-up';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
bundles: [
|
||||||
|
{
|
||||||
|
source: './admin/src/index.ts',
|
||||||
|
import: './dist/admin/index.mjs',
|
||||||
|
require: './dist/admin/index.js',
|
||||||
|
types: './dist/admin/src/index.d.ts',
|
||||||
|
runtime: 'web',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: './server/src/index.ts',
|
||||||
|
import: './dist/server/index.mjs',
|
||||||
|
require: './dist/server/index.js',
|
||||||
|
types: './dist/server/src/index.d.ts',
|
||||||
|
runtime: 'node',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dist: './dist',
|
||||||
|
/**
|
||||||
|
* Because we're exporting a server & client package
|
||||||
|
* which have different runtimes we want to ignore
|
||||||
|
* what they look like in the package.json
|
||||||
|
*/
|
||||||
|
exports: {},
|
||||||
|
});
|
7
packages/core/email/server/.eslintrc.js
Normal file
7
packages/core/email/server/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['custom/back/typescript'],
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./server/tsconfig.eslint.json'],
|
||||||
|
},
|
||||||
|
};
|
43
packages/core/email/server/bootstrap.js
vendored
43
packages/core/email/server/bootstrap.js
vendored
@ -1,43 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const createProvider = (emailConfig) => {
|
|
||||||
const providerName = emailConfig.provider?.toLowerCase();
|
|
||||||
let provider;
|
|
||||||
|
|
||||||
let modulePath;
|
|
||||||
try {
|
|
||||||
modulePath = require.resolve(`@strapi/provider-email-${providerName}`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'MODULE_NOT_FOUND') {
|
|
||||||
modulePath = providerName;
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
provider = require(modulePath);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Could not load email provider "${providerName}".`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider.init(emailConfig.providerOptions, emailConfig.settings);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = async ({ strapi }) => {
|
|
||||||
const emailConfig = strapi.config.get('plugin.email');
|
|
||||||
strapi.plugin('email').provider = createProvider(emailConfig);
|
|
||||||
|
|
||||||
// Add permissions
|
|
||||||
const actions = [
|
|
||||||
{
|
|
||||||
section: 'settings',
|
|
||||||
category: 'email',
|
|
||||||
displayName: 'Access the Email Settings page',
|
|
||||||
uid: 'settings.read',
|
|
||||||
pluginName: 'email',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
await strapi.admin.services.permission.actionProvider.registerMany(actions);
|
|
||||||
};
|
|
@ -1,68 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const { pick } = require('lodash/fp');
|
|
||||||
const { ApplicationError } = require('@strapi/utils').errors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Email.js controller
|
|
||||||
*
|
|
||||||
* @description: A set of functions called "actions" of the `email` plugin.
|
|
||||||
*/
|
|
||||||
module.exports = {
|
|
||||||
async send(ctx) {
|
|
||||||
const options = ctx.request.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await strapi.plugin('email').service('email').send(options);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.statusCode === 400) {
|
|
||||||
throw new ApplicationError(e.message);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Couldn't send email: ${e.message}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send 200 `ok`
|
|
||||||
ctx.send({});
|
|
||||||
},
|
|
||||||
|
|
||||||
async test(ctx) {
|
|
||||||
const { to } = ctx.request.body;
|
|
||||||
|
|
||||||
if (!to) {
|
|
||||||
throw new ApplicationError('No recipient(s) are given');
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = {
|
|
||||||
to,
|
|
||||||
subject: `Strapi test mail to: ${to}`,
|
|
||||||
text: `Great! You have correctly configured the Strapi email plugin with the ${strapi.config.get(
|
|
||||||
'plugin.email.provider'
|
|
||||||
)} provider. \r\nFor documentation on how to use the email plugin checkout: https://docs.strapi.io/developer-docs/latest/plugins/email.html`,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await strapi.plugin('email').service('email').send(email);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.statusCode === 400) {
|
|
||||||
throw new ApplicationError(e.message);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Couldn't send test email: ${e.message}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send 200 `ok`
|
|
||||||
ctx.send({});
|
|
||||||
},
|
|
||||||
|
|
||||||
async getSettings(ctx) {
|
|
||||||
const config = strapi.plugin('email').service('email').getProviderSettings();
|
|
||||||
|
|
||||||
ctx.send({
|
|
||||||
config: pick(
|
|
||||||
['provider', 'settings.defaultFrom', 'settings.defaultReplyTo', 'settings.testAddress'],
|
|
||||||
config
|
|
||||||
),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,7 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const email = require('./email');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
email,
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
admin: require('./admin'),
|
|
||||||
'content-api': require('./content-api'),
|
|
||||||
};
|
|
@ -1,7 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const email = require('./email');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
email,
|
|
||||||
};
|
|
57
packages/core/email/server/src/bootstrap.ts
Normal file
57
packages/core/email/server/src/bootstrap.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import type { Strapi } from '@strapi/types';
|
||||||
|
import type { EmailConfig, SendOptions } from './types';
|
||||||
|
|
||||||
|
interface EmailProvider {
|
||||||
|
send: (options: SendOptions) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmailProviderModule {
|
||||||
|
init: (
|
||||||
|
options: EmailConfig['providerOptions'],
|
||||||
|
settings: EmailConfig['settings']
|
||||||
|
) => EmailProvider;
|
||||||
|
name?: string;
|
||||||
|
provider?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createProvider = (emailConfig: EmailConfig) => {
|
||||||
|
const providerName = emailConfig.provider.toLowerCase();
|
||||||
|
let provider: EmailProviderModule;
|
||||||
|
|
||||||
|
let modulePath: string;
|
||||||
|
try {
|
||||||
|
modulePath = require.resolve(`@strapi/provider-email-${providerName}`);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && 'code' in error && error.code === 'MODULE_NOT_FOUND') {
|
||||||
|
modulePath = providerName;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
provider = require(modulePath);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Could not load email provider "${providerName}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.init(emailConfig.providerOptions, emailConfig.settings);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bootstrap = async ({ strapi }: { strapi: Strapi }) => {
|
||||||
|
const emailConfig: EmailConfig = strapi.config.get('plugin.email');
|
||||||
|
strapi.plugin('email').provider = createProvider(emailConfig);
|
||||||
|
|
||||||
|
// Add permissions
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
section: 'settings',
|
||||||
|
category: 'email',
|
||||||
|
displayName: 'Access the Email Settings page',
|
||||||
|
uid: 'settings.read',
|
||||||
|
pluginName: 'email',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await strapi.admin!.services.permission.actionProvider.registerMany(actions);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
import type { StrapiConfig } from './types';
|
||||||
|
|
||||||
module.exports = {
|
export const config: StrapiConfig = {
|
||||||
default: {
|
default: {
|
||||||
provider: 'sendmail',
|
provider: 'sendmail',
|
||||||
providerOptions: {},
|
providerOptions: {},
|
77
packages/core/email/server/src/controllers/email.ts
Normal file
77
packages/core/email/server/src/controllers/email.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { pick } from 'lodash/fp';
|
||||||
|
import { errors } from '@strapi/utils';
|
||||||
|
|
||||||
|
import type Koa from 'koa';
|
||||||
|
import type { EmailConfig, SendOptions } from '../types';
|
||||||
|
|
||||||
|
const { ApplicationError } = errors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email.js controller
|
||||||
|
*
|
||||||
|
* @description: A set of functions called "actions" of the `email` plugin.
|
||||||
|
*/
|
||||||
|
const emailController = {
|
||||||
|
async send(ctx: Koa.Context) {
|
||||||
|
const options: SendOptions = ctx.request.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await strapi.plugin('email').service('email').send(options);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if ('statusCode' in error && error.statusCode === 400) {
|
||||||
|
throw new ApplicationError(error.message);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Couldn't send email: ${error.message}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send 200 `ok`
|
||||||
|
ctx.send({});
|
||||||
|
},
|
||||||
|
|
||||||
|
async test(ctx: Koa.Context) {
|
||||||
|
const { to } = ctx.request.body;
|
||||||
|
|
||||||
|
if (!to) {
|
||||||
|
throw new ApplicationError('No recipient(s) are given');
|
||||||
|
}
|
||||||
|
|
||||||
|
const email: SendOptions = {
|
||||||
|
to,
|
||||||
|
subject: `Strapi test mail to: ${to}`,
|
||||||
|
text: `Great! You have correctly configured the Strapi email plugin with the ${strapi.config.get(
|
||||||
|
'plugin.email.provider'
|
||||||
|
)} provider. \r\nFor documentation on how to use the email plugin checkout: https://docs.strapi.io/developer-docs/latest/plugins/email.html`,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await strapi.plugin('email').service('email').send(email);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if ('statusCode' in error && error.statusCode === 400) {
|
||||||
|
throw new ApplicationError(error.message);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Couldn't send test email: ${error.message}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send 200 `ok`
|
||||||
|
ctx.send({});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSettings(ctx: Koa.Context) {
|
||||||
|
const config: EmailConfig = strapi.plugin('email').service('email').getProviderSettings();
|
||||||
|
|
||||||
|
ctx.send({
|
||||||
|
config: pick(
|
||||||
|
['provider', 'settings.defaultFrom', 'settings.defaultReplyTo', 'settings.testAddress'],
|
||||||
|
config
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default emailController;
|
3
packages/core/email/server/src/controllers/index.ts
Normal file
3
packages/core/email/server/src/controllers/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import email from './email';
|
||||||
|
|
||||||
|
export const controllers = { email };
|
13
packages/core/email/server/src/index.ts
Normal file
13
packages/core/email/server/src/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { bootstrap } from './bootstrap';
|
||||||
|
import { services } from './services';
|
||||||
|
import { routes } from './routes';
|
||||||
|
import { controllers } from './controllers';
|
||||||
|
import { config } from './config';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
bootstrap,
|
||||||
|
services,
|
||||||
|
routes,
|
||||||
|
controllers,
|
||||||
|
config,
|
||||||
|
};
|
@ -1,6 +1,4 @@
|
|||||||
'use strict';
|
export default {
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
type: 'admin',
|
type: 'admin',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
@ -1,6 +1,4 @@
|
|||||||
'use strict';
|
export default {
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
type: 'content-api',
|
type: 'content-api',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
7
packages/core/email/server/src/routes/index.ts
Normal file
7
packages/core/email/server/src/routes/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import admin from './admin';
|
||||||
|
import contentApi from './content-api';
|
||||||
|
|
||||||
|
export const routes = {
|
||||||
|
admin,
|
||||||
|
'content-api': contentApi,
|
||||||
|
};
|
@ -1,18 +1,19 @@
|
|||||||
'use strict';
|
import * as _ from 'lodash';
|
||||||
|
import { keysDeep, template } from '@strapi/utils';
|
||||||
|
|
||||||
const _ = require('lodash');
|
import type {
|
||||||
const {
|
EmailConfig,
|
||||||
template: { createStrictInterpolationRegExp },
|
EmailOptions,
|
||||||
keysDeep,
|
EmailTemplate,
|
||||||
} = require('@strapi/utils');
|
EmailTemplateData,
|
||||||
|
SendOptions,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
const getProviderSettings = () => {
|
const { createStrictInterpolationRegExp } = template;
|
||||||
return strapi.config.get('plugin.email');
|
|
||||||
};
|
|
||||||
|
|
||||||
const send = async (options) => {
|
const getProviderSettings = (): EmailConfig => strapi.config.get('plugin.email');
|
||||||
return strapi.plugin('email').provider.send(options);
|
|
||||||
};
|
const send = async (options: SendOptions) => strapi.plugin('email').provider.send(options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fill subject, text and html using lodash template
|
* fill subject, text and html using lodash template
|
||||||
@ -21,9 +22,14 @@ const send = async (options) => {
|
|||||||
* @param {object} data - data used to fill the template
|
* @param {object} data - data used to fill the template
|
||||||
* @returns {{ subject, text, subject }}
|
* @returns {{ subject, text, subject }}
|
||||||
*/
|
*/
|
||||||
const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) => {
|
const sendTemplatedEmail = (
|
||||||
|
emailOptions: EmailOptions,
|
||||||
|
emailTemplate: EmailTemplate,
|
||||||
|
data: EmailTemplateData
|
||||||
|
) => {
|
||||||
const attributes = ['subject', 'text', 'html'];
|
const attributes = ['subject', 'text', 'html'];
|
||||||
const missingAttributes = _.difference(attributes, Object.keys(emailTemplate));
|
const missingAttributes = _.difference(attributes, Object.keys(emailTemplate));
|
||||||
|
|
||||||
if (missingAttributes.length > 0) {
|
if (missingAttributes.length > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Following attributes are missing from your email template : ${missingAttributes.join(', ')}`
|
`Following attributes are missing from your email template : ${missingAttributes.join(', ')}`
|
||||||
@ -39,8 +45,6 @@ const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) =>
|
|||||||
? Object.assign(compiled, {
|
? Object.assign(compiled, {
|
||||||
[attribute]: _.template(emailTemplate[attribute], {
|
[attribute]: _.template(emailTemplate[attribute], {
|
||||||
interpolate,
|
interpolate,
|
||||||
evaluate: false,
|
|
||||||
escape: false,
|
|
||||||
})(data),
|
})(data),
|
||||||
})
|
})
|
||||||
: compiled,
|
: compiled,
|
||||||
@ -50,8 +54,10 @@ const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) =>
|
|||||||
return strapi.plugin('email').provider.send({ ...emailOptions, ...templatedAttributes });
|
return strapi.plugin('email').provider.send({ ...emailOptions, ...templatedAttributes });
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = () => ({
|
const emailService = () => ({
|
||||||
getProviderSettings,
|
getProviderSettings,
|
||||||
send,
|
send,
|
||||||
sendTemplatedEmail,
|
sendTemplatedEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default emailService;
|
3
packages/core/email/server/src/services/index.ts
Normal file
3
packages/core/email/server/src/services/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import email from './email';
|
||||||
|
|
||||||
|
export const services = { email };
|
43
packages/core/email/server/src/types.ts
Normal file
43
packages/core/email/server/src/types.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import type { Plugin } from '@strapi/types';
|
||||||
|
|
||||||
|
export interface EmailConfig extends Record<string, unknown> {
|
||||||
|
provider: string;
|
||||||
|
providerOptions?: object;
|
||||||
|
settings?: {
|
||||||
|
defaultFrom?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoadedPluginConfig = Plugin.LoadedPlugin['config'];
|
||||||
|
|
||||||
|
export interface StrapiConfig extends LoadedPluginConfig {
|
||||||
|
default: EmailConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailTemplateData {
|
||||||
|
url?: string;
|
||||||
|
user?: {
|
||||||
|
email: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailOptions {
|
||||||
|
from?: string;
|
||||||
|
to: string;
|
||||||
|
cc?: string;
|
||||||
|
bcc?: string;
|
||||||
|
replyTo?: string;
|
||||||
|
[key: string]: string | undefined; // to allow additional template attributes if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailTemplate {
|
||||||
|
subject: string;
|
||||||
|
text: string;
|
||||||
|
html?: string;
|
||||||
|
[key: string]: string | undefined; // to allow additional template attributes if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SendOptions = EmailOptions & EmailTemplate;
|
9
packages/core/email/server/tsconfig.build.json
Normal file
9
packages/core/email/server/tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["./src"],
|
||||||
|
"exclude": ["./src/**/*.test.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist/server"
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
8
packages/core/email/server/tsconfig.json
Normal file
8
packages/core/email/server/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "tsconfig/base.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
}
|
11
packages/core/email/shared/types.ts
Normal file
11
packages/core/email/shared/types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface EmailSettings {
|
||||||
|
config: ConfigSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigSettings {
|
||||||
|
provider: string;
|
||||||
|
settings: {
|
||||||
|
defaultFrom: string;
|
||||||
|
defaultReplyTo: string;
|
||||||
|
};
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = require('./admin/src').default;
|
|
@ -1,17 +1,3 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const bootstrap = require('./server/bootstrap');
|
module.exports = require('./dist/server');
|
||||||
const services = require('./server/services');
|
|
||||||
const routes = require('./server/routes');
|
|
||||||
const controllers = require('./server/controllers');
|
|
||||||
const config = require('./server/config');
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
return {
|
|
||||||
bootstrap,
|
|
||||||
config,
|
|
||||||
routes,
|
|
||||||
controllers,
|
|
||||||
services,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
8
packages/core/email/tsconfig.build.json
Normal file
8
packages/core/email/tsconfig.build.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "tsconfig/client.json",
|
||||||
|
"include": ["./admin", "./shared", "./server"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"declarationDir": "./dist",
|
||||||
|
"outDir": "./dist"
|
||||||
|
}
|
||||||
|
}
|
@ -12,12 +12,15 @@ import type { domain } from '@strapi/permissions';
|
|||||||
|
|
||||||
type Permission = domain.permission.Permission;
|
type Permission = domain.permission.Permission;
|
||||||
|
|
||||||
export interface CheckPagePermissions {
|
export interface CheckPagePermissionsProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
permissions?: Permission[];
|
permissions?: Permission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckPagePermissions = ({ permissions = [], children }: CheckPagePermissions) => {
|
const CheckPagePermissions = ({
|
||||||
|
permissions = [],
|
||||||
|
children,
|
||||||
|
}: CheckPagePermissionsProps): React.JSX.Element => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const { signal } = abortController;
|
const { signal } = abortController;
|
||||||
const { allPermissions } = useRBACProvider();
|
const { allPermissions } = useRBACProvider();
|
||||||
@ -73,7 +76,7 @@ const CheckPagePermissions = ({ permissions = [], children }: CheckPagePermissio
|
|||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { CheckPagePermissions };
|
export { CheckPagePermissions };
|
||||||
|
@ -11,12 +11,12 @@ type Permission = domain.permission.Permission;
|
|||||||
// NOTE: this component is very similar to the CheckPagePermissions
|
// NOTE: this component is very similar to the CheckPagePermissions
|
||||||
// except that it does not handle redirections nor loading state
|
// except that it does not handle redirections nor loading state
|
||||||
|
|
||||||
export interface CheckPagePermissions {
|
export interface CheckPermissionsProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
permissions?: Permission[];
|
permissions?: Permission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckPermissions = ({ permissions = [], children }: CheckPagePermissions) => {
|
const CheckPermissions = ({ permissions = [], children }: CheckPermissionsProps) => {
|
||||||
const { allPermissions } = useRBACProvider();
|
const { allPermissions } = useRBACProvider();
|
||||||
const toggleNotification = useNotification();
|
const toggleNotification = useNotification();
|
||||||
const [state, setState] = React.useState({ isLoading: true, canAccess: false });
|
const [state, setState] = React.useState({ isLoading: true, canAccess: false });
|
||||||
@ -69,7 +69,7 @@ const CheckPermissions = ({ permissions = [], children }: CheckPagePermissions)
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { CheckPermissions };
|
export { CheckPermissions };
|
||||||
|
@ -2,30 +2,14 @@ import { join } from 'path';
|
|||||||
import fse from 'fs-extra';
|
import fse from 'fs-extra';
|
||||||
import { defaultsDeep, defaults, getOr, get } from 'lodash/fp';
|
import { defaultsDeep, defaults, getOr, get } from 'lodash/fp';
|
||||||
import { env } from '@strapi/utils';
|
import { env } from '@strapi/utils';
|
||||||
import type { Strapi, Common, Schema } from '@strapi/types';
|
import type { Strapi, Plugin } from '@strapi/types';
|
||||||
import { loadFile } from '../../app-configuration/load-config-file';
|
import { loadFile } from '../../app-configuration/load-config-file';
|
||||||
import loadFiles from '../../../load/load-files';
|
import loadFiles from '../../../load/load-files';
|
||||||
import { getEnabledPlugins } from './get-enabled-plugins';
|
import { getEnabledPlugins } from './get-enabled-plugins';
|
||||||
import { getUserPluginsConfig } from './get-user-plugins-config';
|
import { getUserPluginsConfig } from './get-user-plugins-config';
|
||||||
|
|
||||||
type LoadedPlugin = {
|
|
||||||
config: {
|
|
||||||
default: Record<string, unknown> | ((opts: { env: typeof env }) => Record<string, unknown>);
|
|
||||||
validator: (config: Record<string, unknown>) => void;
|
|
||||||
};
|
|
||||||
bootstrap: ({ strapi }: { strapi: Strapi }) => void | Promise<void>;
|
|
||||||
destroy: ({ strapi }: { strapi: Strapi }) => void | Promise<void>;
|
|
||||||
register: ({ strapi }: { strapi: Strapi }) => void | Promise<void>;
|
|
||||||
routes: Record<string, Common.Router>;
|
|
||||||
controllers: Record<string, Common.Controller>;
|
|
||||||
services: Record<string, Common.Service>;
|
|
||||||
policies: Record<string, Common.Policy>;
|
|
||||||
middlewares: Record<string, Common.Middleware>;
|
|
||||||
contentTypes: Record<string, { schema: Schema.ContentType }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Plugins {
|
interface Plugins {
|
||||||
[key: string]: LoadedPlugin;
|
[key: string]: Plugin.LoadedPlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPlugin = {
|
const defaultPlugin = {
|
||||||
|
@ -3,5 +3,5 @@
|
|||||||
export interface AdminInput {
|
export interface AdminInput {
|
||||||
register: unknown;
|
register: unknown;
|
||||||
bootstrap: unknown;
|
bootstrap: unknown;
|
||||||
registerTrads: unknown;
|
registerTrads: ({ locales }: { locales: string[] }) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import { env } from '@strapi/utils';
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
validator: () => unknown;
|
validator: (config: Record<string, unknown>) => void;
|
||||||
default: object | (() => object);
|
default: Record<string, unknown> | ((opts: { env: typeof env }) => Record<string, unknown>);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import type { Common, Shared, Utils } from '../..';
|
import { env } from '@strapi/utils';
|
||||||
|
|
||||||
|
import type { Common, Shared, Utils, Schema } from '../..';
|
||||||
|
import type { Strapi } from '../../..';
|
||||||
|
|
||||||
export type IsEnabled<
|
export type IsEnabled<
|
||||||
TName extends keyof any,
|
TName extends keyof any,
|
||||||
@ -15,4 +18,20 @@ export type IsEnabled<
|
|||||||
: false
|
: false
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
export type LoadedPlugin = {
|
||||||
|
config: {
|
||||||
|
default: Record<string, unknown> | ((opts: { env: typeof env }) => Record<string, unknown>);
|
||||||
|
validator: (config: Record<string, unknown>) => void;
|
||||||
|
};
|
||||||
|
bootstrap: ({ strapi }: { strapi: Strapi }) => void | Promise<void>;
|
||||||
|
destroy: ({ strapi }: { strapi: Strapi }) => void | Promise<void>;
|
||||||
|
register: ({ strapi }: { strapi: Strapi }) => void | Promise<void>;
|
||||||
|
routes: Record<string, Common.Router>;
|
||||||
|
controllers: Record<string, Common.Controller>;
|
||||||
|
services: Record<string, Common.Service>;
|
||||||
|
policies: Record<string, Common.Policy>;
|
||||||
|
middlewares: Record<string, Common.Middleware>;
|
||||||
|
contentTypes: Record<string, { schema: Schema.ContentType }>;
|
||||||
|
};
|
||||||
|
|
||||||
export * as Config from './config';
|
export * as Config from './config';
|
||||||
|
@ -7934,9 +7934,14 @@ __metadata:
|
|||||||
"@strapi/design-system": 1.12.0
|
"@strapi/design-system": 1.12.0
|
||||||
"@strapi/helper-plugin": 4.14.4
|
"@strapi/helper-plugin": 4.14.4
|
||||||
"@strapi/icons": 1.12.0
|
"@strapi/icons": 1.12.0
|
||||||
|
"@strapi/pack-up": "workspace:*"
|
||||||
"@strapi/provider-email-sendmail": 4.14.4
|
"@strapi/provider-email-sendmail": 4.14.4
|
||||||
|
"@strapi/types": "workspace:*"
|
||||||
"@strapi/utils": 4.14.4
|
"@strapi/utils": 4.14.4
|
||||||
"@testing-library/react": 14.0.0
|
"@testing-library/react": 14.0.0
|
||||||
|
"@types/koa": 2.13.4
|
||||||
|
"@types/lodash": ^4.14.191
|
||||||
|
koa: 2.13.4
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
msw: 1.3.0
|
msw: 1.3.0
|
||||||
prop-types: ^15.8.1
|
prop-types: ^15.8.1
|
||||||
@ -7948,6 +7953,7 @@ __metadata:
|
|||||||
styled-components: 5.3.3
|
styled-components: 5.3.3
|
||||||
yup: 0.32.9
|
yup: 0.32.9
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
koa: 2.13.4
|
||||||
react: ^17.0.0 || ^18.0.0
|
react: ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^17.0.0 || ^18.0.0
|
react-dom: ^17.0.0 || ^18.0.0
|
||||||
react-router-dom: 5.3.4
|
react-router-dom: 5.3.4
|
||||||
|
Loading…
x
Reference in New Issue
Block a user