chore: improve validation around email campaigns

This commit is contained in:
Chris Anderson 2025-08-05 08:30:23 -05:00
parent d62da2fca2
commit 5404984850
6 changed files with 48 additions and 34 deletions

View file

@ -37,11 +37,23 @@ export const isValid = (schema: any, data: any): IsValidSchema => {
}
export const parseError = (errors: ErrorObject[] | null | undefined = []) => {
const readablePath = (path: string) => {
const parts = path.split('/')
if (parts[0] === '') parts.shift()
if (parts[0] === 'data') parts.shift()
return parts.join(' ').trim()
}
if (errors === null || errors.length <= 0) return 'There was an unknown error validating your request.'
const error = errors[0]
if (error.keyword === 'type') {
const path = error.instancePath.replace('/', ' ').trim()
const path = readablePath(error.instancePath)
return `The value of \`${path}\` must be a ${error.params.type}.`
}
if (error.keyword === 'format') {
const path = readablePath(error.instancePath)
return `The value of \`${path}\` ${error.message}.`
}
return capitalizeFirstLetter(error.message ?? '')
}

View file

@ -113,7 +113,7 @@ export class EmailTemplate extends Template {
required: ['address'],
properties: {
name: { type: 'string', nullable: true },
address: { type: 'string' },
address: { type: 'string', format: 'email' },
},
},
subject: { type: 'string' },

View file

@ -35,20 +35,24 @@ const templateDataEmailParams = {
address: {
type: 'string',
nullable: true,
format: 'email',
},
},
},
cc: {
type: 'string',
nullable: true,
format: 'email',
},
bcc: {
type: 'string',
nullable: true,
format: 'email',
},
reply_to: {
type: 'string',
nullable: true,
format: 'email',
},
subject: {
type: 'string',
@ -126,12 +130,8 @@ const templateCreateParams: JSONSchemaType<TemplateParams> = {
type: 'string',
enum: ['email'],
},
campaign_id: {
type: 'integer',
},
locale: {
type: 'string',
},
campaign_id: { type: 'integer' },
locale: { type: 'string' },
data: templateDataEmailParams as any,
},
additionalProperties: false,
@ -144,12 +144,8 @@ const templateCreateParams: JSONSchemaType<TemplateParams> = {
type: 'string',
enum: ['text'],
},
campaign_id: {
type: 'integer',
},
locale: {
type: 'string',
},
campaign_id: { type: 'integer' },
locale: { type: 'string' },
data: templateDataTextParams as any,
},
additionalProperties: false,
@ -162,12 +158,8 @@ const templateCreateParams: JSONSchemaType<TemplateParams> = {
type: 'string',
enum: ['push'],
},
campaign_id: {
type: 'integer',
},
locale: {
type: 'string',
},
campaign_id: { type: 'integer' },
locale: { type: 'string' },
data: templateDataPushParams as any,
},
additionalProperties: false,
@ -180,12 +172,8 @@ const templateCreateParams: JSONSchemaType<TemplateParams> = {
type: 'string',
enum: ['webhook'],
},
campaign_id: {
type: 'integer',
},
locale: {
type: 'string',
},
campaign_id: { type: 'integer' },
locale: { type: 'string' },
data: templateDataWebhookParams as any,
},
additionalProperties: false,

View file

@ -173,6 +173,7 @@
"import_users": "Import Users",
"in_timezone": "In Timezone",
"integrations": "Integrations",
"invalid_email": "Invalid Email",
"invite_to_project": "Invite to Project",
"joined_list_at": "Joined List At",
"journey": "Journey",

View file

@ -7,7 +7,7 @@ import './TextInput.css'
type TextInputValue = string | number | readonly string[] | undefined
export interface BaseTextInputProps<T extends TextInputValue> extends Partial<ControlledInputProps<T>> {
type?: 'text' | 'time' | 'date' | 'datetime-local' | 'number' | 'password'
type?: 'text' | 'time' | 'date' | 'datetime-local' | 'number' | 'password' | 'email'
textarea?: boolean
size?: 'tiny' | 'small' | 'regular'
value?: T

View file

@ -18,14 +18,22 @@ import { useTranslation } from 'react-i18next'
const EmailTable = ({ data }: { data: EmailTemplateData }) => {
const { t } = useTranslation()
const validate = (field: string, value: string | undefined, required = true) => {
if (!value && required) return <Tag variant="warn">{t('missing')}</Tag>
if (['cc', 'bcc', 'reply_to', 'from_email'].includes(field) && value && !value.includes('@')) {
return <Tag variant="warn">{t('invalid_email')}: &quot;{value}&quot;</Tag>
}
return value
}
return <>
<InfoTable rows={{
[t('from_email')]: data.from?.address ?? <Tag variant="warn">{t('missing')}</Tag>,
[t('from_name')]: data.from?.name ?? <Tag variant="warn">{t('missing')}</Tag>,
[t('reply_to')]: data.reply_to,
[t('cc')]: data.cc,
[t('bcc')]: data.bcc,
[t('subject')]: data.subject ?? <Tag variant="warn">{t('missing')}</Tag>,
[t('from_email')]: validate('from_email', data.from?.address),
[t('from_name')]: validate('from_name', data.from?.name),
[t('reply_to')]: validate('reply_to', data.reply_to, false),
[t('cc')]: validate('cc', data.cc, false),
[t('bcc')]: validate('bcc', data.bcc, false),
[t('subject')]: validate('subject', data.subject),
[t('preheader')]: data.preheader,
}} />
</>
@ -35,7 +43,12 @@ const EmailForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> })
const { t } = useTranslation()
return <>
<TextInput.Field form={form} name="data.from.name" label={t('from_name')} required />
<TextInput.Field form={form} name="data.from.address" label={t('from_email')} required />
<TextInput.Field
form={form}
name="data.from.address"
label={t('from_email')}
type="email"
required />
<TextInput.Field
form={form}
name="data.subject"