Skip to content

Commit

Permalink
adds modal to list all users who have interacted with a step
Browse files Browse the repository at this point in the history
  • Loading branch information
chrishills committed Oct 15, 2023
1 parent 93f05a5 commit 9377f8c
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 5 deletions.
17 changes: 15 additions & 2 deletions apps/platform/src/journey/JourneyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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, pagedEntrancesByJourney, getEntranceLog } from './JourneyRepository'
import { JourneyStepMapParams, JourneyUserStep, journeyStepTypes, toJourneyStepMap } from './JourneyStep'
import { createJourney, deleteJourney, getJourneyStepMap, getJourney, pagedJourneys, setJourneyStepMap, updateJourney, pagedEntrancesByJourney, getEntranceLog, pagedUsersByStep } from './JourneyRepository'
import { JourneyStep, JourneyStepMapParams, JourneyUserStep, journeyStepTypes, toJourneyStepMap } from './JourneyStep'
import { User } from '../users/User'

const router = new Router<
Expand Down Expand Up @@ -161,4 +161,17 @@ router.get('/:journeyId/entrances', async ctx => {
ctx.body = await pagedEntrancesByJourney(ctx.state.journey!.id, params)
})

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
}
ctx.body = await pagedUsersByStep(step.id, params)
})

export default router
21 changes: 21 additions & 0 deletions apps/platform/src/journey/JourneyRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,27 @@ export const pagedEntrancesByUser = async (userId: number, params: PageParams) =
return r
}

export const pagedUsersByStep = async (stepId: number, params: PageParams) => {
const r = await JourneyUserStep.search(params, q => q
.where('step_id', stepId)
.orderBy('id', 'desc'),
)
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 getEntranceLog = async (entranceId: number) => {
const userSteps = await JourneyUserStep.all(q => q
.where(function() {
Expand Down
3 changes: 3 additions & 0 deletions apps/platform/src/journey/JourneyStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ export type JourneyStepMap = Record<string, JourneyStepMapItem & {
stats?: Record<string, number>
stats_at?: Date
next_scheduled_at?: Date
id?: number
}>

export type JourneyStepMapParams = Record<string, JourneyStepMapItem>
Expand Down Expand Up @@ -481,6 +482,8 @@ export async function toJourneyStepMap(steps: JourneyStep[], children: JourneySt
return a
}, []),
stats: step.stats,
next_scheduled_at: step.next_scheduled_at ?? undefined,
id: step.id,
}
}

Expand Down
3 changes: 3 additions & 0 deletions apps/ui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ const api = {
set: async (projectId: number | string, journeyId: number | string, stepData: JourneyStepMap) => await client
.put<JourneyStepMap>(`/admin/projects/${projectId}/journeys/${journeyId}/steps`, stepData)
.then(r => r.data),
searchUsers: async (projectId: number | string, journeyId: number | string, stepId: number | string, params: SearchParams) => await client
.get<SearchResult<JourneyUserStep>>(`/admin/projects/${projectId}/journeys/${journeyId}/steps/${stepId}/users`, { params })
.then(r => r.data),
},
entrances: {
search: async (projectId: number | string, journeyId: number | string, params: SearchParams) => await client
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 @@ -277,6 +277,7 @@ export interface JourneyStepMap {
children?: JourneyStepMapChild[]
stats?: Record<string, number>
stats_at?: Date
id?: number
}
}

Expand Down
3 changes: 2 additions & 1 deletion apps/ui/src/ui/SearchTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,12 @@ export const useTableSearchParams = () => {
/**
* local state
*/
export function useSearchTableState<T>(loader: (params: SearchParams) => Promise<SearchResult<T> | null>) {
export function useSearchTableState<T>(loader: (params: SearchParams) => Promise<SearchResult<T> | null>, initialParams?: Partial<SearchParams>) {

const [params, setParams] = useState<SearchParams>({
limit: 25,
q: '',
...initialParams ?? {},
})

const [results,, reload] = useResolver(useCallback(async () => await loader(params), [loader, params]))
Expand Down
83 changes: 81 additions & 2 deletions apps/ui/src/views/journey/JourneyEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import { JourneyForm } from './JourneyForm'
import { ActionStepIcon, CheckCircleIcon, CloseIcon, CopyIcon, DelayStepIcon, EntranceStepIcon, ForbiddenIcon } from '../../ui/icons'
import Tag from '../../ui/Tag'
import TextInput from '../../ui/form/TextInput'
import { SearchTable } from '../../ui'
import { useSearchTableState } from '../../ui/SearchTable'

const getStepType = (type: string) => (type ? journeySteps[type as keyof typeof journeySteps] as JourneyStepType : null) ?? null

Expand All @@ -65,6 +67,58 @@ export const stepCategoryColors = {
delay: 'yellow',
}

interface StepUsersProps {
stepId: number
entrance?: boolean
}

function StepUsers({ entrance, stepId }: StepUsersProps) {

const [{ id: projectId }] = useContext(ProjectContext)
const [{ id: journeyId }] = useContext(JourneyContext)

const state = useSearchTableState(useCallback(async params => await api.journeys.steps.searchUsers(projectId, journeyId, stepId, params), [projectId, journeyId, stepId]), {
limit: 10,
})

return (
<>
<SearchTable
{...state}
columns={[
{
key: 'name',
cell: ({ item }) => item.user!.full_name ?? '-',
},
{
key: 'external_id',
cell: ({ item }) => item.user?.external_id ?? '-',
},
{
key: 'email',
cell: ({ item }) => item.user?.email ?? '-',
},
{
key: 'phone',
cell: ({ item }) => item.user?.phone ?? '-',
},
{
key: 'created_at',
title: 'Step Date',
cell: ({ item }) => item.created_at,
},
{
key: 'delay_until',
title: 'Delay Until',
cell: ({ item }) => item.delay_until,
},
]}
onSelectRow={entrance ? ({ id }) => window.open(`/projects/${projectId}/entrances/${id}`, '_blank') : undefined}
/>
</>
)
}

function JourneyStepNode({
id,
data: {
Expand Down Expand Up @@ -303,7 +357,7 @@ function stepsToNodes(stepMap: JourneyStepMap) {
const nodes: Node[] = []
const edges: Edge[] = []

for (const [id, { x, y, type, data, name, children, stats, stats_at }] of Object.entries(stepMap)) {
for (const [id, { x, y, type, data, name, children, stats, stats_at, id: stepId }] of Object.entries(stepMap)) {
nodes.push({
id,
position: {
Expand All @@ -317,6 +371,7 @@ function stepsToNodes(stepMap: JourneyStepMap) {
data,
stats,
stats_at,
stepId,
},
})
const stepType = getStepType(type)
Expand Down Expand Up @@ -510,6 +565,8 @@ export default function JourneyEditor() {

const editNode = nodes.find(n => n.data.editing)

const [viewUsersStep, setViewUsersStep] = useState<null | { stepId: number, entrance?: boolean }>(null)

const onNodeDoubleClick = useCallback<NodeMouseHandler>((_, n) => {
setNodes(nds => nds.map(x => x.id === n.id
? {
Expand Down Expand Up @@ -539,7 +596,17 @@ export default function JourneyEditor() {
<h4 className="step-header-title">{type.name}</h4>
{
editNode.data.stats && (
<div className="step-header-stats">
<div
className="step-header-stats"
role={editNode.data.stepId ? 'button' : undefined}
onClick={editNode.data.stepId
? () => setViewUsersStep({ stepId: editNode.data.stepId, entrance: editNode.data.type === 'entrance' })
: undefined
}
style={{
cursor: editNode.data.stepId ? 'cursor' : undefined,
}}
>
<span className="stat">
{editNode.data.stats.completed ?? 0}
{statIcons.completed}
Expand Down Expand Up @@ -735,6 +802,18 @@ export default function JourneyEditor() {
}}
/>
</Modal>
<Modal
open={!!viewUsersStep}
onClose={() => setViewUsersStep(null)}
title="Users"
size="large"
>
{
viewUsersStep && (
<StepUsers {...viewUsersStep} />
)
}
</Modal>
</Modal>
)
}

0 comments on commit 9377f8c

Please sign in to comment.