diff --git a/console/app/services/onboarding-orchestrator.js b/console/app/services/onboarding-orchestrator.js index df199f6a..fb9fb237 100644 --- a/console/app/services/onboarding-orchestrator.js +++ b/console/app/services/onboarding-orchestrator.js @@ -12,14 +12,26 @@ export default class OnboardingOrchestratorService extends Service { @tracked history = []; @tracked sessionId = null; - start(flowId = null, opts = {}) { + async start(flowId = null, opts = {}) { const flow = this.onboardingRegistry.getFlow(flowId ?? this.onboardingRegistry.defaultFlow); if (!flow) throw new Error(`Onboarding flow '${flowId}' not found`); + this.flow = flow; this.wrapper = flow.wrapper || null; this.sessionId = opts.sessionId || null; this.history = []; - this.goto(flow.entry); + + // Execute onFlowWillStart hook if defined + if (typeof this.flow.onFlowWillStart === 'function') { + await this.flow.onFlowWillStart(this.flow, this); + } + + await this.goto(flow.entry); + + // Execute onFlowDidStart hook if defined + if (typeof this.flow.onFlowDidStart === 'function') { + await this.flow.onFlowDidStart(this.flow, this); + } } async goto(stepId) { @@ -27,27 +39,43 @@ export default class OnboardingOrchestratorService extends Service { const step = this.flow.steps.find((s) => s.id === stepId); if (!step) throw new Error(`Step '${stepId}' not found`); + // Execute onStepWillChange hook if defined + const previousStep = this.current; + if (typeof this.flow.onStepWillChange === 'function') { + await this.flow.onStepWillChange(step, previousStep, this); + } + + // Guard function - skip step if guard returns false if (typeof step.guard === 'function' && !step.guard(this.onboardingContext)) { return this.next(); } + // beforeEnter lifecycle hook if (typeof step.beforeEnter === 'function') { await step.beforeEnter(this.onboardingContext); } this.current = step; + + // Execute onStepDidChange hook if defined + if (typeof this.flow.onStepDidChange === 'function') { + await this.flow.onStepDidChange(this.current, previousStep, this); + } } async next() { if (!this.flow || !this.current) return; const leaving = this.current; + + // afterLeave lifecycle hook if (typeof leaving.afterLeave === 'function') { await leaving.afterLeave(this.onboardingContext); } if (!this.history.includes(leaving)) this.history.push(leaving); + // Support both string and function for next property let nextId; if (typeof leaving.next === 'function') { nextId = leaving.next(this.onboardingContext); @@ -55,8 +83,20 @@ export default class OnboardingOrchestratorService extends Service { nextId = leaving.next; } + // If no next step, flow is complete if (!nextId) { + // Execute onFlowWillEnd hook if defined + if (typeof this.flow.onFlowWillEnd === 'function') { + await this.flow.onFlowWillEnd(leaving, this); + } + this.current = null; // finished + + // Execute onFlowDidEnd hook if defined + if (typeof this.flow.onFlowDidEnd === 'function') { + await this.flow.onFlowDidEnd(leaving, this); + } + return; } @@ -70,4 +110,31 @@ export default class OnboardingOrchestratorService extends Service { this.history = this.history.slice(0, -1); await this.goto(prev.id); } + + /** + * Get the current path (for flows with multiple paths) + * This is a helper method that can be used by flows to determine the current path + */ + getCurrentPath() { + if (!this.flow || !this.flow.paths) return null; + + // Determine path based on context or current step + for (const [pathId, pathDef] of Object.entries(this.flow.paths)) { + if (pathDef.steps && pathDef.steps.some(s => s.id === this.current?.id)) { + return pathDef; + } + } + + return null; + } + + /** + * Check if a step is in the current path + */ + isStepInPath(stepId) { + const currentPath = this.getCurrentPath(); + if (!currentPath) return true; // If no paths defined, all steps are valid + + return currentPath.steps?.some(s => s.id === stepId) ?? false; + } }