diff --git a/apps/platform/src/journey/JourneyController.ts b/apps/platform/src/journey/JourneyController.ts index 47c1570f..96e37653 100644 --- a/apps/platform/src/journey/JourneyController.ts +++ b/apps/platform/src/journey/JourneyController.ts @@ -5,8 +5,8 @@ import { searchParamsSchema } from '../core/searchParams' import { JSONSchemaType, validate } from '../core/validate' import { extractQueryParams } from '../utilities' import Journey, { JourneyEntranceTriggerParams, JourneyParams } from './Journey' -import { createJourney, getJourneyStepMap, getJourney, pagedJourneys, setJourneyStepMap, updateJourney, pagedEntrancesByJourney, getEntranceLog, pagedUsersByStep, archiveJourney, deleteJourney, exitUserFromJourney, publishJourney, skipDelayStep } from './JourneyRepository' -import { JourneyStep, JourneyStepMapParams, journeyStepTypes, toJourneyStepMap } from './JourneyStep' +import { createJourney, getJourney, pagedJourneys, setJourneyStepMap, updateJourney, pagedEntrancesByJourney, getEntranceLog, pagedUsersByStep, archiveJourney, deleteJourney, exitUserFromJourney, publishJourney, skipDelayStep, getJourneyStepMapForUI } from './JourneyRepository' +import { JourneyStep, JourneyStepMapParams, journeyStepTypes } from './JourneyStep' import JourneyUserStep from './JourneyUserStep' import { User } from '../users/User' import { RequestError } from '../core/errors' @@ -165,12 +165,13 @@ const journeyStepsParamsSchema: JSONSchemaType = { } router.get('/:journeyId/steps', async ctx => { - ctx.body = await getJourneyStepMap(ctx.state.journey!.id) + console.log('run!') + ctx.body = await getJourneyStepMapForUI(ctx.state.journey!) }) router.put('/:journeyId/steps', async ctx => { - const { steps, children } = await setJourneyStepMap(ctx.state.journey!, validate(journeyStepsParamsSchema, ctx.request.body)) - ctx.body = await toJourneyStepMap(steps, children) + await setJourneyStepMap(ctx.state.journey!, validate(journeyStepsParamsSchema, ctx.request.body)) + ctx.body = await getJourneyStepMapForUI(ctx.state.journey!) }) router.post('/:journeyId/duplicate', async ctx => { diff --git a/apps/platform/src/journey/JourneyRepository.ts b/apps/platform/src/journey/JourneyRepository.ts index 1b0b7c46..4ded29a5 100644 --- a/apps/platform/src/journey/JourneyRepository.ts +++ b/apps/platform/src/journey/JourneyRepository.ts @@ -3,7 +3,7 @@ import { Database } from '../config/database' import { RequestError } from '../core/errors' import { PageParams } from '../core/searchParams' import Journey, { JourneyParams, UpdateJourneyParams } from './Journey' -import { JourneyStep, JourneyEntrance, JourneyStepMap, toJourneyStepMap, JourneyStepChild } from './JourneyStep' +import { JourneyStep, JourneyEntrance, JourneyStepMap, toJourneyStepMap, JourneyStepChild, journeyStepTypes } from './JourneyStep' import JourneyUserStep from './JourneyUserStep' import { createTagSubquery, getTags, setTags } from '../tags/TagService' import { User } from '../users/User' @@ -138,6 +138,17 @@ export const getJourneyStepChildren = async (journeyId: number, db?: Database): ) } +export const getJourneyStepMapForUI = async (journey: Journey) => { + const map = await getJourneyStepMap(journey.id) + for (const key of Object.keys(map)) { + const step = map[key] + const type = journeyStepTypes[step.type] + console.log('hydrate!', step.type) + map[key] = await type.hydrate(step, journey) + } + return map +} + export const getJourneyStepMap = async (journeyId: number) => { const [steps, children] = await Promise.all([ getJourneySteps(journeyId), diff --git a/apps/platform/src/journey/JourneyStep.ts b/apps/platform/src/journey/JourneyStep.ts index c35f8d78..840e257d 100644 --- a/apps/platform/src/journey/JourneyStep.ts +++ b/apps/platform/src/journey/JourneyStep.ts @@ -1,7 +1,7 @@ import { add, addDays, addHours, addMinutes, getDay, isEqual, isFuture, isPast, parse } from 'date-fns' import Model from '../core/Model' import { getCampaign, getCampaignSend, triggerCampaignSend } from '../campaigns/CampaignService' -import { crossTimezoneCopy, random, snakeCase, uuid } from '../utilities' +import { crossTimezoneCopy, pick, random, snakeCase, uuid } from '../utilities' import { Database } from '../config/database' import { compileTemplate } from '../render' import { logger } from '../config/logger' @@ -14,6 +14,7 @@ import { JourneyState } from './JourneyState' import { EventPostJob, UserPatchJob } from '../jobs' import { exitUserFromJourney, getJourneyUserStepByExternalId } from './JourneyRepository' import JourneyUserStep from './JourneyUserStep' +import Journey from './Journey' export class JourneyStepChild extends Model { @@ -64,6 +65,11 @@ export class JourneyStep extends Model { async next(state: JourneyState): Promise { return state.childrenOf(this.id)[0]?.child_id } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static async hydrate(step: JourneyStepMapItem, journey: Journey): Promise { + return Promise.resolve(step) + } } export class JourneyEntrance extends JourneyStep { @@ -132,6 +138,24 @@ export class JourneyEntrance extends JourneyStep { y: 0, }, db) } + + static async hydrate(step: JourneyStep, journey: Journey) { + if (!step.data) return step + if (step.data.trigger !== 'none') return step + + const references = await Journey.all( + qb => qb + .where('journey_steps.type', 'link') + .whereJsonPath('journey_steps.data', '$.target_id', '=', journey.id) + .where('journeys.status', 'live') + .whereNull('journeys.deleted_at') + .whereNull('journeys.parent_id') + .leftJoin('journey_steps', 'journey_steps.journey_id', '=', 'journeys.id') + .select('journeys.name', 'journeys.id'), + ) + step.data.references = references.map(item => pick(item, ['id', 'name'])) + return step + } } export class JourneyExit extends JourneyStep { @@ -598,7 +622,7 @@ export const journeyStepTypes = [ return a }, {}) -interface JourneyStepMapItem { +export interface JourneyStepMapItem { type: string name?: string data?: Record diff --git a/apps/platform/src/journey/ScheduledEntranceJob.ts b/apps/platform/src/journey/ScheduledEntranceJob.ts index 47a64afd..70cecdbf 100644 --- a/apps/platform/src/journey/ScheduledEntranceJob.ts +++ b/apps/platform/src/journey/ScheduledEntranceJob.ts @@ -71,5 +71,4 @@ export default class ScheduledEntranceJob extends Job { await App.main.queue.enqueueBatch(items.map(({ id }) => JourneyProcessJob.from({ entrance_id: id }))) }) } - } diff --git a/apps/ui/public/locales/en.json b/apps/ui/public/locales/en.json index 6cc48b63..4522d98a 100644 --- a/apps/ui/public/locales/en.json +++ b/apps/ui/public/locales/en.json @@ -127,6 +127,7 @@ "entrance_multiple_entries": "Multiple Entries", "entrance_multiple_entries_desc": "Should people enter this journey multiple times?", "entrance_occurs": " occurs", + "entrance_links": "Users can enter this journey by being sent from links in these journeys:", "entrance_simultaneous_entries": "Simultaneous Entries", "entrance_simultaneous_entries_desc": "If enabled, user could join this journey multiple times before finishing previous ones.", "entrance_trigger": "Trigger Entrance", diff --git a/apps/ui/src/ui/Tag.css b/apps/ui/src/ui/Tag.css index 5d08858e..946b88f5 100644 --- a/apps/ui/src/ui/Tag.css +++ b/apps/ui/src/ui/Tag.css @@ -57,4 +57,10 @@ .ui-tag.tiny .icon { width: 12px; height: 12px; +} + +.ui-tag-list { + display: flex; + gap: 10px; + flex-wrap: wrap; } \ No newline at end of file diff --git a/apps/ui/src/views/journey/JourneyEditor.css b/apps/ui/src/views/journey/JourneyEditor.css index d4648ba8..37fa3d00 100644 --- a/apps/ui/src/views/journey/JourneyEditor.css +++ b/apps/ui/src/views/journey/JourneyEditor.css @@ -418,4 +418,14 @@ display: inline-flex; align-items: center; justify-content: center; +} + +.journey-step.entrance { + max-width: 325px; +} + +.journey-step.entrance .references { + display: flex; + gap: 10px; + flex-direction: column; } \ No newline at end of file diff --git a/apps/ui/src/views/journey/steps/Entrance.tsx b/apps/ui/src/views/journey/steps/Entrance.tsx index 12fc7604..ef5af4a8 100644 --- a/apps/ui/src/views/journey/steps/Entrance.tsx +++ b/apps/ui/src/views/journey/steps/Entrance.tsx @@ -16,6 +16,8 @@ import { env } from '../../../config/env' import { useTranslation, Trans } from 'react-i18next' import { createEventRule, isEventWrapper } from '../../users/rules/RuleHelpers' import { ruleDescription } from '../../users/rules/RuleDescriptions' +import { Tag } from '../../../ui' +import { Link } from 'react-router' interface EntranceConfig { trigger: 'none' | 'event' | 'schedule' @@ -29,6 +31,8 @@ interface EntranceConfig { // schedule based list_id?: number schedule?: string + + references?: Array<{ id: number, name: string }> } const triggerOptions = [ @@ -79,6 +83,7 @@ export const entranceStep: JourneyStepType = { rule, list_id, schedule, + references = [], }, }) { const { t } = useTranslation() @@ -138,6 +143,19 @@ export const entranceStep: JourneyStepType = { ) } + if (references.length) { + return
+ {t('entrance_links')} +
+ {references.map(journey => + + {journey.name} + , + )} +
+
+ } + return <>{t('entrance_empty')} }, Edit({ onChange, project: { id: projectId }, journey: { id: journeyId }, stepId, value }) {