Skip to content

Commit

Permalink
Added step child path field for explicit grouping (#401)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrishills authored Mar 23, 2024
1 parent b2436d3 commit 8b81756
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
exports.up = async function(knex) {
await knex.schema.alterTable('journey_step_child', function(table) {
table.string('path', 128)
})
await knex.raw(`
update journey_step_child sc
inner join journey_steps s on sc.step_id = s.id
set sc.path = 'yes'
where s.type = 'gate' and priority = 0;
`)
await knex.raw(`
update journey_step_child sc
inner join journey_steps s on sc.step_id = s.id
set sc.path = 'no'
where s.type = 'gate' and priority > 0;
`)
}

exports.down = async function(knex) {
await knex.schema.alterTable('journey_step_child', function(table) {
table.dropColumn('path')
})
}
4 changes: 4 additions & 0 deletions apps/platform/src/journey/JourneyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ const journeyStepsParamsSchema: JSONSchemaType<JourneyStepMapParams> = {
external_id: {
type: 'string',
},
path: {
type: 'string',
nullable: true,
},
data: {
type: 'object', // TODO: this is also specific to the parent node's type
nullable: true,
Expand Down
4 changes: 3 additions & 1 deletion apps/platform/src/journey/JourneyRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepM
const childIds: number[] = []

let ci = 0
for (const { external_id, data = {} } of list) {
for (const { external_id, path, data = {} } of list) {
const child = steps.find(s => s.external_id === external_id)
if (!child) continue
const idx = children.findIndex(sc => sc.step_id === step.id && sc.child_id === child.id)
Expand All @@ -173,12 +173,14 @@ export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepM
step_id: step.id,
child_id: child.id,
data,
path,
priority: ci,
}, trx))
} else {
stepChild = children[idx]
children[idx] = await JourneyStepChild.updateAndFetch(stepChild.id, {
data,
path,
priority: ci,
}, trx)
}
Expand Down
11 changes: 8 additions & 3 deletions apps/platform/src/journey/JourneyStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class JourneyStepChild extends Model {
step_id!: number
child_id!: number
data?: Record<string, unknown>
path?: string
priority!: number

static tableName = 'journey_step_child'
Expand Down Expand Up @@ -302,9 +303,11 @@ export class JourneyGate extends JourneyStep {
if (!this.rule) return

const children = state.childrenOf(this.id)
if (!children.length) return
const passed = children.find(c => c.path === 'yes')
const failed = children.find(c => c.path === 'no')

if (!passed && !failed) return

const [passed, failed] = children
const events = await state.events()

const params = {
Expand Down Expand Up @@ -561,6 +564,7 @@ interface JourneyStepMapItem {
y: number
children?: Array<{
external_id: string
path?: string
data?: Record<string, unknown>
}>
}
Expand All @@ -586,12 +590,13 @@ export async function toJourneyStepMap(steps: JourneyStep[], children: JourneySt
data_key: step.data_key,
x: step.x ?? 0,
y: step.y ?? 0,
children: children.reduce<JourneyStepMap[string]['children']>((a, { step_id, child_id, data }) => {
children: children.reduce<JourneyStepMap[string]['children']>((a, { step_id, child_id, path, data }) => {
if (step_id === step.id) {
const child = steps.find(s => s.id === child_id)
if (child) {
a!.push({
external_id: child.external_id,
path,
data,
})
}
Expand Down
23 changes: 15 additions & 8 deletions apps/platform/src/journey/__tests__/JourneyService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { UserEvent } from '../../users/UserEvent'
import Journey from '../Journey'
import { setupProject, setupTestJourney } from './helpers'
import { JourneyState, enterJourneysFromEvent } from '../JourneyService'
import { JourneyStep, JourneyUserStep } from '../JourneyStep'
import { JourneyStep, JourneyStepMapParams, JourneyUserStep } from '../JourneyStep'
import { make } from '../../rules/RuleEngine'
import { uuid } from '../../utilities'

Expand Down Expand Up @@ -33,14 +33,21 @@ describe('JourneyService', () => {
}
}

const gate = (rule: RuleTree, childIds: string[]) => {
const gate = (rule: RuleTree, yes?: string, no?: string) => {
const children: JourneyStepMapParams[string]['children'] = []
if (yes) {
children.push({ external_id: yes, path: 'yes' })
}
if (no) {
children.push({ external_id: no, path: 'no' })
}
return {
...baseStep,
type: 'gate',
data: {
rule,
},
children: childIds.map(external_id => ({ external_id })),
children,
}
}

Expand Down Expand Up @@ -186,7 +193,7 @@ describe('JourneyService', () => {
path: 'state',
operator: '=',
value: 'AL',
}, ['g2', 'd1']),
}, 'g2', 'd1'),

// match if users favorite guitar brand is Fender
g2: gate({
Expand All @@ -207,7 +214,7 @@ describe('JourneyService', () => {
value: 'Fender',
},
],
}, ['d2', 'd3']),
}, 'd2', 'd3'),

d1: delay(0, ''),
d2: delay(0, ''),
Expand Down Expand Up @@ -307,9 +314,9 @@ describe('JourneyService', () => {

const { steps } = await Journey.create(project.id, 'infinite loop journey', {
e: entrance(0, 'g1'),
g1: gate(rule, ['g2', 'g3']),
g2: gate(rule, ['g1', 'g3']),
g3: gate(rule, []),
g1: gate(rule, 'g2', 'g3'),
g2: gate(rule, 'g1', 'g3'),
g3: gate(rule),
})

const user = await User.insertAndFetch({
Expand Down
7 changes: 3 additions & 4 deletions apps/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ export type JourneyStepParams = Omit<JourneyStep, 'id'>

interface JourneyStepMapChild<E = any> {
external_id: string
path?: string
data?: E
}

Expand Down Expand Up @@ -306,10 +307,8 @@ export interface JourneyStepType<T = any, E = any> {
newEdgeData?: () => Promise<E>
Edit?: ComponentType<JourneyStepTypeEditProps<T>>
EditEdge?: ComponentType<JourneyStepTypeEdgeProps<T, E>>
sources?:
| 'single' // single child (default)
| 'multi' // multiple children, one handle (unordered)
| string[] // enumerated handles (ordered)
sources?: string[]
multiChildSources?: boolean
hasDataKey?: boolean
}

Expand Down
40 changes: 16 additions & 24 deletions apps/ui/src/views/journey/JourneyEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function JourneyStepNode({

const validateConnection = useCallback((conn: Connection) => {
if (!type) return false
if (type.sources === 'multi') return true
if (type.multiChildSources) return true
const sourceNode = conn.source && getNode(conn.source)
if (!sourceNode) return true
const existing = getConnectedEdges([sourceNode], getEdges())
Expand Down Expand Up @@ -232,25 +232,25 @@ function JourneyStepNode({
Array.isArray(type.sources)
? type.sources
: ['']
).map((label, index, arr) => {
).map((key, index, arr) => {
const left = (((index + 1) / (arr.length + 1)) * 100) + '%'
return (
<Fragment key={label}>
<Fragment key={key}>
{
label && (
key && (
<span
className="step-handle-label"
style={{
left,
}}
>
{label}
{key}
</span>
)
}
<Handle
type="source"
position={Position.Bottom} id={index + '-s-' + id}
position={Position.Bottom} id={key + '-s-' + id}
isValidConnection={validateConnection}
style={{
left,
Expand Down Expand Up @@ -347,21 +347,19 @@ interface CreateEdgeParams {
sourceId: string
targetId: string
data: any
index: number
stepType: JourneyStepType<any, any> | null
path?: string
}

function createEdge({
data,
index,
sourceId,
stepType,
targetId,
path,
}: CreateEdgeParams): Edge {
return {
id: 'e-' + sourceId + '__' + targetId,
source: sourceId,
sourceHandle: (Array.isArray(stepType?.sources) ? index : 0) + '-s-' + sourceId,
sourceHandle: (path ?? '') + '-s-' + sourceId,
target: targetId,
targetHandle: 't-' + targetId,
data,
Expand Down Expand Up @@ -395,20 +393,18 @@ function stepsToNodes(stepMap: JourneyStepMap) {
stepId,
},
})
const stepType = getStepType(type)
children?.forEach(({ external_id, data }, index) => edges.push(createEdge({
children?.forEach(({ external_id, path, data }) => edges.push(createEdge({
sourceId: id,
targetId: external_id,
index,
data,
stepType,
path,
})))
}

return { nodes, edges }
}

const getSourceIndex = (handleId: string) => parseInt(handleId.substring(0, handleId.indexOf('-s-')), 10)
const getSourcePath = (handleId: string) => handleId.substring(0, handleId.indexOf('-s-'))

function nodesToSteps(nodes: Node[], edges: Edge[]) {
return nodes.reduce<JourneyStepMap>((a, {
Expand All @@ -433,9 +429,9 @@ function nodesToSteps(nodes: Node[], edges: Edge[]) {
y,
children: edges
.filter(e => e.source === id)
.sort((x, y) => getSourceIndex(x.sourceHandle!) - getSourceIndex(y.sourceHandle!))
.map(({ data = {}, target }) => ({
.map(({ data = {}, sourceHandle, target }) => ({
external_id: target,
path: getSourcePath(sourceHandle!),
data,
})),
}
Expand Down Expand Up @@ -464,15 +460,11 @@ function cloneNodes(edges: Edge[], targets: Node[]) {
}
const edgeCopies = getConnectedEdges(targets, edges)
.filter(edge => edge.source in mapping && edge.target in mapping)
.map((edge, index) => createEdge({
.map((edge) => createEdge({
sourceId: mapping[edge.source],
targetId: mapping[edge.target],
index,
data: edge.data ?? {},
stepType: targets
.filter(n => n.id === edge.source)
.map(n => getStepType(n.data.type))
.at(0)!,
path: getSourcePath(edge.sourceHandle!),
}))
return { nodeCopies, edgeCopies }
}
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/views/journey/steps/Balancer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@ export const balancerStep: JourneyStepType<BalancerStepChildConfig> = {
/>
</div>
),
sources: 'multi',
multiChildSources: true,
}
2 changes: 1 addition & 1 deletion apps/ui/src/views/journey/steps/Experiment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ export const experimentStep: JourneyStepType<{}, ExperimentStepChildConfig> = {
/>
)
},
sources: 'multi',
multiChildSources: true,
}
2 changes: 1 addition & 1 deletion apps/ui/src/views/journey/steps/Gate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ export const gateStep: JourneyStepType<GateConfig> = {
/>
)
},
sources: ['Yes', 'No'],
sources: ['yes', 'no'],
}

0 comments on commit 8b81756

Please sign in to comment.