Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor(core): allow tour system to be shared more easily across apps #212

Merged
merged 6 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-mark
import { PicsaCommonComponentsService } from '@picsa/components/src';
import { APP_VERSION, ENVIRONMENT } from '@picsa/environments';
import { MonitoringToolService } from '@picsa/monitoring/src/app/services/monitoring-tool.service';
import { TourService } from '@picsa/shared/services/core/tour.service';
import { TourService } from '@picsa/shared/services/core/tour';
import { CommunicationService } from '@picsa/shared/services/promptToHomePageService.service';
import { Subscription } from 'rxjs';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ITourStep } from '@picsa/shared/services/core/tour.service';
import type { ITourStep } from '@picsa/shared/services/core/tour';

export const HOME_TOUR: ITourStep[] = [
{
Expand Down
104 changes: 104 additions & 0 deletions apps/picsa-tools/budget-tool/src/app/data/tour.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker';
import type { ITourStep } from '@picsa/shared/services/core/tour';
import { _wait } from '@picsa/utils';

/**
* Example tour to select a site from list
* Includes route listeners to automatically trigger table tour once table loaded
*/
export const BUDGET_CREATE_TOUR: ITourStep[] = [
{
text: 'Welcome to the budget tool tour. We will first show the main features and then create a new tour',
},
{
id: 'create',
text: 'New budgets ',

tourOptions: {
showBullets: false,
showButtons: false,
},
// Resume the tour once the user has navigated to a station
routeEvents: {
handler: ({ queryParams }, service) => {
if (queryParams.stationId) {
_wait(500).then(() => {
service.startTour(BUDGET_TABLE_TOUR);
});
return true;
}
return false;
},
},
},
];

/**
* Example tour to interact with crop probability table
* Steps are independent of station select tour to make it easier to handle tables that
* will be loaded dynamically
*/
export const BUDGET_TABLE_TOUR: ITourStep[] = [
{
customElement: {
selector: 'section.table-container',
},
text: translateMarker(
'In the crop information table, you will be able to see the probabilities for different crops through the different seasons.'
),
},

{
id: 'season-start',
text: translateMarker(
'Crop probabilities depend on when the season starts.\nHere you can see the probabilities of the season starting at different dates'
),
},
{
customElement: {
selector: 'tr[mat-header-row]:last-of-type',
},
text: translateMarker(
'Each row contains information about crop, variety, days to maturity and water requirement. Probabilities of receiving requirements are shown for different planting dates'
),
},
{
customElement: {
autoScroll: false,
selector: 'tbody>tr>td:nth-of-type(2)',
},
text: translateMarker('Here we can see information for a specific crop variety'),
},
{
customElement: {
autoScroll: false,
selector: 'tbody>tr>td:nth-of-type(3)',
},
text: translateMarker('This is the number of days to maturity for the variety'),
},
{
customElement: {
autoScroll: false,
selector: 'tbody>tr>td:nth-of-type(4)',
},
text: translateMarker('This is water requirement for the variety'),
},
{
customElement: {
autoScroll: false,
selector: 'tbody>tr>td:nth-of-type(5)',
},
text: translateMarker(
'The maturity and water requirements can be used to calculate the chance of satisfying these conditions for a specific planting date'
),
},
{
customElement: {
selector: 'crop-probability-crop-select',
},
text: translateMarker('The crop filter shows more information for specific crops'),
},
{
text: translateMarker('Now you are ready to explore the crop information tool'),
},
];
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="page-content">
<div class="button-grid">
<button mat-stroked-button color="primary" (click)="createClicked()">
<button mat-stroked-button color="primary" (click)="createClicked()" data-tourid="create">
<div>{{ 'Create New Budget' | translate }}</div>
<img src="assets/budget-icons/budget-create.svg" />
</button>
Expand All @@ -11,7 +11,7 @@
</div>

<h2>{{ 'Saved Budgets' | translate }}</h2>
<div *mobxAutorun>
<div *mobxAutorun data-tourid="create">
<budget-list-item
*ngFor="let budget of store.savedBudgets"
[routerLink]="['./view', budget._key]"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component, Input } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TourService } from '@picsa/shared/services/core/tour.service';

import { STATION_CROP_DATA } from '../../data/mock';
import { IStationRouteQueryParams } from '../../models';
Expand All @@ -15,7 +14,7 @@ export class CropProbabilityStationSelectComponent {

@Input() selectedStationId?: string;

constructor(private router: Router, private route: ActivatedRoute, private tourService: TourService) {}
constructor(private router: Router, private route: ActivatedRoute) {}

/** When station changes update route query params so that parent can handle updates */
public handleStationChange(stationId: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker';
import type { ITourStep } from '@picsa/shared/services/core/tour.service';
import type { ITourStep } from '@picsa/shared/services/core/tour';
import { _wait } from '@picsa/utils';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
style="padding: 1em; margin-top: 1em"
data-tour-id="station-select"
></crop-probability-station-select>
<button mat-button class="tour-button" color="primary" (click)="startTour()">
<mat-icon class="tour-icon mat-elevation-z4">question_mark</mat-icon>
<span>{{ 'Demo' | translate }}</span>
</button>
<picsa-tour-button [tourId]="activeStation ? 'cropProbabilityTable' : 'cropProbabilitySelect'"></picsa-tour-button>
</div>
<crop-probability-table [activeStation]="activeStation" *ngIf="activeStation"></crop-probability-table>
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
.tour-button {
picsa-tour-button {
margin-left: auto;
margin-right: 8px;
min-height: 48px;
padding: 4px;
}
.tour-icon {
border: 1px solid var(--color-primary);
border-radius: 50%;
padding: 4px;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { TourService } from '@picsa/shared/services/core/tour.service';
import { TourService } from '@picsa/shared/services/core/tour';
import { Subject, takeUntil } from 'rxjs';

import { STATION_CROP_DATA } from '../../data/mock';
Expand All @@ -21,6 +21,8 @@ export class HomeComponent implements OnInit, OnDestroy {

ngOnInit(): void {
this.subscribeToRouteChanges();
this.tourService.registerTour('cropProbabilityTable', CROP_PROBABILITY_TABLE_TOUR);
this.tourService.registerTour('cropProbabilitySelect', CROP_PROBABILITY_SELECT_TOUR);
}
ngOnDestroy(): void {
this.componentDestroyed$.next(true);
Expand All @@ -42,10 +44,4 @@ export class HomeComponent implements OnInit, OnDestroy {
}
});
}

public startTour() {
// If no site is selected show the select tour, otherwise show the table tour
const targetTour = this.activeStation ? CROP_PROBABILITY_TABLE_TOUR : CROP_PROBABILITY_SELECT_TOUR;
this.tourService.startTour(targetTour);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { Route, RouterModule } from '@angular/router';
import { PicsaTranslateModule } from '@picsa/shared/modules';
import { PicsaTourButton } from '@picsa/shared/services/core/tour';

import { CropProbabilityToolComponentsModule } from '../../components/components.module';
import { HomeComponent } from './home.component';
Expand All @@ -14,7 +15,13 @@ const routes: Route[] = [
];

@NgModule({
imports: [CommonModule, CropProbabilityToolComponentsModule, RouterModule.forChild(routes), PicsaTranslateModule],
imports: [
CommonModule,
CropProbabilityToolComponentsModule,
RouterModule.forChild(routes),
PicsaTranslateModule,
PicsaTourButton,
],
exports: [],
declarations: [HomeComponent],
providers: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ConfigurationService } from '@picsa/configuration/src';
import { IFarmerVideosById, PICSA_FARMER_VIDEO_RESOURCES } from '@picsa/resources/src/app/data/picsa/farmer-videos';
import { IResourceFile } from '@picsa/resources/src/app/schemas';
import { VideoPlayerComponent } from '@picsa/shared/features/video-player/video-player.component';
import { TourService } from '@picsa/shared/services/core/tour.service';
import { TourService } from '@picsa/shared/services/core/tour';
import { jsonNestedProperty } from '@picsa/utils';

import { ACTIVITY_DATA, IActivityEntry } from '../../data';
Expand Down
3 changes: 3 additions & 0 deletions libs/shared/src/services/core/tour/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './tour-button.component';
export * from './tour.service';
export type { ITourStep } from './tour.types';
50 changes: 50 additions & 0 deletions libs/shared/src/services/core/tour/tour-button.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';

import { PicsaTranslateModule } from '../../../modules/translate';
import { TourService } from './tour.service';

/**
* Help button which, when clicked triggers start of tour with id as provided.
* NOTE - tourId must first be registered with tour service to be available
*/
@Component({
selector: 'picsa-tour-button',
template: ` <button mat-button class="tour-button" color="primary" (click)="startTour()">
<mat-icon class="tour-icon mat-elevation-z4">question_mark</mat-icon>
<span>{{ 'Demo' | translate }}</span>
</button>`,
standalone: true,
imports: [CommonModule, MatIconModule, MatButtonModule, PicsaTranslateModule],
styles: [
`
:host {
display: block;
}
.tour-button {
min-height: 48px;
padding: 4px;
}
.tour-icon {
border: 1px solid var(--color-primary);
border-radius: 50%;
padding: 4px;
}
`,
],
})
export class PicsaTourButton implements OnInit {
@Input() tourId: string;
constructor(private service: TourService) {}

ngOnInit() {}

public startTour() {
if (!this.tourId) {
throw new Error(`No tourId provided to component`);
}
this.service.startTourById(this.tourId);
}
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,12 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { _wait } from '@picsa/utils';
import introJs from 'intro.js';
import type { IntroStep } from 'intro.js/src/core/steps';
import type { IntroJs } from 'intro.js/src/intro';
import type { Options } from 'intro.js/src/option';
import { filter, map, merge, skip, Subscription, take } from 'rxjs';

export interface ITourStep extends Partial<IntroStep> {
/** value of target element selector, selected by [attr.data-tour-id] */
id?: string;

/** Text to display in tour step */
text: string;

/** Specific tour options that will only be enabled for step */
tourOptions?: Partial<Options>;

/**
* Provide a custom element selector to use as intro element.
* Supports elements dynamically injected into dom (will wait max 2s for visisble) */
customElement?: {
selector: string;
/** Auto scroll to element (default: true) */
autoScroll?: boolean;
};

/** Add custom handler for click events. Will be triggered once */
clickEvents?: {
/** Element to add click event listener to via querySelectorAll. Default to step target el */
selector?: string;
handler: (service: TourService) => void;
};

/**
* Add custom handler for route events. Triggers on any route param or queryParam changes
* Must return boolean value that indicates whether event handled and subscriptions can be removed
* */
routeEvents?: {
handler: (data: { params: Params; queryParams: Params }, service: TourService) => boolean;
};
}
import type { ITourStep } from './tour.types';

const DEFAULT_OPTIONS: Partial<Options> = {
hidePrev: true,
Expand All @@ -53,6 +19,8 @@ const DEFAULT_OPTIONS: Partial<Options> = {
/** Interact with Intro.JS tours */
@Injectable({ providedIn: 'root' })
export class TourService {
private registeredTours: Record<string, ITourStep[]> = {};

private intro: IntroJs;

/** List of active tour steps as configured on tour start */
Expand Down Expand Up @@ -87,6 +55,11 @@ export class TourService {
this.tourRootElSelector = enabled ? 'mat-tab-body.mat-mdc-tab-body-active' : undefined;
}

/** Register a set of tour steps to allow triggering by id */
public registerTour(id: string, steps: ITourStep[]) {
this.registeredTours[id] = steps;
}

/** Hide tour interface but retain event subscribers that may be used to resume */
public async pauseTour() {
this.tourPaused = true;
Expand All @@ -99,6 +72,14 @@ export class TourService {
await this.intro.nextStep();
}

public async startTourById(id: string) {
const tourSteps = this.registeredTours[id];
if (!tourSteps) {
throw new Error(`[${id}] tour must be registered by use`);
}
this.startTour(tourSteps);
}

public async startTour(tourSteps: ITourStep[], tourOptions: Partial<Options> = {}) {
this.prepareTour(tourSteps, tourOptions);
await this.intro.start();
Expand Down
Loading
Loading