Skip to content

Commit

Permalink
Allows for exiting a journey via API (#408)
Browse files Browse the repository at this point in the history
  • Loading branch information
pushchris authored Mar 27, 2024
1 parent 29a276a commit 57ecb39
Show file tree
Hide file tree
Showing 8 changed files with 54 additions and 12 deletions.
32 changes: 28 additions & 4 deletions apps/platform/src/journey/JourneyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import JourneyError from './JourneyError'
import { EventPostJob } from '../jobs'
import { UserEvent } from '../users/UserEvent'
import JourneyProcessJob from './JourneyProcessJob'
import { getUserFromContext } from '../users/UserRepository'

const router = new Router<
ProjectState & { journey?: Journey }
Expand Down Expand Up @@ -177,19 +178,42 @@ router.get('/:journeyId/entrances', async ctx => {
ctx.body = await pagedEntrancesByJourney(ctx.state.journey!.id, params)
})

router.delete('/:journeyId/entrances/:entranceId/users/:userId', async ctx => {
const user = await getUserFromContext(ctx)
if (!user) return ctx.throw(404)
const results = await JourneyUserStep.update(
q => q.where('user_id', user.id)
.where('entrance_id', parseInt(ctx.params.entranceId))
.whereNull('ended_at')
.where('journey_id', ctx.state.journey!.id),
{ ended_at: new Date() },
)
ctx.body = { exits: results }
})

router.get('/:journeyId/steps/:stepId/users', async ctx => {
const params = extractQueryParams(ctx.query, searchParamsSchema)
const step = await JourneyStep.first(q => q
.where('journey_id', ctx.state.journey!.id)
.where('id', parseInt(ctx.params.stepId)),
)
if (!step) {
ctx.throw(404)
return
}
if (!step) return ctx.throw(404)
ctx.body = await pagedUsersByStep(step.id, params)
})

router.delete('/:journeyId/users/:userId', async ctx => {
const user = await getUserFromContext(ctx)
if (!user) return ctx.throw(404)
const results = await JourneyUserStep.update(
q => q.where('user_id', user.id)
.whereNull('entrance_id')
.whereNull('ended_at')
.where('journey_id', ctx.state.journey!.id),
{ ended_at: new Date() },
)
ctx.body = { exits: results }
})

interface JourneyEntranceTriggerParams {
entrance_id: number
user: Pick<User, 'email' | 'phone' | 'timezone' | 'locale'> & { external_id: string, device_token?: string }
Expand Down
1 change: 1 addition & 0 deletions apps/platform/src/journey/JourneyRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ export const pagedEntrancesByUser = async (userId: number, params: PageParams) =
...r,
results: r.results.map(s => ({
id: s.id,
entrance_id: s.id,
journey: journeys.get(s.journey_id),
created_at: s.created_at,
updated_at: s.updated_at,
Expand Down
4 changes: 2 additions & 2 deletions apps/platform/src/users/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { JSONSchemaType, validate } from '../core/validate'
import { User, UserParams } from './User'
import { extractQueryParams } from '../utilities'
import { searchParamsSchema, SearchSchema } from '../core/searchParams'
import { getUser, pagedUsers } from './UserRepository'
import { getUser, getUserFromContext, pagedUsers } from './UserRepository'
import { getUserLists } from '../lists/ListService'
import { getUserSubscriptions, toggleSubscription } from '../subscriptions/SubscriptionService'
import { SubscriptionState } from '../subscriptions/Subscription'
Expand Down Expand Up @@ -146,7 +146,7 @@ router.delete('/', projectRoleMiddleware('editor'), async ctx => {
})

router.param('userId', async (value, ctx, next) => {
ctx.state.user = await getUser(parseInt(value), ctx.state.project.id)
ctx.state.user = await getUserFromContext(ctx)
if (!ctx.state.user) {
ctx.throw(404)
return
Expand Down
7 changes: 7 additions & 0 deletions apps/platform/src/users/UserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { uuid } from '../utilities'
import { getRuleEventNames } from '../rules/RuleHelpers'
import { UserEvent } from './UserEvent'
import { createEvent } from './UserEventRepository'
import { Context } from 'koa'

export const getUser = async (id: number, projectId?: number): Promise<User | undefined> => {
return await User.find(id, qb => {
Expand All @@ -18,6 +19,12 @@ export const getUser = async (id: number, projectId?: number): Promise<User | un
})
}

export const getUserFromContext = async (ctx: Context): Promise<User | undefined> => {
return ctx.state.scope === 'secret'
? await getUserFromClientId(ctx.state.project.id, { external_id: ctx.params.userId })
: await getUser(parseInt(ctx.params.userId), ctx.state.project.id)
}

export const getUsersFromIdentity = async (projectId: number, identity: ClientIdentity) => {
const externalId = `${identity.external_id}`
const anonymousId = `${identity.anonymous_id}`
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ export interface JourneyStepType<T = any, E = any> {

export interface JourneyUserStep {
id: number
entrance_id: number
type: string
delay_until?: string
created_at: string
Expand Down
9 changes: 6 additions & 3 deletions apps/ui/src/views/journey/EntranceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PreferencesContext } from '../../ui/PreferencesContext'
import * as stepTypes from './steps'
import clsx from 'clsx'
import { stepCategoryColors } from './JourneyEditor'
import { useTranslation } from 'react-i18next'

export const typeVariants: Record<string, TagProps['variant']> = {
completed: 'success',
Expand All @@ -19,6 +20,7 @@ export const typeVariants: Record<string, TagProps['variant']> = {

export default function EntranceDetails() {

const { t } = useTranslation()
const [preferences] = useContext(PreferencesContext)

const { journey, user, userSteps } = useLoaderData() as JourneyEntranceDetail
Expand Down Expand Up @@ -59,22 +61,23 @@ export default function EntranceDetails() {
</div>
<div className="text">
<div className="title">{item.step!.name || 'Untitled'}</div>
<div className="subtitle">{item.step!.type}</div>
<div className="subtitle">{t(item.step!.type)}</div>
</div>
</div>
)
},
},
{
key: 'type',
title: 'Type',
cell: ({ item }) => (
<Tag variant={typeVariants[item.type]}>
{camelToTitle(item.type)}
</Tag>
),
},
{ key: 'created_at' },
{ key: 'delay_until' },
{ key: 'created_at', title: t('created_at') },
{ key: 'delay_until', title: t('delay_until') },
]}
/>
</PageContent>
Expand Down
3 changes: 2 additions & 1 deletion apps/ui/src/views/journey/JourneyEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function JourneyStepNode({

if (!stats) stats = {}

const { t } = useTranslation()
const [project] = useContext(ProjectContext)
const [journey] = useContext(JourneyContext)
const { getNode, getEdges } = useReactFlow()
Expand Down Expand Up @@ -191,7 +192,7 @@ function JourneyStepNode({
<span className={clsx('step-header-icon', stepCategoryColors[type.category])}>
{type.icon}
</span>
<h4 className="step-header-title">{name || type.name}</h4>
<h4 className="step-header-title">{name || t(type.name)}</h4>
<div className="step-header-stats">
<span className="stat">
{(stats.completed ?? 0).toLocaleString()}
Expand Down
9 changes: 7 additions & 2 deletions apps/ui/src/views/users/UserDetailJourneys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import api from '../../api'
import { Tag } from '../../ui'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { PreferencesContext } from '../../ui/PreferencesContext'
import { formatDate } from '../../utils'

export default function UserDetailJourneys() {

Expand All @@ -17,6 +19,7 @@ export default function UserDetailJourneys() {
const projectId = project.id
const userId = user.id

const [preferences] = useContext(PreferencesContext)
const state = useSearchTableQueryState(useCallback(async params => await api.users.journeys.search(projectId, userId, params), [projectId, userId]))

return (
Expand All @@ -36,10 +39,12 @@ export default function UserDetailJourneys() {
{
key: 'ended_at',
title: t('ended_at'),
cell: ({ item }) => item.ended_at ?? <Tag variant="info">{t('running')}</Tag>,
cell: ({ item }) => item.ended_at
? formatDate(preferences, item.ended_at, 'Ppp')
: <Tag variant="info">{t('running')}</Tag>,
},
]}
onSelectRow={e => navigate(`../../entrances/${e.id}`)}
onSelectRow={e => navigate(`../../entrances/${e.entrance_id}`)}
/>
)
}

0 comments on commit 57ecb39

Please sign in to comment.