Merge pull request #481 from fleetbase/feature/onboarding-wrapper-architecture

feat: Add wrapper component support to onboarding orchestrator
This commit is contained in:
Ron
2025-12-19 15:56:49 +08:00
committed by GitHub
4 changed files with 83 additions and 10 deletions

View File

@@ -1,7 +1,9 @@
<section class="onboarding step-host">
{{#if this.initialized}}
{{#if this.currentComponent}}
{{component this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
{{#if this.orchestrator.wrapper}}
{{component (lazy-engine-component this.orchestrator.wrapper) currentStepComponent=this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
{{else if this.currentComponent}}
{{component (lazy-engine-component this.currentComponent) context=this.context orchestrator=this.orchestrator brand=@brand}}
{{/if}}
{{else}}
<div class="flex items-center justify-center min-h-24">

View File

@@ -7,17 +7,31 @@ export default class OnboardingOrchestratorService extends Service {
@service onboardingContext;
@tracked flow = null;
@tracked wrapper = null;
@tracked current = null;
@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) {
@@ -25,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);
@@ -53,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;
}
@@ -68,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;
}
}

View File

@@ -9,7 +9,7 @@ export default class OnboardingRegistryService extends Service {
this.defaultFlow = flowId;
}
registerFlow(flow) {
registerFlow(flow, options = {}) {
if (!flow || !flow.id || !flow.entry || !Array.isArray(flow.steps)) {
throw new Error('Invalid FlowDef: id, entry, steps are required');
}
@@ -23,6 +23,11 @@ export default class OnboardingRegistryService extends Service {
}
}
this.flows.set(flow.id, flow);
// If specified, set as default flow
if (options.default) {
this.defaultFlow = flow.id;
}
}
getFlow(id) {

View File

@@ -1,6 +1,3 @@
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
{{outlet}}
</div>
<Spacer @height="300px" />
<div class="onboard-route-wrapper">
{{outlet}}
</div>