mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-08-28 11:46:02 +08:00
Improved Campaign State Handling (#651)
This commit is contained in:
parent
610c820943
commit
c528c06d9d
15 changed files with 131 additions and 37 deletions
|
@ -8,7 +8,7 @@ import { crossTimezoneCopy } from '../utilities'
|
|||
import Project from '../projects/Project'
|
||||
import { User } from '../users/User'
|
||||
|
||||
export type CampaignState = 'draft' | 'scheduled' | 'loading' | 'running' | 'finished' | 'aborted'
|
||||
export type CampaignState = 'draft' | 'scheduled' | 'loading' | 'running' | 'finished' | 'aborting' | 'aborted'
|
||||
export interface CampaignDelivery {
|
||||
sent: number
|
||||
total: number
|
||||
|
|
32
apps/platform/src/campaigns/CampaignAbortJob.ts
Normal file
32
apps/platform/src/campaigns/CampaignAbortJob.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { Job } from '../queue'
|
||||
import Campaign, { CampaignJobParams } from './Campaign'
|
||||
import CampaignGenerateListJob from './CampaignGenerateListJob'
|
||||
import { abortCampaign, getCampaign } from './CampaignService'
|
||||
|
||||
export interface CampaignAbortParams extends CampaignJobParams {
|
||||
reschedule?: boolean
|
||||
}
|
||||
|
||||
export default class CampaignAbortJob extends Job {
|
||||
static $name = 'campaign_abort_job'
|
||||
|
||||
static from({ id, project_id, reschedule }: CampaignAbortParams): CampaignAbortJob {
|
||||
return new this({ id, project_id, reschedule }).jobId(`cid_${id}_abort`)
|
||||
}
|
||||
|
||||
static async handler({ id, project_id, reschedule }: CampaignAbortParams) {
|
||||
const campaign = await getCampaign(id, project_id)
|
||||
if (!campaign) return
|
||||
await abortCampaign(campaign)
|
||||
|
||||
const state = reschedule ? 'loading' : 'aborted'
|
||||
|
||||
await Campaign.update(qb => qb.where('id', id), {
|
||||
state,
|
||||
})
|
||||
|
||||
if (state === 'loading' && campaign.type === 'blast') {
|
||||
await CampaignGenerateListJob.from(campaign).queue()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,8 +10,8 @@ import { CacheKeys, estimatedSendSize, generateSendList, getCampaign } from './C
|
|||
export default class CampaignGenerateListJob extends Job {
|
||||
static $name = 'campaign_generate_list_job'
|
||||
|
||||
static from(data: CampaignJobParams): CampaignGenerateListJob {
|
||||
return new this(data)
|
||||
static from({ id, project_id }: CampaignJobParams): CampaignGenerateListJob {
|
||||
return new this({ id, project_id }).jobId(`cid_${id}_generate`)
|
||||
}
|
||||
|
||||
static async handler({ id, project_id }: CampaignJobParams) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import Model, { raw, ref } from '../core/Model'
|
|||
import { cacheGet, cacheIncr } from '../config/redis'
|
||||
import App from '../app'
|
||||
import { releaseLock } from '../core/Lock'
|
||||
import CampaignAbortJob from './CampaignAbortJob'
|
||||
|
||||
export const CacheKeys = {
|
||||
pendingStats: 'campaigns:pending_stats',
|
||||
|
@ -119,18 +120,19 @@ export const updateCampaign = async (id: number, projectId: number, { tags, ...p
|
|||
const data: Partial<Campaign> = { ...params }
|
||||
let send_at: Date | undefined | null = data.send_at ? new Date(data.send_at) : undefined
|
||||
|
||||
const isRescheduling = send_at != null
|
||||
&& campaign.send_at != null
|
||||
&& send_at !== campaign.send_at
|
||||
|
||||
// If we are aborting, reset `send_at`
|
||||
if (data.state === 'aborted') {
|
||||
send_at = null
|
||||
await abortCampaign(campaign)
|
||||
data.state = 'aborting'
|
||||
}
|
||||
|
||||
// If we are rescheduling, abort sends so they are reset
|
||||
if (send_at
|
||||
&& campaign.send_at
|
||||
&& send_at !== campaign.send_at) {
|
||||
data.state = 'loading'
|
||||
await abortCampaign(campaign)
|
||||
if (isRescheduling) {
|
||||
data.state = 'aborting'
|
||||
}
|
||||
|
||||
// Check templates to make sure we can schedule a send
|
||||
|
@ -164,6 +166,10 @@ export const updateCampaign = async (id: number, projectId: number, { tags, ...p
|
|||
await CampaignGenerateListJob.from(campaign).queue()
|
||||
}
|
||||
|
||||
if (data.state === 'aborting') {
|
||||
await CampaignAbortJob.from({ ...campaign, reschedule: isRescheduling }).queue()
|
||||
}
|
||||
|
||||
return await getCampaign(id, projectId)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,10 @@ import ScheduledEntranceOrchestratorJob from '../journey/ScheduledEntranceOrches
|
|||
import ListRefreshJob from '../lists/ListRefreshJob'
|
||||
import ListEvaluateUserJob from '../lists/ListEvaluateUserJob'
|
||||
import UserListMatchJob from '../lists/UserListMatchJob'
|
||||
import CampaignAbortJob from '../campaigns/CampaignAbortJob'
|
||||
|
||||
export const jobs = [
|
||||
CampaignAbortJob,
|
||||
CampaignGenerateListJob,
|
||||
CampaignEnqueueSendJob,
|
||||
CampaignInteractJob,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"abort_campaign": "Abort Campaign",
|
||||
"aborted": "Aborted",
|
||||
"aborting": "Aborting",
|
||||
"action": "Action",
|
||||
"add_admin": "Add Admin",
|
||||
"add_admin_description": "Add a new admin to this organization. Admins have full access to all projects and settings, members can only access projects they are a part of.",
|
||||
|
@ -170,7 +171,10 @@
|
|||
"label": "Label",
|
||||
"language": "Language",
|
||||
"last_name": "Last Name",
|
||||
"launch": "Launch",
|
||||
"launch_campaign": "Launch Campaign",
|
||||
"launch_period": "Launch Period",
|
||||
"launch_subtitle": "Please check to ensure all settings are correct before launching a campaign. A scheduled campaign can be aborted, but one sent immediately cannot.",
|
||||
"launched_at": "Launched At",
|
||||
"light_mode": "Use Light Theme",
|
||||
"link": "Link",
|
||||
|
@ -211,6 +215,7 @@
|
|||
"new_team_member": "New Team Member",
|
||||
"no_providers": "No Providers",
|
||||
"no_template_alert_body": "There are no templates yet for this campaign. Add a locale above or use the button below to get started.",
|
||||
"now": "Now",
|
||||
"onboarding_installation_success": "Looks like everything is working with the installation! Now let's get you setup and ready to run some campaigns!",
|
||||
"onboarding_project_setup_description": "At Parcelvoy, projects represent a single workspace for sending messages. You can use them for creating staging environments, isolating different clients, etc. Let's create your first one to get you started!",
|
||||
"onboarding_project_setup_title": "Project Setup",
|
||||
|
@ -245,6 +250,8 @@
|
|||
"remove": "Remove",
|
||||
"remove_locale_warning": "Are you sure you want to delete this locale? The template cannot be recovered.",
|
||||
"reply_to": "Reply To",
|
||||
"reschedule": "Reschedule",
|
||||
"rescheduling": "Rescheduling",
|
||||
"restart": "Restart",
|
||||
"restart_campaign": "Restart Campaign",
|
||||
"role": "Role",
|
||||
|
@ -269,8 +276,13 @@
|
|||
"second": "Second",
|
||||
"send": "Send",
|
||||
"send_at": "Send At",
|
||||
"send_at_date": "Send At Date",
|
||||
"send_at_time": "Send At Time",
|
||||
"send_at_timezone_notice": "The selected date and time will be in the project's timezone not your own.",
|
||||
"send_campaign_desc": "Send this campaign when users reach this step.",
|
||||
"send_desc": "Trigger a send (email, sms, push notification, webhook) to a user.",
|
||||
"send_in_user_timezone": "Send In Users Timezone",
|
||||
"send_in_user_timezone_desc": "Should the campaign go out at the selected time in the users timezone or in the projects timezone?",
|
||||
"send_lists": "Send Lists",
|
||||
"send_proof": "Send Proof",
|
||||
"sent": "Sent",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"abort_campaign": "Cancelar campaña",
|
||||
"aborted": "Cancellado",
|
||||
"aborting": "Abortando",
|
||||
"action": "Acción",
|
||||
"add_admin": "Añadir Admin",
|
||||
"add_admin_description": "Añade un nuevo administrador a esta organización. Los administradores tienen acceso completo a todos los proyectos y ajustes, los miembros sólo pueden acceder a los proyectos de los que forman parte.",
|
||||
|
@ -170,7 +171,10 @@
|
|||
"label": "Etiqueta",
|
||||
"language": "Idioma",
|
||||
"last_name": "Apellido",
|
||||
"launch": "Enviar",
|
||||
"launch_campaign": "Enviar Campaña",
|
||||
"launch_period": "Cuando Enviar",
|
||||
"launch_subtitle": "Verifique que todas las configuraciones sean correctas antes de iniciar una campaña. Una campaña programada se puede cancelar, pero una que se envía de inmediato no.",
|
||||
"launched_at": "Lanzado en",
|
||||
"light_mode": "Usar tema de luz",
|
||||
"link": "Enlace",
|
||||
|
@ -210,7 +214,8 @@
|
|||
"name": "Nombre",
|
||||
"new_team_member": "Nuevo miembro del equipo",
|
||||
"no_providers": "Sin proveedores",
|
||||
"no_template_alert_body": "",
|
||||
"no_template_alert_body": "Todavía no hay plantillas para esta campaña. Agrega una configuración regional arriba o usa el botón a continuación para comenzar.",
|
||||
"now": "Ahora",
|
||||
"onboarding_installation_success": "¡Parece que todo está funcionando con la instalación! ¡Ahora vamos a configurarlo y prepararlo para ejecutar algunas campañas!",
|
||||
"onboarding_project_setup_description": "En Parcelvoy, los proyectos representan un espacio de trabajo único para el envío de mensajes. Puede utilizarlos para crear entornos de ensayo, aislar diferentes clientes, etc. ¡Vamos a crear tu primero para empezar!",
|
||||
"onboarding_project_setup_title": "Configuración del proyecto",
|
||||
|
@ -245,6 +250,8 @@
|
|||
"remove": "Eliminar",
|
||||
"remove_locale_warning": "¿Está seguro de que desea eliminar esta configuración regional? La plantilla no se puede recuperar.",
|
||||
"reply_to": "Responder a",
|
||||
"reschedule": "Reprogramar",
|
||||
"rescheduling": "Reprogramando",
|
||||
"restart": "Reinicie",
|
||||
"restart_campaign": "Reiniciar campaña",
|
||||
"role": "Role",
|
||||
|
@ -269,8 +276,13 @@
|
|||
"second": "Segundo",
|
||||
"send": "Enviar",
|
||||
"send_at": "Enviado En",
|
||||
"send_at_date": "Fecha de envio",
|
||||
"send_at_time": "Hora de envio",
|
||||
"send_at_timezone_notice": "La fecha y hora seleccionadas estarán en la zona horaria del proyecto, no en la suya.",
|
||||
"send_campaign_desc": "Envíe esta campaña cuando los usuarios lleguen a este paso.",
|
||||
"send_desc": "Activar un envío (email, sms, notificación push, webhook) a un usuario.",
|
||||
"send_in_user_timezone": "Enviar la Zona Horaria ",
|
||||
"send_in_user_timezone_desc": "¿La campaña debe salir a la hora seleccionada en la zona horaria de los usuarios o en la zona horaria de los proyectos?",
|
||||
"send_lists": "Listas para enviar",
|
||||
"send_proof": "Enviar Prueba",
|
||||
"sent": "Enviado",
|
||||
|
|
|
@ -14,6 +14,7 @@ i18n
|
|||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss
|
||||
},
|
||||
fallbackLng: 'en',
|
||||
}).catch(() => {})
|
||||
|
||||
export default i18n
|
||||
|
|
|
@ -349,7 +349,7 @@ export interface JourneyEntranceDetail {
|
|||
userSteps: JourneyUserStep[]
|
||||
}
|
||||
|
||||
export type CampaignState = 'draft' | 'loading' | 'scheduled' | 'running' | 'finished' | 'aborted'
|
||||
export type CampaignState = 'draft' | 'loading' | 'scheduled' | 'running' | 'finished' | 'aborting' | 'aborted'
|
||||
|
||||
export interface CampaignDelivery {
|
||||
sent: number
|
||||
|
@ -389,7 +389,7 @@ export interface Campaign {
|
|||
updated_at: string
|
||||
}
|
||||
|
||||
export type CampaignSendState = 'pending' | 'throttled' | 'bounced' | 'sent' | 'failed'
|
||||
export type CampaignSendState = 'pending' | 'sent' | 'throttled' | 'failed' | 'bounced' | 'aborted'
|
||||
|
||||
export type CampaignUpdateParams = Partial<Pick<Campaign, 'name' | 'state' | 'list_ids' | 'exclusion_list_ids' | 'subscription_id' | 'tags'>>
|
||||
export type CampaignCreateParams = Pick<Campaign, 'name' | 'type' | 'list_ids' | 'exclusion_list_ids' | 'channel' | 'subscription_id' | 'provider_id' | 'tags'>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useContext, useEffect } from 'react'
|
||||
import api from '../../api'
|
||||
import { CampaignContext, ProjectContext } from '../../contexts'
|
||||
import { CampaignDelivery as Delivery, CampaignSendState } from '../../types'
|
||||
import { CampaignDelivery as Delivery, CampaignSendState, CampaignState } from '../../types'
|
||||
import Alert from '../../ui/Alert'
|
||||
import Heading from '../../ui/Heading'
|
||||
import { PreferencesContext } from '../../ui/PreferencesContext'
|
||||
|
@ -19,13 +19,14 @@ export const CampaignSendTag = ({ state }: { state: CampaignSendState }) => {
|
|||
bounced: 'error',
|
||||
sent: 'success',
|
||||
failed: 'error',
|
||||
aborted: 'warn',
|
||||
}
|
||||
return <Tag variant={variant[state]}>
|
||||
<Translation>{ (t) => t(state) }</Translation>
|
||||
</Tag>
|
||||
}
|
||||
|
||||
export const CampaignStats = ({ delivery }: { delivery: Delivery }) => {
|
||||
export const CampaignStats = ({ state, delivery }: { state: CampaignState, delivery: Delivery }) => {
|
||||
const { t } = useTranslation()
|
||||
const percent = new Intl.NumberFormat(undefined, { style: 'percent', minimumFractionDigits: 2 })
|
||||
|
||||
|
@ -35,7 +36,7 @@ export const CampaignStats = ({ delivery }: { delivery: Delivery }) => {
|
|||
const openRate = percent.format(delivery.total ? delivery.opens / delivery.total : 0)
|
||||
const clickRate = percent.format(delivery.total ? delivery.clicks / delivery.total : 0)
|
||||
|
||||
const SentSpan: React.ReactNode = <span>{sent}/<small>{total}</small></span>
|
||||
const SentSpan: React.ReactNode = <span>{sent}/<small>{state === 'loading' ? `~${total}` : total}</small></span>
|
||||
|
||||
return (
|
||||
<TileGrid numColumns={4}>
|
||||
|
@ -60,11 +61,11 @@ export default function CampaignDelivery() {
|
|||
const refresh = () => {
|
||||
api.campaigns.get(project.id, campaign.id)
|
||||
.then(setCampaign)
|
||||
.then(() => searchState.reload)
|
||||
.then(searchState.reload)
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (state !== 'loading') return
|
||||
if (!['loading', 'aborting'].includes(state)) return
|
||||
const complete = progress?.complete ?? 0
|
||||
const total = progress?.total ?? 0
|
||||
const percent = total > 0 ? complete / total * 100 : 0
|
||||
|
@ -83,7 +84,7 @@ export default function CampaignDelivery() {
|
|||
{state === 'scheduled'
|
||||
&& <Alert title={t('scheduled')}>{t('campaign_alert_scheduled')} <strong>{formatDate(preferences, send_at)}</strong></Alert>
|
||||
}
|
||||
{delivery && <CampaignStats delivery={delivery} />}
|
||||
{delivery && <CampaignStats delivery={delivery} state={state} />}
|
||||
<Heading title={t('users')} size="h4" />
|
||||
<SearchTable
|
||||
{...searchState}
|
||||
|
|
|
@ -57,7 +57,7 @@ export default function CampaignDetail() {
|
|||
const [project] = useContext(ProjectContext)
|
||||
const { t } = useTranslation()
|
||||
const [campaign, setCampaign] = useContext(CampaignContext)
|
||||
const { name, templates, state, progress } = campaign
|
||||
const { name, templates, state, send_at, progress } = campaign
|
||||
const [locale, setLocale] = useState<LocaleSelection>(localeState(templates ?? []))
|
||||
useEffect(() => {
|
||||
setLocale(localeState(templates ?? []))
|
||||
|
@ -68,6 +68,7 @@ export default function CampaignDetail() {
|
|||
const handleAbort = async () => {
|
||||
setIsLoading(true)
|
||||
const value = await api.campaigns.update(project.id, campaign.id, { state: 'aborted' })
|
||||
console.log('finished', value)
|
||||
setCampaign(value)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
@ -108,6 +109,19 @@ export default function CampaignDetail() {
|
|||
onClick={() => setIsLaunchOpen(true)}
|
||||
>{t('restart_campaign')}</Button>
|
||||
),
|
||||
aborting: send_at
|
||||
? (
|
||||
<Button
|
||||
icon={<SendIcon />}
|
||||
isLoading={true}
|
||||
>{t('rescheduling')}</Button>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
icon={<ForbiddenIcon />}
|
||||
isLoading={true}
|
||||
>{t('abort_campaign')}</Button>
|
||||
),
|
||||
loading: <></>,
|
||||
scheduled: (
|
||||
<>
|
||||
|
@ -135,7 +149,11 @@ export default function CampaignDetail() {
|
|||
return (
|
||||
<PageContent
|
||||
title={name}
|
||||
desc={state !== 'draft' && <CampaignTag state={state} progress={progress} />}
|
||||
desc={state !== 'draft' && <CampaignTag
|
||||
state={state}
|
||||
progress={progress}
|
||||
send_at={send_at}
|
||||
/>}
|
||||
actions={campaign.type !== 'trigger' && action[state]}
|
||||
fullscreen={true}>
|
||||
<NavigationTabs tabs={tabs} />
|
||||
|
|
|
@ -78,7 +78,7 @@ export default function CampaignOverview() {
|
|||
{campaign.type === 'blast' && <>
|
||||
<Heading title={t('delivery')} size="h4" />
|
||||
<InfoTable rows={{
|
||||
[t('state')]: CampaignTag({ state: campaign.state }),
|
||||
[t('state')]: CampaignTag({ state: campaign.state, send_at: campaign.send_at }),
|
||||
[t('launched_at')]: campaign.send_at ? formatDate(preferences, campaign.send_at, undefined, project.timezone) : undefined,
|
||||
[t('in_timezone')]: campaign.send_in_user_timezone ? 'Yes' : 'No',
|
||||
[t('send_lists')]: DelimitedLists({ lists: campaign.lists }),
|
||||
|
|
|
@ -18,10 +18,11 @@ import { ProjectContext } from '../../contexts'
|
|||
import { PreferencesContext } from '../../ui/PreferencesContext'
|
||||
import { Translation, useTranslation } from 'react-i18next'
|
||||
|
||||
export const CampaignTag = ({ state, progress }: Pick<Campaign, 'state' | 'progress'>) => {
|
||||
export const CampaignTag = ({ state, progress, send_at }: Pick<Campaign, 'state' | 'progress' | 'send_at'>) => {
|
||||
const variant: Record<CampaignState, TagVariant> = {
|
||||
draft: 'plain',
|
||||
aborted: 'error',
|
||||
aborting: 'error',
|
||||
loading: 'info',
|
||||
scheduled: 'info',
|
||||
running: 'info',
|
||||
|
@ -32,8 +33,11 @@ export const CampaignTag = ({ state, progress }: Pick<Campaign, 'state' | 'progr
|
|||
const total = progress?.total ?? 0
|
||||
const percent = total > 0 ? complete / total : 0
|
||||
const percentStr = percent.toLocaleString(undefined, { style: 'percent', minimumFractionDigits: 0 })
|
||||
|
||||
const label = state === 'aborting' && send_at ? 'rescheduling' : state
|
||||
|
||||
return <Tag variant={variant[state]}>
|
||||
<Translation>{ (t) => t(state) }</Translation>
|
||||
<Translation>{ (t) => t(label) }</Translation>
|
||||
{progress && ` (${percentStr})`}
|
||||
</Tag>
|
||||
}
|
||||
|
@ -136,7 +140,7 @@ export default function Campaigns() {
|
|||
key: 'state',
|
||||
title: t('state'),
|
||||
sortable: true,
|
||||
cell: ({ item: { state } }) => CampaignTag({ state }),
|
||||
cell: ({ item: { state, send_at } }) => CampaignTag({ state, send_at }),
|
||||
},
|
||||
{
|
||||
key: 'delivery',
|
||||
|
|
|
@ -13,14 +13,17 @@ import { zonedTimeToUtc } from 'date-fns-tz'
|
|||
import { Column, Columns } from '../../ui/Columns'
|
||||
import { useController } from 'react-hook-form'
|
||||
import { SelectionProps } from '../../ui/form/Field'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
interface DateTimeFieldProps extends SelectionProps<CampaignLaunchParams> {
|
||||
label: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
function DateTimeField({ label, name, control, required }: DateTimeFieldProps) {
|
||||
function DateTimeField({ name, control, required }: DateTimeFieldProps) {
|
||||
const [project] = useContext(ProjectContext)
|
||||
const { t } = useTranslation()
|
||||
const [date, setDate] = useState('')
|
||||
const [time, setTime] = useState('')
|
||||
|
||||
|
@ -55,7 +58,7 @@ function DateTimeField({ label, name, control, required }: DateTimeFieldProps) {
|
|||
<TextInput<string>
|
||||
type="date"
|
||||
name="date"
|
||||
label={`${label} Date`}
|
||||
label={t('send_at_date')}
|
||||
onChange={handleSetDate}
|
||||
onBlur={handleOnChange}
|
||||
value={date}
|
||||
|
@ -65,7 +68,7 @@ function DateTimeField({ label, name, control, required }: DateTimeFieldProps) {
|
|||
<TextInput<string>
|
||||
type="time"
|
||||
name="time"
|
||||
label={`${label} Time`}
|
||||
label={t('send_at_time')}
|
||||
onChange={handleSetTime}
|
||||
onBlur={handleOnChange}
|
||||
value={time}
|
||||
|
@ -73,7 +76,7 @@ function DateTimeField({ label, name, control, required }: DateTimeFieldProps) {
|
|||
</Column>
|
||||
</Columns>
|
||||
<span className="label-subtitle">
|
||||
{"The selected date and time will be in the project's timezone not your own."}
|
||||
{t('send_at_timezone_notice')}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
@ -85,6 +88,8 @@ interface LaunchCampaignParams {
|
|||
|
||||
export default function LaunchCampaign({ open, onClose }: LaunchCampaignParams) {
|
||||
const [project] = useContext(ProjectContext)
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [campaign, setCampaign] = useContext(CampaignContext)
|
||||
const [launchType, setLaunchType] = useState('now')
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
|
@ -103,6 +108,7 @@ export default function LaunchCampaign({ open, onClose }: LaunchCampaignParams)
|
|||
const value = await api.campaigns.update(project.id, campaign.id, params)
|
||||
setCampaign(value)
|
||||
onClose(false)
|
||||
navigate('delivery')
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data) {
|
||||
setError(error?.response?.data?.error)
|
||||
|
@ -110,29 +116,29 @@ export default function LaunchCampaign({ open, onClose }: LaunchCampaignParams)
|
|||
}
|
||||
}
|
||||
|
||||
return <Modal title="Launch Campaign" open={open} onClose={onClose}>
|
||||
return <Modal title={t('launch_campaign')} open={open} onClose={onClose}>
|
||||
{error && <Alert variant="error" title="Error">{error}</Alert>}
|
||||
<p>Please check to ensure all settings are correct before launching a campaign. A scheduled campaign can be aborted, but one sent immediately cannot.</p>
|
||||
<p>{t('launch_subtitle')}</p>
|
||||
<FormWrapper<CampaignLaunchParams>
|
||||
submitLabel="Launch"
|
||||
submitLabel={t(campaign.send_at ? 'reschedule' : 'launch')}
|
||||
onSubmit={handleLaunchCampaign}>
|
||||
{form => <>
|
||||
<RadioInput
|
||||
label="Launch Period"
|
||||
options={[{ key: 'now', label: 'Now' }, { key: 'later', label: 'Schedule' }]}
|
||||
label={t('launch_period')}
|
||||
options={[{ key: 'now', label: t('now') }, { key: 'later', label: t('schedule') }]}
|
||||
value={launchType}
|
||||
onChange={setLaunchType} />
|
||||
{launchType === 'later' && <>
|
||||
<DateTimeField
|
||||
control={form.control}
|
||||
name="send_at"
|
||||
label="Send At"
|
||||
label={t('Send At')}
|
||||
required />
|
||||
<SwitchField
|
||||
form={form}
|
||||
name="send_in_user_timezone"
|
||||
label="Send In Users Timezone"
|
||||
subtitle="Should the campaign go out at the selected time in the users timezone or in the projects timezone?" />
|
||||
label={t('send_in_user_timezone')}
|
||||
subtitle={t('send_in_user_timezone_desc')} />
|
||||
</>}
|
||||
</>}
|
||||
</FormWrapper>
|
||||
|
|
|
@ -35,7 +35,7 @@ export default function Settings() {
|
|||
|
||||
toast.success('Saved organization settings')
|
||||
}}
|
||||
submitLabel="Save Settings"
|
||||
submitLabel={t('save_settings')}
|
||||
>
|
||||
{form => <>
|
||||
<Heading size="h3" title={t('general')} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue