Skip to content

Commit

Permalink
fix: re-fetch campaign details after campaign is sent (#1116)
Browse files Browse the repository at this point in the history
* fix(campaign): correct Date types to string

Even though sent_at, created_at, and status_updated_at are represented
as Dates in the backend, these fields will be serialized to an ISO
string which the frontend then receives.

Hence, correct the type definition of these fields.

* refactor(dashboard): refactor onModalConfirm to a util function

* fix(dashboard): update local sentAt when sending campaign

When sending a campaign for the first time, the `sentAt` field for the
in-memory representation of `Campaign` is still undefined. This causes
"invalid_Date" to appear in the filename of delivery reports.

Fix this by updating the `sentAt` field of the in-memory `Campaign` to
the current date when sending the campaign for the first time.

Fixes issue #1113

* fix(campaign): mark sentAt and statusUpdatedAt as possibly undefined

* chore: refactor campaign stats polling to a custom hook

Note: make `refreshCampaignStats` a memoized callback to satisfy the
useEffect dependencies.

* Revert "fix(dashboard): update local sentAt when sending campaign"

Instead, we'll get the actual sentAt from the source.

This reverts commit 2abf7b3.

* fix: fall back to the current date if sentAt is undefined

* fix: fetch finalized campaign details after campaign is sent
  • Loading branch information
zwliew authored Apr 14, 2021
1 parent 53180e6 commit 381f1cf
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 139 deletions.
10 changes: 5 additions & 5 deletions frontend/src/classes/Campaign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ export class Campaign {
id: number
name: string
type: ChannelType
createdAt: Date
sentAt: Date
createdAt: string
sentAt?: string
status: Status
isCsvProcessing: boolean
statusUpdatedAt: Date
statusUpdatedAt?: string
protect: boolean
redacted: boolean
demoMessageLimit: number | null
Expand Down Expand Up @@ -71,8 +71,8 @@ export class CampaignStats {
sent: number
invalid: number
status: Status
statusUpdatedAt: Date // Timestamp when job's status was changed to this status
updatedAt: Date // Timestamp when statistic was updated
statusUpdatedAt: string // Timestamp when job's status was changed to this status
updatedAt: string // Timestamp when statistic was updated
halted?: boolean
waitTime?: number
redacted?: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ const ExportRecipients = ({
campaignId: number
campaignName: string
campaignType: ChannelType
sentAt: Date
sentAt?: string
status: Status
statusUpdatedAt: Date
statusUpdatedAt?: string
iconPosition: 'left' | 'right'
isButton?: boolean
}) => {
Expand All @@ -48,7 +48,7 @@ const ExportRecipients = ({
let timeoutId: NodeJS.Timeout

function getExportStatus(): void {
if (status === Status.Sending) {
if (status === Status.Sending || !statusUpdatedAt) {
return setExportStatus(CampaignExportStatus.Unavailable)
}

Expand Down Expand Up @@ -104,7 +104,16 @@ const ExportRecipients = ({
content = content.concat(emptyExplanation)
}

const sentAtTime = new Date(sentAt)
let sentAtTime
if (!sentAt) {
console.error(
'sentAt is undefined. Using current Date() as a fallback.'
)
sentAtTime = new Date()
} else {
sentAtTime = new Date(sentAt)
}

download(
new Blob(content),
`${campaignName}_${sentAtTime.toLocaleDateString()}_${sentAtTime.toLocaleTimeString()}.csv`,
Expand Down
40 changes: 40 additions & 0 deletions frontend/src/components/custom-hooks/use-poll-campaign-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CampaignStats, Status } from 'classes'
import { useState, useEffect, useCallback, useContext } from 'react'
import { getCampaignStats, getCampaignDetails } from 'services/campaign.service'
import { CampaignContext } from 'contexts/campaign.context'

function usePollCampaignStats() {
const { campaign, setCampaign } = useContext(CampaignContext)
const { id } = campaign
const [stats, setStats] = useState(new CampaignStats({}))

const refreshCampaignStats = useCallback(
async (forceRefresh = false) => {
const updatedStats = await getCampaignStats(id, forceRefresh)
setStats(updatedStats)
return updatedStats
},
[id]
)

useEffect(() => {
let timeoutId: NodeJS.Timeout

async function poll() {
const { status } = await refreshCampaignStats()
if (status !== Status.Sent) {
timeoutId = setTimeout(poll, 2000)
} else {
const updatedCampaign = await getCampaignDetails(id)
setCampaign(updatedCampaign)
}
}
poll()

return () => clearTimeout(timeoutId)
}, [stats.status, refreshCampaignStats, id, setCampaign])

return { stats, refreshCampaignStats }
}

export default usePollCampaignStats
42 changes: 8 additions & 34 deletions frontend/src/components/dashboard/create/email/EmailDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import React, { useState, useEffect, useContext } from 'react'
import React, { useContext } from 'react'

import { CampaignContext } from 'contexts/campaign.context'
import { Status, CampaignStats, ChannelType } from 'classes/Campaign'
import {
getCampaignStats,
stopCampaign,
retryCampaign,
} from 'services/campaign.service'
import { Status, ChannelType } from 'classes/Campaign'
import { stopCampaign, retryCampaign } from 'services/campaign.service'
import { StepHeader, ProgressDetails } from 'components/common'
import { GA_USER_EVENTS, sendUserEvent } from 'services/ga.service'
import usePollCampaignStats from 'components/custom-hooks/use-poll-campaign-stats'

const EmailDetail = () => {
const { campaign } = useContext(CampaignContext)
const { id } = campaign
const [stats, setStats] = useState(new CampaignStats({}))

async function refreshCampaignStats(id: number, forceRefresh = false) {
const campaignStats = await getCampaignStats(id, forceRefresh)
setStats(campaignStats)
return campaignStats
}
const { stats, refreshCampaignStats } = usePollCampaignStats()

async function handleRefreshStats() {
try {
await refreshCampaignStats(id, true)
await refreshCampaignStats(true)
} catch (err) {
console.error(err)
}
Expand All @@ -33,7 +24,7 @@ const EmailDetail = () => {
try {
sendUserEvent(GA_USER_EVENTS.PAUSE_SENDING, ChannelType.Email)
await stopCampaign(id)
await refreshCampaignStats(id)
await refreshCampaignStats()
} catch (err) {
console.error(err)
}
Expand All @@ -43,29 +34,12 @@ const EmailDetail = () => {
try {
sendUserEvent(GA_USER_EVENTS.RETRY_RESUME_SENDING, ChannelType.Email)
await retryCampaign(id)
await refreshCampaignStats(id)
await refreshCampaignStats()
} catch (err) {
console.error(err)
}
}

useEffect(() => {
let timeoutId: NodeJS.Timeout

async function poll() {
const { status } = await refreshCampaignStats(id)

if (status !== Status.Sent) {
timeoutId = setTimeout(poll, 2000)
}
}

poll()
return () => {
timeoutId && clearTimeout(timeoutId)
}
}, [id, stats.status])

function renderProgressHeader() {
if (stats.waitTime && stats.waitTime > 0) {
const waitMin = Math.ceil(stats.waitTime / 60)
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/components/dashboard/create/email/EmailSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, {
import { useParams } from 'react-router-dom'

import { CampaignContext } from 'contexts/campaign.context'
import { EmailProgress, Status } from 'classes'
import { ChannelType, EmailProgress } from 'classes'
import { ModalContext } from 'contexts/modal.context'
import {
EmailPreviewBlock,
Expand All @@ -20,8 +20,7 @@ import {
StepSection,
} from 'components/common'
import { getPreviewMessage } from 'services/email.service'
import { sendCampaign } from 'services/campaign.service'

import { confirmSendCampaign } from '../util'
import styles from '../Create.module.scss'

const EmailSend = ({
Expand Down Expand Up @@ -61,9 +60,13 @@ const EmailSend = ({
loadPreview(campaignId)
}, [campaignId])

const onModalConfirm = async () => {
await sendCampaign(+campaignId, 0)
updateCampaign({ status: Status.Sending })
const onModalConfirm = () => {
confirmSendCampaign({
campaignId: +campaignId,
sendRate: 0,
channelType: ChannelType.Email,
updateCampaign,
})
}

const openModal = () => {
Expand Down
42 changes: 8 additions & 34 deletions frontend/src/components/dashboard/create/sms/SMSDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
import React, { useState, useEffect, useContext } from 'react'
import React, { useEffect, useContext } from 'react'

import { CampaignContext } from 'contexts/campaign.context'
import { Status, CampaignStats, ChannelType } from 'classes/Campaign'
import {
getCampaignStats,
stopCampaign,
retryCampaign,
} from 'services/campaign.service'
import { Status, ChannelType } from 'classes/Campaign'
import { stopCampaign, retryCampaign } from 'services/campaign.service'
import { StepHeader, ProgressDetails } from 'components/common'
import { ModalContext } from 'contexts/modal.context'
import { GA_USER_EVENTS, sendUserEvent } from 'services/ga.service'
import CompletedDemoModal from 'components/dashboard/demo/completed-demo-modal'
import usePollCampaignStats from 'components/custom-hooks/use-poll-campaign-stats'

const SMSDetail = () => {
const { setModalContent } = useContext(ModalContext) // Destructured to avoid the addition of modalContext to useEffect's dependencies
const { campaign } = useContext(CampaignContext)
const { id, demoMessageLimit } = campaign
const isDemo = !!demoMessageLimit
const [stats, setStats] = useState(new CampaignStats({}))

async function refreshCampaignStats(id: number, forceRefresh = false) {
const campaignStats = await getCampaignStats(id, forceRefresh)
setStats(campaignStats)
return campaignStats
}
const { stats, refreshCampaignStats } = usePollCampaignStats()

async function handleRefreshStats() {
try {
await refreshCampaignStats(id, true)
await refreshCampaignStats(true)
} catch (err) {
console.error(err)
}
Expand All @@ -37,7 +28,7 @@ const SMSDetail = () => {
try {
sendUserEvent(GA_USER_EVENTS.PAUSE_SENDING, ChannelType.SMS)
await stopCampaign(id)
await refreshCampaignStats(id)
await refreshCampaignStats()
} catch (err) {
console.error(err)
}
Expand All @@ -47,29 +38,12 @@ const SMSDetail = () => {
try {
sendUserEvent(GA_USER_EVENTS.RETRY_RESUME_SENDING, ChannelType.SMS)
await retryCampaign(id)
await refreshCampaignStats(id)
await refreshCampaignStats()
} catch (err) {
console.error(err)
}
}

useEffect(() => {
let timeoutId: NodeJS.Timeout

async function poll() {
const { status } = await refreshCampaignStats(id)

if (status !== Status.Sent) {
timeoutId = setTimeout(poll, 2000)
}
}

poll()
return () => {
timeoutId && clearTimeout(timeoutId)
}
}, [id, stats.status])

useEffect(() => {
function renderCompletedDemoModal() {
setModalContent(
Expand Down
19 changes: 9 additions & 10 deletions frontend/src/components/dashboard/create/sms/SMSSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useContext, useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'

import { CampaignContext } from 'contexts/campaign.context'
import { Status, ChannelType } from 'classes'
import { ChannelType } from 'classes'
import { ModalContext } from 'contexts/modal.context'
import {
PreviewBlock,
Expand All @@ -15,12 +15,10 @@ import {
StepSection,
} from 'components/common'
import { getPreviewMessage } from 'services/sms.service'
import { sendCampaign } from 'services/campaign.service'
import { GA_USER_EVENTS, sendUserEvent } from 'services/ga.service'

import type { SMSProgress } from 'classes'
import type { Dispatch, SetStateAction } from 'react'

import { confirmSendCampaign } from '../util'
import styles from '../Create.module.scss'

const SMSSend = ({
Expand Down Expand Up @@ -54,12 +52,13 @@ const SMSSend = ({
loadPreview(campaignId)
}, [campaignId])

const onModalConfirm = async () => {
await sendCampaign(+campaignId, +sendRate)
if (sendRate) {
sendUserEvent(GA_USER_EVENTS.USE_SEND_RATE, ChannelType.SMS)
}
updateCampaign({ status: Status.Sending })
const onModalConfirm = () => {
confirmSendCampaign({
campaignId: +campaignId,
sendRate: +sendRate,
channelType: ChannelType.SMS,
updateCampaign,
})
}

const openModal = () => {
Expand Down
Loading

0 comments on commit 381f1cf

Please sign in to comment.