Skip to content

Commit

Permalink
add journey step name, journey entrance previewing (#267)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrishills authored Oct 11, 2023
1 parent 56ad60c commit 56757e0
Show file tree
Hide file tree
Showing 30 changed files with 784 additions and 115 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
exports.up = async function(knex) {
await knex.schema.alterTable('journey_steps', function(table) {
table.string('name', 128)
})
}

exports.down = async function(knex) {
await knex.schema.alterTable('journey_steps', function(table) {
table.dropColumn('name')
})
}
20 changes: 20 additions & 0 deletions apps/platform/src/core/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export default class Model {
for (const attribute of this.jsonAttributes) {
json[attribute] = JSON.stringify(json[attribute])
}

// remove any virtual attributes that have been set
for (const attribute of this.virtualAttributes) {
delete (json as any)[attribute]
}

return json
}

Expand Down Expand Up @@ -99,6 +105,20 @@ export default class Model {
return this.fromJson(record)
}

static async findMap<T extends typeof Model>(
this: T,
ids: number[],
db: Database = App.main.db,
) {
const m = new Map<number, InstanceType<T>>()
if (!ids.length) return m
const records = await this.all(q => q.whereIn('id', ids), db)
for (const record of records) {
m.set(record.id, record)
}
return m
}

static async all<T extends typeof Model>(
this: T,
query: Query = qb => qb,
Expand Down
4 changes: 2 additions & 2 deletions apps/platform/src/journey/Journey.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Model, { ModelParams } from '../core/Model'
import { setJourneyStepMap } from './JourneyRepository'
import { JourneyStepMap } from './JourneyStep'
import { JourneyStepMapParams } from './JourneyStep'

export default class Journey extends Model {
name!: string
Expand All @@ -14,7 +14,7 @@ export default class Journey extends Model {

static jsonAttributes = ['stats']

static async create(project_id: number, name: string, stepMap: JourneyStepMap) {
static async create(project_id: number, name: string, stepMap: JourneyStepMapParams) {
const journey = await this.insertAndFetch({
project_id,
name,
Expand Down
32 changes: 30 additions & 2 deletions apps/platform/src/journey/JourneyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { searchParamsSchema } from '../core/searchParams'
import { JSONSchemaType, validate } from '../core/validate'
import { extractQueryParams } from '../utilities'
import Journey, { JourneyParams } from './Journey'
import { createJourney, deleteJourney, getJourneyStepMap, getJourney, pagedJourneys, setJourneyStepMap, updateJourney } from './JourneyRepository'
import { JourneyStepMapParams, journeyStepTypes, toJourneyStepMap } from './JourneyStep'
import { createJourney, deleteJourney, getJourneyStepMap, getJourney, pagedJourneys, setJourneyStepMap, updateJourney, pagedEntrancesByJourney, getEntranceLog } from './JourneyRepository'
import { JourneyStepMapParams, JourneyUserStep, journeyStepTypes, toJourneyStepMap } from './JourneyStep'
import { User } from '../users/User'

const router = new Router<
ProjectState & { journey?: Journey }
Expand Down Expand Up @@ -52,6 +53,24 @@ router.post('/', async ctx => {
ctx.body = await createJourney(ctx.state.project.id, payload)
})

router.get('/entrances/:entranceId', async ctx => {
const entrance = await JourneyUserStep.first(q => q
.join('journeys', 'journey_user_step.journey_id', '=', 'journeys.id')
.where('journeys.project_id', ctx.state.project.id)
.where('journey_user_step.id', parseInt(ctx.params.entranceId, 10))
.whereNull('journey_user_step.entrance_id'),
)
if (!entrance) {
return ctx.throw(404)
}
const [user, journey, userSteps] = await Promise.all([
User.find(entrance.user_id),
Journey.find(entrance.journey_id),
getEntranceLog(entrance.id),
])
ctx.body = { journey, user, userSteps }
})

router.param('journeyId', async (value, ctx, next) => {
ctx.state.journey = await getJourney(parseInt(value), ctx.state.project.id)
if (!ctx.state.journey) {
Expand Down Expand Up @@ -86,6 +105,10 @@ const journeyStepsParamsSchema: JSONSchemaType<JourneyStepMapParams> = {
type: 'string',
enum: Object.keys(journeyStepTypes),
},
name: {
type: 'string',
nullable: true,
},
data: {
type: 'object', // TODO: Could validate further based on sub types
nullable: true,
Expand Down Expand Up @@ -133,4 +156,9 @@ router.put('/:journeyId/steps', async ctx => {
ctx.body = await toJourneyStepMap(steps, children)
})

router.get('/:journeyId/entrances', async ctx => {
const params = extractQueryParams(ctx.query, searchParamsSchema)
ctx.body = await pagedEntrancesByJourney(ctx.state.journey!.id, params)
})

export default router
62 changes: 61 additions & 1 deletion apps/platform/src/journey/JourneyRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PageParams } from '../core/searchParams'
import Journey, { JourneyParams, UpdateJourneyParams } from './Journey'
import { JourneyStep, JourneyEntrance, JourneyUserStep, JourneyStepMap, toJourneyStepMap, JourneyStepChild } from './JourneyStep'
import { createTagSubquery, getTags, setTags } from '../tags/TagService'
import { User } from '../users/User'

export const pagedJourneys = async (params: PageParams, projectId: number) => {
const result = await Journey.search(
Expand Down Expand Up @@ -109,12 +110,13 @@ export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepM
])

// Create or update steps
for (const [external_id, { type, x = 0, y = 0, data = {}, data_key }] of Object.entries(stepMap)) {
for (const [external_id, { type, x = 0, y = 0, data = {}, data_key, name = '' }] of Object.entries(stepMap)) {
const idx = steps.findIndex(s => s.external_id === external_id)
if (idx === -1) {
steps.push(await JourneyStep.insertAndFetch({
journey_id: journeyId,
type,
name,
external_id,
data,
data_key,
Expand All @@ -125,6 +127,7 @@ export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepM
const step = steps[idx]
steps[idx] = await JourneyStep.updateAndFetch(step.id, {
type,
name,
external_id,
data,
data_key,
Expand Down Expand Up @@ -204,3 +207,60 @@ export const getEntranceSubsequentSteps = async (entranceId: number) => {
.orderBy('id', 'asc'),
)
}

export const pagedEntrancesByJourney = async (journeyId: number, params: PageParams) => {
const r = await JourneyUserStep.search(params, q => q
.where('journey_id', journeyId)
.whereNull('entrance_id'),
)
if (r.results?.length) {
const users = await User.findMap(r.results.map(s => s.user_id))
return {
...r,
results: r.results.map(s => ({
id: s.id,
user: users.get(s.user_id),
created_at: s.created_at,
updated_at: s.updated_at,
ended_at: s.ended_at,
})),
}
}
return r
}

export const pagedEntrancesByUser = async (userId: number, params: PageParams) => {
const r = await JourneyUserStep.search(params, q => q
.where('user_id', userId)
.whereNull('entrance_id'),
)
if (r.results?.length) {
const journeys = await Journey.findMap(r.results.map(s => s.journey_id))
return {
...r,
results: r.results.map(s => ({
id: s.id,
journey: journeys.get(s.journey_id),
created_at: s.created_at,
updated_at: s.updated_at,
ended_at: s.ended_at,
})),
}
}
return r
}

export const getEntranceLog = async (entranceId: number) => {
const userSteps = await JourneyUserStep.all(q => q
.where(function() {
return this.where('id', entranceId).orWhere('entrance_id', entranceId)
})
.orderBy('id', 'asc'),
)
if (!userSteps.length) return userSteps
const steps = await JourneyStep.findMap(userSteps.map(s => s.step_id))
for (const userStep of userSteps) {
userStep.step = steps.get(userStep.step_id)
}
return userSteps
}
6 changes: 6 additions & 0 deletions apps/platform/src/journey/JourneyStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ export class JourneyUserStep extends Model {
data?: Record<string, unknown>
ref?: string

step?: JourneyStep

static tableName = 'journey_user_step'

static jsonAttributes = ['data']
static virtualAttributes = ['step']

static getDataMap(steps: JourneyStep[], userSteps: JourneyUserStep[]) {
return userSteps.reduceRight<Record<string, unknown>>((a, { data, step_id }) => {
Expand Down Expand Up @@ -52,6 +55,7 @@ export class JourneyStepChild extends Model {
export class JourneyStep extends Model {
type!: string
journey_id!: number
name?: string
data?: Record<string, unknown>
external_id!: string
data_key?: string // make data stored in user steps available in templates
Expand Down Expand Up @@ -396,6 +400,7 @@ export const journeyStepTypes = [

interface JourneyStepMapItem {
type: string
name?: string
data?: Record<string, unknown>
data_key?: string
x: number
Expand All @@ -420,6 +425,7 @@ export async function toJourneyStepMap(steps: JourneyStep[], children: JourneySt
for (const step of steps) {
editData[step.external_id] = {
type: step.type,
name: step.name ?? '',
data: step.data ?? {},
data_key: step.data_key,
x: step.x ?? 0,
Expand Down
6 changes: 6 additions & 0 deletions apps/platform/src/users/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getUserSubscriptions, toggleSubscription } from '../subscriptions/Subsc
import { SubscriptionState } from '../subscriptions/Subscription'
import { getUserEvents } from './UserEventRepository'
import { projectRoleMiddleware } from '../projects/ProjectService'
import { pagedEntrancesByUser } from '../journey/JourneyRepository'

const router = new Router<
ProjectState & { user?: User }
Expand Down Expand Up @@ -188,4 +189,9 @@ router.patch('/:userId/subscriptions', async ctx => {
ctx.body = await getUser(ctx.state.user!.id)
})

router.get('/:userId/journeys', async ctx => {
const params = extractQueryParams(ctx.query, searchParamsSchema)
ctx.body = await pagedEntrancesByUser(ctx.state.user!.id, params)
})

export default router
16 changes: 15 additions & 1 deletion apps/ui/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Axios from 'axios'
import { env } from './config/env'
import { Admin, AuthMethod, Campaign, CampaignCreateParams, CampaignLaunchParams, CampaignUpdateParams, CampaignUser, Image, Journey, JourneyStepMap, List, ListCreateParams, ListUpdateParams, Locale, Metric, Organization, OrganizationUpdateParams, Project, ProjectAdmin, ProjectAdminInviteParams, ProjectAdminParams, ProjectApiKey, ProjectApiKeyParams, Provider, ProviderCreateParams, ProviderMeta, ProviderUpdateParams, QueueMetric, RuleSuggestions, SearchParams, SearchResult, Subscription, SubscriptionParams, Tag, Template, TemplateCreateParams, TemplatePreviewParams, TemplateProofParams, TemplateUpdateParams, User, UserEvent, UserSubscription } from './types'
import { Admin, AuthMethod, Campaign, CampaignCreateParams, CampaignLaunchParams, CampaignUpdateParams, CampaignUser, Image, Journey, JourneyEntranceDetail, JourneyStepMap, JourneyUserStep, List, ListCreateParams, ListUpdateParams, Locale, Metric, Organization, OrganizationUpdateParams, Project, ProjectAdmin, ProjectAdminInviteParams, ProjectAdminParams, ProjectApiKey, ProjectApiKeyParams, Provider, ProviderCreateParams, ProviderMeta, ProviderUpdateParams, QueueMetric, RuleSuggestions, SearchParams, SearchResult, Subscription, SubscriptionParams, Tag, Template, TemplateCreateParams, TemplatePreviewParams, TemplateProofParams, TemplateUpdateParams, User, UserEvent, UserSubscription } from './types'

function appendValue(params: URLSearchParams, name: string, value: unknown) {
if (typeof value === 'undefined' || value === null || typeof value === 'function') return
Expand Down Expand Up @@ -177,6 +177,14 @@ const api = {
.put<JourneyStepMap>(`/admin/projects/${projectId}/journeys/${journeyId}/steps`, stepData)
.then(r => r.data),
},
entrances: {
search: async (projectId: number | string, journeyId: number | string, params: SearchParams) => await client
.get<SearchResult<JourneyUserStep>>(`/admin/projects/${projectId}/journeys/${journeyId}/entrances`, { params })
.then(r => r.data),
log: async (projectId: number | string, entranceId: number | string) => await client
.get<JourneyEntranceDetail>(`${projectUrl(projectId)}/journeys/entrances/${entranceId}`)
.then(r => r.data),
},
},

templates: {
Expand All @@ -199,6 +207,12 @@ const api = {
updateSubscriptions: async (projectId: number | string, userId: number | string, subscriptions: SubscriptionParams[]) => await client
.patch(`${projectUrl(projectId)}/users/${userId}/subscriptions`, subscriptions)
.then(r => r.data),

journeys: {
search: async (projectId: number | string, userId: number | string, params: SearchParams) => await client
.get<SearchResult<JourneyUserStep>>(`${projectUrl(projectId)}/users/${userId}/journeys`, { params })
.then(r => r.data),
},
},

lists: {
Expand Down
37 changes: 37 additions & 0 deletions apps/ui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,43 @@ label .switch .slider.round:before {
height: 16px;
}

.icon-box {
align-self: start;
border-radius: 4px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}

code {
word-wrap: break-word;
background-color: var(--color-grey-soft);
border-radius: 2px;
padding: 2px;
}

.blue {
background-color: var(--color-blue-soft);
color: var(--color-blue-hard);
}

.green {
background-color: var(--color-green-soft);
color: var(--color-green-hard);
}

.red {
background-color: var(--color-red-soft);
color: var(--color-red-hard);
}

.yellow {
background-color: var(--color-yellow-soft);
color: var(--color-yellow-hard);
}

@media only screen and (max-width: 600px) {
.page-content {
padding: 0 20px 20px;
Expand Down
23 changes: 22 additions & 1 deletion apps/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export interface Journey {
export interface JourneyStep<T = any> {
id: number
type: string
child_id?: number
name: string
data: T
x: number
y: number
Expand All @@ -267,6 +267,7 @@ interface JourneyStepMapChild<E = any> {
export interface JourneyStepMap {
[external_id: string]: {
type: string
name: string
data?: Record<string, unknown>
x: number
y: number
Expand All @@ -293,6 +294,7 @@ export interface JourneyStepType<T = any, E = any> {
icon: ReactNode
category: 'entrance' | 'delay' | 'flow' | 'action'
description: string
Describe?: ComponentType<JourneyStepTypeEditProps<T>>
newData?: () => Promise<T>
newEdgeData?: () => Promise<E>
Edit?: ComponentType<JourneyStepTypeEditProps<T>>
Expand All @@ -304,6 +306,25 @@ export interface JourneyStepType<T = any, E = any> {
hasDataKey?: boolean
}

export interface JourneyUserStep {
id: number
type: string
delay_until?: string
created_at: string
updated_at: string
ended_at?: string

user?: User
journey?: Journey
step?: JourneyStep
}

export interface JourneyEntranceDetail {
journey: Journey
user: User
userSteps: JourneyUserStep[]
}

export type CampaignState = 'draft' | 'pending' | 'scheduled' | 'running' | 'finished' | 'aborted'

export interface CampaignDelivery {
Expand Down
Loading

0 comments on commit 56757e0

Please sign in to comment.