From d6185820b63ecce8078006af7be3db5ebe832f33 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 29 Dec 2023 21:01:26 +0000 Subject: [PATCH] refactor: tour shared components --- .../src/app/pages/home/home.page.ts | 2 +- .../src/app/pages/home/home.tour.ts | 2 +- .../budget-tool/src/app/data/tour.ts | 104 ++++++++++++++++++ .../station-select.component.ts | 3 +- .../src/app/data/tour.ts | 2 +- .../src/app/pages/home/home.component.html | 5 +- .../src/app/pages/home/home.component.scss | 9 +- .../src/app/pages/home/home.component.ts | 10 +- .../src/app/pages/home/home.module.ts | 9 +- .../activity-details.component.ts | 2 +- libs/shared/src/services/core/tour/index.ts | 3 + .../core/tour/tour-button.component.ts | 49 +++++++++ .../services/core/{ => tour}/tour.service.ts | 54 +++------ .../src/services/core/tour/tour.types.ts | 40 +++++++ 14 files changed, 232 insertions(+), 62 deletions(-) create mode 100644 apps/picsa-tools/budget-tool/src/app/data/tour.ts create mode 100644 libs/shared/src/services/core/tour/index.ts create mode 100644 libs/shared/src/services/core/tour/tour-button.component.ts rename libs/shared/src/services/core/{ => tour}/tour.service.ts (91%) create mode 100644 libs/shared/src/services/core/tour/tour.types.ts diff --git a/apps/picsa-apps/extension-app/src/app/pages/home/home.page.ts b/apps/picsa-apps/extension-app/src/app/pages/home/home.page.ts index 139ae38a4..84578e264 100644 --- a/apps/picsa-apps/extension-app/src/app/pages/home/home.page.ts +++ b/apps/picsa-apps/extension-app/src/app/pages/home/home.page.ts @@ -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'; diff --git a/apps/picsa-apps/extension-app/src/app/pages/home/home.tour.ts b/apps/picsa-apps/extension-app/src/app/pages/home/home.tour.ts index aef7673dd..f5d7f97de 100644 --- a/apps/picsa-apps/extension-app/src/app/pages/home/home.tour.ts +++ b/apps/picsa-apps/extension-app/src/app/pages/home/home.tour.ts @@ -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[] = [ { diff --git a/apps/picsa-tools/budget-tool/src/app/data/tour.ts b/apps/picsa-tools/budget-tool/src/app/data/tour.ts new file mode 100644 index 000000000..8ef9b4d47 --- /dev/null +++ b/apps/picsa-tools/budget-tool/src/app/data/tour.ts @@ -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'), + }, +]; diff --git a/apps/picsa-tools/crop-probability-tool/src/app/components/station-select/station-select.component.ts b/apps/picsa-tools/crop-probability-tool/src/app/components/station-select/station-select.component.ts index 9d407179a..b6a59dadd 100644 --- a/apps/picsa-tools/crop-probability-tool/src/app/components/station-select/station-select.component.ts +++ b/apps/picsa-tools/crop-probability-tool/src/app/components/station-select/station-select.component.ts @@ -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'; @@ -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) { diff --git a/apps/picsa-tools/crop-probability-tool/src/app/data/tour.ts b/apps/picsa-tools/crop-probability-tool/src/app/data/tour.ts index 75922a25c..2b9d3e7db 100644 --- a/apps/picsa-tools/crop-probability-tool/src/app/data/tour.ts +++ b/apps/picsa-tools/crop-probability-tool/src/app/data/tour.ts @@ -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'; /** diff --git a/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.html b/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.html index 31f8e7450..ee79868a8 100644 --- a/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.html +++ b/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.html @@ -4,9 +4,6 @@ style="padding: 1em; margin-top: 1em" data-tour-id="station-select" > - + diff --git a/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.scss b/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.scss index e0772b459..c58a361ec 100644 --- a/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.scss +++ b/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.scss @@ -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; } diff --git a/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.ts b/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.ts index 03ac58487..e66ad9035 100644 --- a/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.ts +++ b/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.component.ts @@ -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'; @@ -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); @@ -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); - } } diff --git a/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.module.ts b/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.module.ts index 46a3c7c6c..b0436c139 100644 --- a/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.module.ts +++ b/apps/picsa-tools/crop-probability-tool/src/app/pages/home/home.module.ts @@ -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[] = [ { @@ -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: [], diff --git a/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.ts b/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.ts index 88ae80d6b..f8acb2680 100644 --- a/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.ts +++ b/apps/picsa-tools/farmer-activity/src/app/pages/activity-details/activity-details.component.ts @@ -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'; diff --git a/libs/shared/src/services/core/tour/index.ts b/libs/shared/src/services/core/tour/index.ts new file mode 100644 index 000000000..c0aecff72 --- /dev/null +++ b/libs/shared/src/services/core/tour/index.ts @@ -0,0 +1,3 @@ +export * from './tour-button.component'; +export * from './tour.service'; +export type { ITourStep } from './tour.types'; diff --git a/libs/shared/src/services/core/tour/tour-button.component.ts b/libs/shared/src/services/core/tour/tour-button.component.ts new file mode 100644 index 000000000..69345a201 --- /dev/null +++ b/libs/shared/src/services/core/tour/tour-button.component.ts @@ -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: ` `, + 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); + } +} diff --git a/libs/shared/src/services/core/tour.service.ts b/libs/shared/src/services/core/tour/tour.service.ts similarity index 91% rename from libs/shared/src/services/core/tour.service.ts rename to libs/shared/src/services/core/tour/tour.service.ts index 53b3f2e71..d58328632 100644 --- a/libs/shared/src/services/core/tour.service.ts +++ b/libs/shared/src/services/core/tour/tour.service.ts @@ -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 { - /** 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; - - /** - * 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 = { hidePrev: true, @@ -53,6 +19,8 @@ const DEFAULT_OPTIONS: Partial = { /** Interact with Intro.JS tours */ @Injectable({ providedIn: 'root' }) export class TourService { + private registeredTours: Record = {}; + private intro: IntroJs; /** List of active tour steps as configured on tour start */ @@ -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; @@ -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 = {}) { this.prepareTour(tourSteps, tourOptions); await this.intro.start(); diff --git a/libs/shared/src/services/core/tour/tour.types.ts b/libs/shared/src/services/core/tour/tour.types.ts new file mode 100644 index 000000000..7b2dbc26f --- /dev/null +++ b/libs/shared/src/services/core/tour/tour.types.ts @@ -0,0 +1,40 @@ +import type { Params } from '@angular/router'; + +import type { IntroStep } from 'intro.js/src/core/steps'; +import type { Options } from 'intro.js/src/option'; +import type { TourService } from './tour.service'; + +export interface ITourStep extends Partial { + /** 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; + + /** + * 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; + }; +}