Skip to content

Commit

Permalink
refactor: tour shared components
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismclarke committed Dec 29, 2023
1 parent 66bc218 commit d618582
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 62 deletions.
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,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 @@ -5,6 +5,7 @@ import { PicsaTranslateModule } from '@picsa/shared/modules';

import { CropProbabilityToolComponentsModule } from '../../components/components.module';
import { HomeComponent } from './home.component';
import { PicsaTourButton } from '@picsa/components/src';

const routes: Route[] = [
{
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';
49 changes: 49 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,49 @@
import { Component, Input, OnInit } from '@angular/core';
import { TourService } from './tour.service';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { PicsaTranslateModule } from '@picsa/shared/modules';

/**
* 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,12 @@ 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[]) {
console.log('register tour 1', id, steps);
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 +73,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

0 comments on commit d618582

Please sign in to comment.