From 2fae4b47ea047bc6f856ecb12f983ed0cae24488 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 31 Oct 2024 14:35:54 +0000 Subject: [PATCH 01/12] chore: tidy template page code --- src/app/feature/template/template.page.html | 18 ++++++++---------- src/app/feature/template/template.page.ts | 9 +++++---- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/app/feature/template/template.page.html b/src/app/feature/template/template.page.html index d0695df135..63f2a2e767 100644 --- a/src/app/feature/template/template.page.html +++ b/src/app/feature/template/template.page.html @@ -1,17 +1,15 @@ - -
+ @if (templateName) { + + } @else { +

Select a Template

- {{template.flow_name}} + @for(template of filteredTemplates; track trackByFn) { + {{template.flow_name}} + }
+ } diff --git a/src/app/feature/template/template.page.ts b/src/app/feature/template/template.page.ts index d6cca5f063..1916242b62 100644 --- a/src/app/feature/template/template.page.ts +++ b/src/app/feature/template/template.page.ts @@ -26,14 +26,15 @@ export class TemplatePage implements OnInit, OnDestroy { ngOnInit() { this.templateName = this.route.snapshot.params.templateName; - const allTemplates = this.appDataService.listSheetsByType("template"); - this.allTemplates = allTemplates.sort((a, b) => (a.flow_name > b.flow_name ? 1 : -1)); - this.filteredTemplates = allTemplates; + if (!this.templateName) { + const allTemplates = this.appDataService.listSheetsByType("template"); + this.allTemplates = allTemplates.sort((a, b) => (a.flow_name > b.flow_name ? 1 : -1)); + this.filteredTemplates = allTemplates; + } this.subscribeToAppConfigChanges(); } search() { - this.allTemplates = this.allTemplates; this.filteredTemplates = this.allTemplates.filter( (i) => i.flow_name.toLocaleLowerCase().indexOf(this.filterTerm.toLowerCase()) > -1 ); From 861a1fec5ff694d14380e9dec7ebff3c21b3b7c2 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 31 Oct 2024 17:45:52 +0000 Subject: [PATCH 02/12] feat(wip): add landscape query param to track template metadata property --- src/app/feature/template/template.page.ts | 15 ++++--- .../template/services/template-nav.service.ts | 39 +++++++++++++++++-- .../template/services/template.service.ts | 8 ++++ .../template/template-container.component.ts | 5 +++ 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/app/feature/template/template.page.ts b/src/app/feature/template/template.page.ts index 1916242b62..1f5805f599 100644 --- a/src/app/feature/template/template.page.ts +++ b/src/app/feature/template/template.page.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { Capacitor } from "@capacitor/core"; import { FlowTypes, IAppConfig } from "data-models"; import { Subscription } from "rxjs"; +import { TemplateNavService } from "src/app/shared/components/template/services/template-nav.service"; import { AppConfigService } from "src/app/shared/services/app-config/app-config.service"; import { AppDataService } from "src/app/shared/services/data/app-data.service"; @@ -18,15 +19,19 @@ export class TemplatePage implements OnInit, OnDestroy { filteredTemplates: FlowTypes.FlowTypeBase[] = []; appConfigChanges$: Subscription; shouldEmitScrollEvents: boolean = false; + constructor( private route: ActivatedRoute, private appDataService: AppDataService, - private appConfigService: AppConfigService + private appConfigService: AppConfigService, + private templateNavService: TemplateNavService ) {} - ngOnInit() { + async ngOnInit() { this.templateName = this.route.snapshot.params.templateName; - if (!this.templateName) { + if (this.templateName) { + this.templateNavService.applyQueryParamsForTemplate(this.templateName); + } else { const allTemplates = this.appDataService.listSheetsByType("template"); this.allTemplates = allTemplates.sort((a, b) => (a.flow_name > b.flow_name ? 1 : -1)); this.filteredTemplates = allTemplates; @@ -34,13 +39,13 @@ export class TemplatePage implements OnInit, OnDestroy { this.subscribeToAppConfigChanges(); } - search() { + public search() { this.filteredTemplates = this.allTemplates.filter( (i) => i.flow_name.toLocaleLowerCase().indexOf(this.filterTerm.toLowerCase()) > -1 ); } - trackByFn(index) { + public trackByFn(index) { return index; } diff --git a/src/app/shared/components/template/services/template-nav.service.ts b/src/app/shared/components/template/services/template-nav.service.ts index af2b722444..d3a4b07501 100644 --- a/src/app/shared/components/template/services/template-nav.service.ts +++ b/src/app/shared/components/template/services/template-nav.service.ts @@ -12,6 +12,7 @@ import { } from "../components/layout/popup/popup.component"; import { ITemplateContainerProps } from "../models"; import { TemplateContainerComponent } from "../template-container.component"; +import { TemplateService } from "./template.service"; // Toggle logs used across full service for debugging purposes (there's quite a few and tedious to comment) const SHOW_DEBUG_LOGS = false; @@ -29,7 +30,8 @@ export class TemplateNavService extends SyncServiceBase { private modalCtrl: ModalController, private location: Location, private router: Router, - private route: ActivatedRoute + private route: ActivatedRoute, + private templateService: TemplateService ) { super("TemplateNav"); } @@ -43,6 +45,23 @@ export class TemplateNavService extends SyncServiceBase { [templatename: string]: { modal: HTMLIonModalElement; props: ITemplateContainerProps }; } = {}; + public async applyQueryParamsForTemplate(templateName: string) { + const templateMetadata = await this.templateService.getTemplateMetadata(templateName); + await this.updateQueryParamsFromTemplateMetadata(templateMetadata); + } + public async updateQueryParamsFromTemplateMetadata( + templateMetadata: FlowTypes.Template["parameter_list"] + ) { + const templateMetadataQueryParams: ITemplateMetadataQueryParams = {}; + templateMetadataQueryParams.landscape = templateMetadata?.landscape || null; + this.router.navigate([], { + relativeTo: this.route, + queryParams: templateMetadataQueryParams, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } + public async handleQueryParamChange( params: INavQueryParams, container: TemplateContainerComponent @@ -85,8 +104,6 @@ export class TemplateNavService extends SyncServiceBase { const [templatename, key, value] = action.args; const nav_parent_triggered_by = action._triggeredBy?.name; const queryParams: INavQueryParams = { nav_parent: parentName, nav_parent_triggered_by }; - // handle direct page or template navigation - const navTarget = templatename.startsWith("/") ? [templatename] : ["template", templatename]; // If "dismiss_pop_up" is set to true for the go_to action, dismiss the current popup before navigating away if (key === "dismiss_pop_up" && parseBoolean(value)) { @@ -108,6 +125,17 @@ export class TemplateNavService extends SyncServiceBase { this.dismissPopup(popup_child); } } + + let navTarget: any[]; + // handle direct page navigation + if (templatename.startsWith("/")) { + navTarget = [templatename]; + } + // handle template navigation + else { + navTarget = ["template", templatename]; + this.applyQueryParamsForTemplate(templatename); + } return this.router.navigate(navTarget, { queryParams, queryParamsHandling: "merge", @@ -362,3 +390,8 @@ export interface INavQueryParams { popup_parent?: string; popup_parent_triggered_by?: string; // } + +/** Templates can add additional query params to the url based on authored metadata */ +export interface ITemplateMetadataQueryParams { + landscape?: boolean; +} diff --git a/src/app/shared/components/template/services/template.service.ts b/src/app/shared/components/template/services/template.service.ts index f171fcb275..f9854ab923 100644 --- a/src/app/shared/components/template/services/template.service.ts +++ b/src/app/shared/components/template/services/template.service.ts @@ -165,6 +165,14 @@ export class TemplateService extends SyncServiceBase { } } + public async getTemplateMetadata(templateName: string) { + const template = (await this.appDataService.getSheet( + "template", + templateName + )) as FlowTypes.Template; + return template?.parameter_list; + } + /** * Check if target template contains any conditional overrides. Evaluate condition and override if satisfied. * @param isOverrideTarget indicate if self-referencing override target from override (prevent infinite loop) diff --git a/src/app/shared/components/template/template-container.component.ts b/src/app/shared/components/template/template-container.component.ts index 1fe5170441..3c24dc6fe8 100644 --- a/src/app/shared/components/template/template-container.component.ts +++ b/src/app/shared/components/template/template-container.component.ts @@ -128,6 +128,11 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC * ``` */ public async forceRerender(full = false, shouldProcess = false) { + // ensure query params are applied on rerender, only for top-level templates + if (!this.parent) { + this.templateNavService.updateQueryParamsFromTemplateMetadata(this.template.parameter_list); + } + if (shouldProcess) { if (full) { console.log("[Force Reload]", this.name); From b8f4fc043ccf96b13c835e1f5147897b262c55ad Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 31 Oct 2024 18:02:22 +0000 Subject: [PATCH 03/12] refactor: move template metadata logic to dedicated service --- src/app/feature/template/template.page.ts | 6 +-- .../instance/template-process.service.ts | 5 +++ .../template-metadata.service.spec.ts | 16 ++++++++ .../services/template-metadata.service.ts | 41 +++++++++++++++++++ .../template/services/template-nav.service.ts | 28 ++----------- .../template/template-container.component.ts | 6 ++- 6 files changed, 73 insertions(+), 29 deletions(-) create mode 100644 src/app/shared/components/template/services/template-metadata.service.spec.ts create mode 100644 src/app/shared/components/template/services/template-metadata.service.ts diff --git a/src/app/feature/template/template.page.ts b/src/app/feature/template/template.page.ts index 1f5805f599..847c02afb6 100644 --- a/src/app/feature/template/template.page.ts +++ b/src/app/feature/template/template.page.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { Capacitor } from "@capacitor/core"; import { FlowTypes, IAppConfig } from "data-models"; import { Subscription } from "rxjs"; -import { TemplateNavService } from "src/app/shared/components/template/services/template-nav.service"; +import { TemplateMetadataService } from "src/app/shared/components/template/services/template-metadata.service"; import { AppConfigService } from "src/app/shared/services/app-config/app-config.service"; import { AppDataService } from "src/app/shared/services/data/app-data.service"; @@ -24,13 +24,13 @@ export class TemplatePage implements OnInit, OnDestroy { private route: ActivatedRoute, private appDataService: AppDataService, private appConfigService: AppConfigService, - private templateNavService: TemplateNavService + private templateMetadataService: TemplateMetadataService ) {} async ngOnInit() { this.templateName = this.route.snapshot.params.templateName; if (this.templateName) { - this.templateNavService.applyQueryParamsForTemplate(this.templateName); + this.templateMetadataService.applyQueryParamsForTemplate(this.templateName); } else { const allTemplates = this.appDataService.listSheetsByType("template"); this.allTemplates = allTemplates.sort((a, b) => (a.flow_name > b.flow_name ? 1 : -1)); diff --git a/src/app/shared/components/template/services/instance/template-process.service.ts b/src/app/shared/components/template/services/instance/template-process.service.ts index 568f322696..8bb1aa807a 100644 --- a/src/app/shared/components/template/services/instance/template-process.service.ts +++ b/src/app/shared/components/template/services/instance/template-process.service.ts @@ -7,6 +7,7 @@ import { TemplateContainerComponent } from "../../template-container.component"; import { TemplateNavService } from "../template-nav.service"; import { TemplateService } from "../template.service"; import { CampaignService } from "src/app/feature/campaign/campaign.service"; +import { TemplateMetadataService } from "../template-metadata.service"; /** * The template process service is a slightly hacky wrapper around the template container component so that @@ -30,6 +31,9 @@ export class TemplateProcessService extends SyncServiceBase { private get templateService() { return getGlobalService(this.injector, TemplateService); } + private get templateMetadataService() { + return getGlobalService(this.injector, TemplateMetadataService); + } private get templateNavService() { return getGlobalService(this.injector, TemplateNavService); } @@ -56,6 +60,7 @@ export class TemplateProcessService extends SyncServiceBase { // Create mock template container component this.container = new TemplateContainerComponent( this.templateService, + this.templateMetadataService, this.templateNavService, this.injector ); diff --git a/src/app/shared/components/template/services/template-metadata.service.spec.ts b/src/app/shared/components/template/services/template-metadata.service.spec.ts new file mode 100644 index 0000000000..8673fab604 --- /dev/null +++ b/src/app/shared/components/template/services/template-metadata.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from "@angular/core/testing"; + +import { TemplateMetadataService } from "./template-metadata.service"; + +describe("TemplateMetadataService", () => { + let service: TemplateMetadataService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TemplateMetadataService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/template/services/template-metadata.service.ts b/src/app/shared/components/template/services/template-metadata.service.ts new file mode 100644 index 0000000000..7addf8b21e --- /dev/null +++ b/src/app/shared/components/template/services/template-metadata.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from "@angular/core"; +import { SyncServiceBase } from "src/app/shared/services/syncService.base"; +import { TemplateService } from "./template.service"; +import { FlowTypes } from "src/app/shared/model"; +import { ActivatedRoute, Router } from "@angular/router"; + +/** Some authored template metadata values should be stored in the url via query params */ +export interface ITemplateMetadataQueryParams { + landscape?: boolean; +} + +@Injectable({ + providedIn: "root", +}) +export class TemplateMetadataService extends SyncServiceBase { + route: ActivatedRoute; + + constructor( + private templateService: TemplateService, + private router: Router + ) { + super("TemplateMetadata"); + } + + public async applyQueryParamsForTemplate(templateName: string) { + const templateMetadata = await this.templateService.getTemplateMetadata(templateName); + await this.updateQueryParamsFromTemplateMetadata(templateMetadata); + } + public async updateQueryParamsFromTemplateMetadata( + templateMetadata: FlowTypes.Template["parameter_list"] + ) { + const templateMetadataQueryParams: ITemplateMetadataQueryParams = {}; + templateMetadataQueryParams.landscape = templateMetadata?.landscape || null; + this.router.navigate([], { + relativeTo: this.route, + queryParams: templateMetadataQueryParams, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } +} diff --git a/src/app/shared/components/template/services/template-nav.service.ts b/src/app/shared/components/template/services/template-nav.service.ts index d3a4b07501..556e22dcfe 100644 --- a/src/app/shared/components/template/services/template-nav.service.ts +++ b/src/app/shared/components/template/services/template-nav.service.ts @@ -12,7 +12,7 @@ import { } from "../components/layout/popup/popup.component"; import { ITemplateContainerProps } from "../models"; import { TemplateContainerComponent } from "../template-container.component"; -import { TemplateService } from "./template.service"; +import { TemplateMetadataService } from "./template-metadata.service"; // Toggle logs used across full service for debugging purposes (there's quite a few and tedious to comment) const SHOW_DEBUG_LOGS = false; @@ -31,7 +31,7 @@ export class TemplateNavService extends SyncServiceBase { private location: Location, private router: Router, private route: ActivatedRoute, - private templateService: TemplateService + private templateMetadataService: TemplateMetadataService ) { super("TemplateNav"); } @@ -45,23 +45,6 @@ export class TemplateNavService extends SyncServiceBase { [templatename: string]: { modal: HTMLIonModalElement; props: ITemplateContainerProps }; } = {}; - public async applyQueryParamsForTemplate(templateName: string) { - const templateMetadata = await this.templateService.getTemplateMetadata(templateName); - await this.updateQueryParamsFromTemplateMetadata(templateMetadata); - } - public async updateQueryParamsFromTemplateMetadata( - templateMetadata: FlowTypes.Template["parameter_list"] - ) { - const templateMetadataQueryParams: ITemplateMetadataQueryParams = {}; - templateMetadataQueryParams.landscape = templateMetadata?.landscape || null; - this.router.navigate([], { - relativeTo: this.route, - queryParams: templateMetadataQueryParams, - queryParamsHandling: "merge", - replaceUrl: true, - }); - } - public async handleQueryParamChange( params: INavQueryParams, container: TemplateContainerComponent @@ -134,7 +117,7 @@ export class TemplateNavService extends SyncServiceBase { // handle template navigation else { navTarget = ["template", templatename]; - this.applyQueryParamsForTemplate(templatename); + this.templateMetadataService.applyQueryParamsForTemplate(templatename); } return this.router.navigate(navTarget, { queryParams, @@ -390,8 +373,3 @@ export interface INavQueryParams { popup_parent?: string; popup_parent_triggered_by?: string; // } - -/** Templates can add additional query params to the url based on authored metadata */ -export interface ITemplateMetadataQueryParams { - landscape?: boolean; -} diff --git a/src/app/shared/components/template/template-container.component.ts b/src/app/shared/components/template/template-container.component.ts index 3c24dc6fe8..b783e5d1a6 100644 --- a/src/app/shared/components/template/template-container.component.ts +++ b/src/app/shared/components/template/template-container.component.ts @@ -19,6 +19,7 @@ import { TemplateNavService } from "./services/template-nav.service"; import { TemplateRowService } from "./services/instance/template-row.service"; import { TemplateService } from "./services/template.service"; import { getIonContentScrollTop, setElStyleAnimated, setIonContentScrollTop } from "./utils"; +import { TemplateMetadataService } from "./services/template-metadata.service"; /** Logging Toggle - rewrite default functions to enable or disable inline logs */ let SHOW_DEBUG_LOGS = false; @@ -67,6 +68,7 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC constructor( private templateService: TemplateService, + private templateMetadataService: TemplateMetadataService, private templateNavService: TemplateNavService, private injector: Injector, // Containers created in headless context may not have specific injectors @@ -130,7 +132,9 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC public async forceRerender(full = false, shouldProcess = false) { // ensure query params are applied on rerender, only for top-level templates if (!this.parent) { - this.templateNavService.updateQueryParamsFromTemplateMetadata(this.template.parameter_list); + this.templateMetadataService.updateQueryParamsFromTemplateMetadata( + this.template.parameter_list + ); } if (shouldProcess) { From 89373788a09ba4dfdec9375ee6fdf1da6427f852 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 1 Nov 2024 09:08:06 +0000 Subject: [PATCH 04/12] feat: set screen orientation based on 'landscape' queryParam --- .../screen-orientation.service.ts | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/app/shared/services/screen-orientation/screen-orientation.service.ts b/src/app/shared/services/screen-orientation/screen-orientation.service.ts index f0bcc103f4..aa9bf9be04 100644 --- a/src/app/shared/services/screen-orientation/screen-orientation.service.ts +++ b/src/app/shared/services/screen-orientation/screen-orientation.service.ts @@ -1,20 +1,41 @@ -import { Injectable } from "@angular/core"; +import { effect, Injectable, WritableSignal } from "@angular/core"; import { SyncServiceBase } from "../syncService.base"; import { ScreenOrientation, OrientationLockType } from "@capacitor/screen-orientation"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; +import { ActivatedRoute } from "@angular/router"; +import { Capacitor } from "@capacitor/core"; +import { distinctUntilChanged, filter, map } from "rxjs"; +// Supported orientation types const ORIENTATION_TYPES: OrientationLockType[] = ["portrait", "landscape"]; +type IOrientationType = (typeof ORIENTATION_TYPES)[number]; + @Injectable({ providedIn: "root", }) export class ScreenOrientationService extends SyncServiceBase { - constructor(private templateActionRegistry: TemplateActionRegistry) { + private orientation: WritableSignal; + constructor( + private templateActionRegistry: TemplateActionRegistry, + private route: ActivatedRoute + ) { super("Screen Orientation Service"); + effect(() => { + console.log(`[SCREEN ORIENTATION] - Orientation: ${this.orientation()}`); + this.setOrientation(this.orientation()); + }); this.initialise(); } - initialise() { + async initialise() { + // TODO: also check if any templates actually use screen orientation metadata? + // Or maybe have a toggle to enable "landscape_mode" at deployment config level + if (Capacitor.isNativePlatform()) { + const currentOrientation = await this.getOrientation(); + this.orientation.set(currentOrientation); + this.watchOrientationParam(); + } this.registerTemplateActionHandlers(); } @@ -34,4 +55,22 @@ export class ScreenOrientationService extends SyncServiceBase { private async setOrientation(orientation: OrientationLockType) { return await ScreenOrientation.lock({ orientation }); } + + private async getOrientation() { + return (await ScreenOrientation.orientation()).type; + } + + private watchOrientationParam() { + this.route.queryParamMap + .pipe( + map((params) => + params.get("landscape") === "true" ? "landscape" : ("portrait" as IOrientationType) + ), + distinctUntilChanged(), + filter((targetOrientation) => targetOrientation !== this.orientation()) + ) + .subscribe((targetOrientation: OrientationType) => { + this.orientation.set(targetOrientation); + }); + } } From a0391ea97f1f14b5b233f064742e8bf79a049d89 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Sat, 2 Nov 2024 17:34:56 +0000 Subject: [PATCH 05/12] fix(screen-orientation): fix service init; finish refactoring to async service --- src/app/app.component.ts | 3 +-- .../screen-orientation.service.ts | 17 ++++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1f09d1f52c..0848663fdb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -251,9 +251,8 @@ export class AppComponent { this.feedbackService, this.shareService, this.fileManagerService, - this.screenOrientationService, ], - deferred: [this.analyticsService], + deferred: [this.analyticsService, this.screenOrientationService], implicit: [ this.dbService, this.templateTranslateService, diff --git a/src/app/shared/services/screen-orientation/screen-orientation.service.ts b/src/app/shared/services/screen-orientation/screen-orientation.service.ts index aa9bf9be04..0be2661308 100644 --- a/src/app/shared/services/screen-orientation/screen-orientation.service.ts +++ b/src/app/shared/services/screen-orientation/screen-orientation.service.ts @@ -1,10 +1,10 @@ -import { effect, Injectable, WritableSignal } from "@angular/core"; -import { SyncServiceBase } from "../syncService.base"; +import { effect, Injectable, signal } from "@angular/core"; import { ScreenOrientation, OrientationLockType } from "@capacitor/screen-orientation"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; import { ActivatedRoute } from "@angular/router"; import { Capacitor } from "@capacitor/core"; import { distinctUntilChanged, filter, map } from "rxjs"; +import { AsyncServiceBase } from "../asyncService.base"; // Supported orientation types const ORIENTATION_TYPES: OrientationLockType[] = ["portrait", "landscape"]; @@ -14,29 +14,28 @@ type IOrientationType = (typeof ORIENTATION_TYPES)[number]; @Injectable({ providedIn: "root", }) -export class ScreenOrientationService extends SyncServiceBase { - private orientation: WritableSignal; +export class ScreenOrientationService extends AsyncServiceBase { + private orientation = signal("portrait"); constructor( private templateActionRegistry: TemplateActionRegistry, private route: ActivatedRoute ) { super("Screen Orientation Service"); effect(() => { - console.log(`[SCREEN ORIENTATION] - Orientation: ${this.orientation()}`); this.setOrientation(this.orientation()); }); - this.initialise(); + this.registerInitFunction(this.initialise); } async initialise() { - // TODO: also check if any templates actually use screen orientation metadata? - // Or maybe have a toggle to enable "landscape_mode" at deployment config level + // TODO: expose a property at deployment config level to enable "landscape_mode" to avoid unnecessary checks + // AND/OR: check on init if any templates actually use screen orientation metadata? if (Capacitor.isNativePlatform()) { const currentOrientation = await this.getOrientation(); this.orientation.set(currentOrientation); this.watchOrientationParam(); + this.registerTemplateActionHandlers(); } - this.registerTemplateActionHandlers(); } private registerTemplateActionHandlers() { From e47f9c48b1ae37f20f2de310b41ff6f0bc1b1955 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 5 Nov 2024 15:45:42 +0000 Subject: [PATCH 06/12] refactor: screen orientation logic From 2ad38175cd65e6da552724df1eab5a9dbea08955 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 5 Nov 2024 15:45:48 +0000 Subject: [PATCH 07/12] refactor: screen orientation logic --- src/app/app.component.ts | 6 +- src/app/feature/template/template.page.ts | 10 +-- .../instance/template-process.service.ts | 5 -- .../services/template-metadata.service.ts | 60 ++++++++++++------ .../template/services/template-nav.service.ts | 17 +----- .../template/template-container.component.ts | 9 --- .../screen-orientation.service.ts | 61 ++++++++----------- 7 files changed, 81 insertions(+), 87 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0848663fdb..03c698138b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -43,6 +43,7 @@ import { ShareService } from "./shared/services/share/share.service"; import { LocalStorageService } from "./shared/services/local-storage/local-storage.service"; import { DeploymentService } from "./shared/services/deployment/deployment.service"; import { ScreenOrientationService } from "./shared/services/screen-orientation/screen-orientation.service"; +import { TemplateMetadataService } from "./shared/components/template/services/template-metadata.service"; @Component({ selector: "app-root", @@ -90,6 +91,7 @@ export class AppComponent { private tourService: TourService, private templateService: TemplateService, private templateFieldService: TemplateFieldService, + private templateMetadataService: TemplateMetadataService, private templateProcessService: TemplateProcessService, private appEventService: AppEventService, private campaignService: CampaignService, @@ -251,8 +253,10 @@ export class AppComponent { this.feedbackService, this.shareService, this.fileManagerService, + this.templateMetadataService, + this.screenOrientationService, ], - deferred: [this.analyticsService, this.screenOrientationService], + deferred: [this.analyticsService], implicit: [ this.dbService, this.templateTranslateService, diff --git a/src/app/feature/template/template.page.ts b/src/app/feature/template/template.page.ts index 847c02afb6..ea07eff9e1 100644 --- a/src/app/feature/template/template.page.ts +++ b/src/app/feature/template/template.page.ts @@ -3,7 +3,6 @@ import { ActivatedRoute } from "@angular/router"; import { Capacitor } from "@capacitor/core"; import { FlowTypes, IAppConfig } from "data-models"; import { Subscription } from "rxjs"; -import { TemplateMetadataService } from "src/app/shared/components/template/services/template-metadata.service"; import { AppConfigService } from "src/app/shared/services/app-config/app-config.service"; import { AppDataService } from "src/app/shared/services/data/app-data.service"; @@ -23,15 +22,12 @@ export class TemplatePage implements OnInit, OnDestroy { constructor( private route: ActivatedRoute, private appDataService: AppDataService, - private appConfigService: AppConfigService, - private templateMetadataService: TemplateMetadataService + private appConfigService: AppConfigService ) {} - async ngOnInit() { + ngOnInit() { this.templateName = this.route.snapshot.params.templateName; - if (this.templateName) { - this.templateMetadataService.applyQueryParamsForTemplate(this.templateName); - } else { + if (!this.templateName) { const allTemplates = this.appDataService.listSheetsByType("template"); this.allTemplates = allTemplates.sort((a, b) => (a.flow_name > b.flow_name ? 1 : -1)); this.filteredTemplates = allTemplates; diff --git a/src/app/shared/components/template/services/instance/template-process.service.ts b/src/app/shared/components/template/services/instance/template-process.service.ts index 8bb1aa807a..568f322696 100644 --- a/src/app/shared/components/template/services/instance/template-process.service.ts +++ b/src/app/shared/components/template/services/instance/template-process.service.ts @@ -7,7 +7,6 @@ import { TemplateContainerComponent } from "../../template-container.component"; import { TemplateNavService } from "../template-nav.service"; import { TemplateService } from "../template.service"; import { CampaignService } from "src/app/feature/campaign/campaign.service"; -import { TemplateMetadataService } from "../template-metadata.service"; /** * The template process service is a slightly hacky wrapper around the template container component so that @@ -31,9 +30,6 @@ export class TemplateProcessService extends SyncServiceBase { private get templateService() { return getGlobalService(this.injector, TemplateService); } - private get templateMetadataService() { - return getGlobalService(this.injector, TemplateMetadataService); - } private get templateNavService() { return getGlobalService(this.injector, TemplateNavService); } @@ -60,7 +56,6 @@ export class TemplateProcessService extends SyncServiceBase { // Create mock template container component this.container = new TemplateContainerComponent( this.templateService, - this.templateMetadataService, this.templateNavService, this.injector ); diff --git a/src/app/shared/components/template/services/template-metadata.service.ts b/src/app/shared/components/template/services/template-metadata.service.ts index 7addf8b21e..778e0860d6 100644 --- a/src/app/shared/components/template/services/template-metadata.service.ts +++ b/src/app/shared/components/template/services/template-metadata.service.ts @@ -1,8 +1,10 @@ -import { Injectable } from "@angular/core"; +import { Injectable, signal, WritableSignal } from "@angular/core"; import { SyncServiceBase } from "src/app/shared/services/syncService.base"; import { TemplateService } from "./template.service"; import { FlowTypes } from "src/app/shared/model"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; +import { distinctUntilChanged, filter, map } from "rxjs"; +import { Capacitor } from "@capacitor/core"; /** Some authored template metadata values should be stored in the url via query params */ export interface ITemplateMetadataQueryParams { @@ -13,29 +15,53 @@ export interface ITemplateMetadataQueryParams { providedIn: "root", }) export class TemplateMetadataService extends SyncServiceBase { - route: ActivatedRoute; + public parameterList: WritableSignal = signal({}); + private enabled: boolean; constructor( private templateService: TemplateService, private router: Router ) { super("TemplateMetadata"); + + // Currently the only watched parameter is for screen orientation, + // which is only supported on native platforms + this.enabled = Capacitor.isNativePlatform(); + + this.initialise(); } - public async applyQueryParamsForTemplate(templateName: string) { - const templateMetadata = await this.templateService.getTemplateMetadata(templateName); - await this.updateQueryParamsFromTemplateMetadata(templateMetadata); + private initialise() { + if (this.enabled) { + this.watchRouteForTopLevelTemplate(); + } } - public async updateQueryParamsFromTemplateMetadata( - templateMetadata: FlowTypes.Template["parameter_list"] - ) { - const templateMetadataQueryParams: ITemplateMetadataQueryParams = {}; - templateMetadataQueryParams.landscape = templateMetadata?.landscape || null; - this.router.navigate([], { - relativeTo: this.route, - queryParams: templateMetadataQueryParams, - queryParamsHandling: "merge", - replaceUrl: true, - }); + + private watchRouteForTopLevelTemplate() { + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), // Listen for route changes + map(() => this.router.routerState.root), + map((root) => { + let active = root; + while (active.firstChild) { + active = active.firstChild; + } + return active.snapshot.params["templateName"]; // Access the parameter here + }), + distinctUntilChanged() + ) + .subscribe(async (templateName: string | undefined) => { + console.log("*****templateName*****", templateName); + if (!templateName) return this.parameterList.set({}); + try { + const parameterList = await this.templateService.getTemplateMetadata(templateName); + this.parameterList.set(parameterList || {}); + console.log("this.parameterList", this.parameterList()); + } catch (error) { + console.error("[TEMPLATE METADATA] Failed to fetch template parameter_list", error); + this.parameterList.set({}); + } + }); } } diff --git a/src/app/shared/components/template/services/template-nav.service.ts b/src/app/shared/components/template/services/template-nav.service.ts index 556e22dcfe..af2b722444 100644 --- a/src/app/shared/components/template/services/template-nav.service.ts +++ b/src/app/shared/components/template/services/template-nav.service.ts @@ -12,7 +12,6 @@ import { } from "../components/layout/popup/popup.component"; import { ITemplateContainerProps } from "../models"; import { TemplateContainerComponent } from "../template-container.component"; -import { TemplateMetadataService } from "./template-metadata.service"; // Toggle logs used across full service for debugging purposes (there's quite a few and tedious to comment) const SHOW_DEBUG_LOGS = false; @@ -30,8 +29,7 @@ export class TemplateNavService extends SyncServiceBase { private modalCtrl: ModalController, private location: Location, private router: Router, - private route: ActivatedRoute, - private templateMetadataService: TemplateMetadataService + private route: ActivatedRoute ) { super("TemplateNav"); } @@ -87,6 +85,8 @@ export class TemplateNavService extends SyncServiceBase { const [templatename, key, value] = action.args; const nav_parent_triggered_by = action._triggeredBy?.name; const queryParams: INavQueryParams = { nav_parent: parentName, nav_parent_triggered_by }; + // handle direct page or template navigation + const navTarget = templatename.startsWith("/") ? [templatename] : ["template", templatename]; // If "dismiss_pop_up" is set to true for the go_to action, dismiss the current popup before navigating away if (key === "dismiss_pop_up" && parseBoolean(value)) { @@ -108,17 +108,6 @@ export class TemplateNavService extends SyncServiceBase { this.dismissPopup(popup_child); } } - - let navTarget: any[]; - // handle direct page navigation - if (templatename.startsWith("/")) { - navTarget = [templatename]; - } - // handle template navigation - else { - navTarget = ["template", templatename]; - this.templateMetadataService.applyQueryParamsForTemplate(templatename); - } return this.router.navigate(navTarget, { queryParams, queryParamsHandling: "merge", diff --git a/src/app/shared/components/template/template-container.component.ts b/src/app/shared/components/template/template-container.component.ts index b783e5d1a6..1fe5170441 100644 --- a/src/app/shared/components/template/template-container.component.ts +++ b/src/app/shared/components/template/template-container.component.ts @@ -19,7 +19,6 @@ import { TemplateNavService } from "./services/template-nav.service"; import { TemplateRowService } from "./services/instance/template-row.service"; import { TemplateService } from "./services/template.service"; import { getIonContentScrollTop, setElStyleAnimated, setIonContentScrollTop } from "./utils"; -import { TemplateMetadataService } from "./services/template-metadata.service"; /** Logging Toggle - rewrite default functions to enable or disable inline logs */ let SHOW_DEBUG_LOGS = false; @@ -68,7 +67,6 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC constructor( private templateService: TemplateService, - private templateMetadataService: TemplateMetadataService, private templateNavService: TemplateNavService, private injector: Injector, // Containers created in headless context may not have specific injectors @@ -130,13 +128,6 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC * ``` */ public async forceRerender(full = false, shouldProcess = false) { - // ensure query params are applied on rerender, only for top-level templates - if (!this.parent) { - this.templateMetadataService.updateQueryParamsFromTemplateMetadata( - this.template.parameter_list - ); - } - if (shouldProcess) { if (full) { console.log("[Force Reload]", this.name); diff --git a/src/app/shared/services/screen-orientation/screen-orientation.service.ts b/src/app/shared/services/screen-orientation/screen-orientation.service.ts index 0be2661308..7549f1599a 100644 --- a/src/app/shared/services/screen-orientation/screen-orientation.service.ts +++ b/src/app/shared/services/screen-orientation/screen-orientation.service.ts @@ -1,39 +1,45 @@ -import { effect, Injectable, signal } from "@angular/core"; +import { effect, Injectable } from "@angular/core"; import { ScreenOrientation, OrientationLockType } from "@capacitor/screen-orientation"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; -import { ActivatedRoute } from "@angular/router"; import { Capacitor } from "@capacitor/core"; -import { distinctUntilChanged, filter, map } from "rxjs"; -import { AsyncServiceBase } from "../asyncService.base"; +import { TemplateMetadataService } from "../../components/template/services/template-metadata.service"; +import { SyncServiceBase } from "../syncService.base"; // Supported orientation types -const ORIENTATION_TYPES: OrientationLockType[] = ["portrait", "landscape"]; +const ORIENTATION_TYPES = ["portrait", "landscape"] as const; type IOrientationType = (typeof ORIENTATION_TYPES)[number]; @Injectable({ providedIn: "root", }) -export class ScreenOrientationService extends AsyncServiceBase { - private orientation = signal("portrait"); +export class ScreenOrientationService extends SyncServiceBase { + private enabled: boolean; + constructor( private templateActionRegistry: TemplateActionRegistry, - private route: ActivatedRoute + private templateMetadataService: TemplateMetadataService ) { super("Screen Orientation Service"); - effect(() => { - this.setOrientation(this.orientation()); - }); - this.registerInitFunction(this.initialise); - } - async initialise() { // TODO: expose a property at deployment config level to enable "landscape_mode" to avoid unnecessary checks // AND/OR: check on init if any templates actually use screen orientation metadata? - if (Capacitor.isNativePlatform()) { - const currentOrientation = await this.getOrientation(); - this.orientation.set(currentOrientation); - this.watchOrientationParam(); + this.enabled = Capacitor.isNativePlatform(); + + if (this.enabled) { + effect(() => { + const targetOrientation = + this.templateMetadataService.parameterList().orientation || "portrait"; + if (targetOrientation && ORIENTATION_TYPES.includes(targetOrientation)) { + this.setOrientation(targetOrientation); + } + }); + } + this.initialise(); + } + + async initialise() { + if (this.enabled) { this.registerTemplateActionHandlers(); } } @@ -51,25 +57,12 @@ export class ScreenOrientationService extends AsyncServiceBase { }); } - private async setOrientation(orientation: OrientationLockType) { - return await ScreenOrientation.lock({ orientation }); + public async setOrientation(orientation: IOrientationType) { + console.log(`[SCREEN ORIENTATION] - Setting to ${orientation}`); + return await ScreenOrientation.lock({ orientation: orientation as OrientationLockType }); } private async getOrientation() { return (await ScreenOrientation.orientation()).type; } - - private watchOrientationParam() { - this.route.queryParamMap - .pipe( - map((params) => - params.get("landscape") === "true" ? "landscape" : ("portrait" as IOrientationType) - ), - distinctUntilChanged(), - filter((targetOrientation) => targetOrientation !== this.orientation()) - ) - .subscribe((targetOrientation: OrientationType) => { - this.orientation.set(targetOrientation); - }); - } } From 4fa97542c96607031dfc1fafe97351f23acfef4c Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 6 Nov 2024 11:50:29 +0000 Subject: [PATCH 08/12] chore: code tidy --- .../services/template-metadata.service.ts | 69 +++++++++++-------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/app/shared/components/template/services/template-metadata.service.ts b/src/app/shared/components/template/services/template-metadata.service.ts index 778e0860d6..db84cc655e 100644 --- a/src/app/shared/components/template/services/template-metadata.service.ts +++ b/src/app/shared/components/template/services/template-metadata.service.ts @@ -1,22 +1,22 @@ -import { Injectable, signal, WritableSignal } from "@angular/core"; +import { Injectable, OnDestroy, signal } from "@angular/core"; import { SyncServiceBase } from "src/app/shared/services/syncService.base"; import { TemplateService } from "./template.service"; import { FlowTypes } from "src/app/shared/model"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; -import { distinctUntilChanged, filter, map } from "rxjs"; +import { distinctUntilChanged, filter, map, Subject, takeUntil } from "rxjs"; import { Capacitor } from "@capacitor/core"; -/** Some authored template metadata values should be stored in the url via query params */ -export interface ITemplateMetadataQueryParams { - landscape?: boolean; -} - +/** + * Service responsible for handling metadata of the current top-level template, + * i.e. parameters authored through the template's parameter_list in a contents list + */ @Injectable({ providedIn: "root", }) -export class TemplateMetadataService extends SyncServiceBase { - public parameterList: WritableSignal = signal({}); +export class TemplateMetadataService extends SyncServiceBase implements OnDestroy { + public parameterList = signal({}); private enabled: boolean; + private destroy$ = new Subject(); constructor( private templateService: TemplateService, @@ -26,8 +26,7 @@ export class TemplateMetadataService extends SyncServiceBase { // Currently the only watched parameter is for screen orientation, // which is only supported on native platforms - this.enabled = Capacitor.isNativePlatform(); - + this.enabled = !Capacitor.isNativePlatform(); this.initialise(); } @@ -40,28 +39,38 @@ export class TemplateMetadataService extends SyncServiceBase { private watchRouteForTopLevelTemplate() { this.router.events .pipe( - filter((event) => event instanceof NavigationEnd), // Listen for route changes + filter((event) => event instanceof NavigationEnd), map(() => this.router.routerState.root), - map((root) => { - let active = root; - while (active.firstChild) { - active = active.firstChild; - } - return active.snapshot.params["templateName"]; // Access the parameter here - }), - distinctUntilChanged() + map((root) => this.extractTemplateNameFromRoute(root)), + distinctUntilChanged(), + takeUntil(this.destroy$) ) .subscribe(async (templateName: string | undefined) => { - console.log("*****templateName*****", templateName); - if (!templateName) return this.parameterList.set({}); - try { - const parameterList = await this.templateService.getTemplateMetadata(templateName); - this.parameterList.set(parameterList || {}); - console.log("this.parameterList", this.parameterList()); - } catch (error) { - console.error("[TEMPLATE METADATA] Failed to fetch template parameter_list", error); - this.parameterList.set({}); - } + await this.updateParameterList(templateName); }); } + + private extractTemplateNameFromRoute(root: ActivatedRoute): string | undefined { + let active = root; + while (active.firstChild) { + active = active.firstChild; + } + return active.snapshot.params["templateName"]; + } + + private async updateParameterList(templateName: string | undefined) { + if (!templateName) return this.parameterList.set({}); + try { + const parameterList = await this.templateService.getTemplateMetadata(templateName); + this.parameterList.set(parameterList || {}); + } catch (error) { + console.error("[TEMPLATE METADATA] Failed to fetch template parameter_list", error); + this.parameterList.set({}); + } + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.complete(); + } } From 79380021d6322efbdfda9ac7b57e2705457f7822 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 6 Nov 2024 17:20:51 -0800 Subject: [PATCH 09/12] chore: code tidying --- src/app/feature/template/template.page.html | 2 +- src/app/feature/template/template.page.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/feature/template/template.page.html b/src/app/feature/template/template.page.html index 63f2a2e767..6c88a5fd24 100644 --- a/src/app/feature/template/template.page.html +++ b/src/app/feature/template/template.page.html @@ -6,7 +6,7 @@

Select a Template

- @for(template of filteredTemplates; track trackByFn) { + @for(template of filteredTemplates; track $index) { {{template.flow_name}} } diff --git a/src/app/feature/template/template.page.ts b/src/app/feature/template/template.page.ts index ea07eff9e1..16f3a57bfa 100644 --- a/src/app/feature/template/template.page.ts +++ b/src/app/feature/template/template.page.ts @@ -28,9 +28,7 @@ export class TemplatePage implements OnInit, OnDestroy { ngOnInit() { this.templateName = this.route.snapshot.params.templateName; if (!this.templateName) { - const allTemplates = this.appDataService.listSheetsByType("template"); - this.allTemplates = allTemplates.sort((a, b) => (a.flow_name > b.flow_name ? 1 : -1)); - this.filteredTemplates = allTemplates; + this.listTemplates(); } this.subscribeToAppConfigChanges(); } @@ -41,8 +39,11 @@ export class TemplatePage implements OnInit, OnDestroy { ); } - public trackByFn(index) { - return index; + /** Create a list of all templates to display when no specific template loaded */ + private listTemplates() { + const allTemplates = this.appDataService.listSheetsByType("template"); + this.allTemplates = allTemplates.sort((a, b) => (a.flow_name > b.flow_name ? 1 : -1)); + this.filteredTemplates = allTemplates; } private subscribeToAppConfigChanges() { From 5eefc8c7849a3a2fac3cabbea9ab65457e0e82a4 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 6 Nov 2024 17:21:22 -0800 Subject: [PATCH 10/12] refactor: orientation service --- .../screen-orientation.service.ts | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/app/shared/services/screen-orientation/screen-orientation.service.ts b/src/app/shared/services/screen-orientation/screen-orientation.service.ts index 7549f1599a..65505a3198 100644 --- a/src/app/shared/services/screen-orientation/screen-orientation.service.ts +++ b/src/app/shared/services/screen-orientation/screen-orientation.service.ts @@ -1,20 +1,22 @@ import { effect, Injectable } from "@angular/core"; -import { ScreenOrientation, OrientationLockType } from "@capacitor/screen-orientation"; +import { ScreenOrientation } from "@capacitor/screen-orientation"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; import { Capacitor } from "@capacitor/core"; import { TemplateMetadataService } from "../../components/template/services/template-metadata.service"; import { SyncServiceBase } from "../syncService.base"; +import { environment } from "src/environments/environment"; -// Supported orientation types -const ORIENTATION_TYPES = ["portrait", "landscape"] as const; +/** List of possible orientations provided by authors */ +const SCREEN_ORIENTATIONS = ["portrait", "landscape"] as const; -type IOrientationType = (typeof ORIENTATION_TYPES)[number]; +type IScreenOrientation = (typeof SCREEN_ORIENTATIONS)[number]; @Injectable({ providedIn: "root", }) export class ScreenOrientationService extends SyncServiceBase { - private enabled: boolean; + /** Actively locked screen orientation */ + private lockedOrientation: IScreenOrientation | undefined; constructor( private templateActionRegistry: TemplateActionRegistry, @@ -24,45 +26,44 @@ export class ScreenOrientationService extends SyncServiceBase { // TODO: expose a property at deployment config level to enable "landscape_mode" to avoid unnecessary checks // AND/OR: check on init if any templates actually use screen orientation metadata? - this.enabled = Capacitor.isNativePlatform(); + const isEnabled = Capacitor.isNativePlatform() || !environment.production; - if (this.enabled) { + if (isEnabled) { + // Add handlers to set orientation on action + this.registerTemplateActionHandlers(); + // Set orientation when template parameter orientation changes effect(() => { - const targetOrientation = - this.templateMetadataService.parameterList().orientation || "portrait"; - if (targetOrientation && ORIENTATION_TYPES.includes(targetOrientation)) { - this.setOrientation(targetOrientation); - } + const targetOrientation = this.templateMetadataService.parameterList().orientation; + this.setOrientation(targetOrientation); }); } - this.initialise(); - } - - async initialise() { - if (this.enabled) { - this.registerTemplateActionHandlers(); - } } private registerTemplateActionHandlers() { this.templateActionRegistry.register({ screen_orientation: async ({ args }) => { const [targetOrientation] = args; - if (ORIENTATION_TYPES.includes(targetOrientation)) { - this.setOrientation(targetOrientation); - } else { - console.error(`[SCREEN ORIENTATION] - Invalid orientation: ${targetOrientation}`); - } + this.setOrientation(targetOrientation); }, }); } - public async setOrientation(orientation: IOrientationType) { - console.log(`[SCREEN ORIENTATION] - Setting to ${orientation}`); - return await ScreenOrientation.lock({ orientation: orientation as OrientationLockType }); - } + private async setOrientation(orientation: IScreenOrientation) { + // avoid re-locking same orientation + if (orientation === this.lockedOrientation) return; - private async getOrientation() { - return (await ScreenOrientation.orientation()).type; + this.lockedOrientation = orientation; + + if (orientation) { + if (SCREEN_ORIENTATIONS.includes(orientation)) { + console.log(`[SCREEN ORIENTATION] - Lock ${orientation}`); + return ScreenOrientation.lock({ orientation }); + } else { + console.error(`[SCREEN ORIENTATION] - Invalid orientation: ${orientation}`); + } + } else { + console.log(`[SCREEN ORIENTATION] - Unlock`); + return ScreenOrientation.unlock(); + } } } From 9dcba43cd0bf90d94420129076dac3d5319df97e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 6 Nov 2024 17:55:52 -0800 Subject: [PATCH 11/12] refactor: template metadata service --- packages/data-models/flowTypes.ts | 1 - .../services/template-metadata.service.ts | 81 ++++++------------- .../template/services/template.service.ts | 4 +- .../screen-orientation.service.ts | 6 +- src/app/shared/utils/angular.utils.ts | 37 +++++++++ 5 files changed, 67 insertions(+), 62 deletions(-) create mode 100644 src/app/shared/utils/angular.utils.ts diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 3999bc9f28..51f8f6fabf 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -1,7 +1,6 @@ /* eslint @typescript-eslint/sort-type-constituents: "warn" */ import type { IDataPipeOperation } from "shared"; -import type { IAppConfig } from "./appConfig"; import type { IAssetEntry } from "./assets.model"; /********************************************************************************************* diff --git a/src/app/shared/components/template/services/template-metadata.service.ts b/src/app/shared/components/template/services/template-metadata.service.ts index db84cc655e..80d450cfe3 100644 --- a/src/app/shared/components/template/services/template-metadata.service.ts +++ b/src/app/shared/components/template/services/template-metadata.service.ts @@ -1,10 +1,11 @@ -import { Injectable, OnDestroy, signal } from "@angular/core"; +import { computed, effect, Injectable, signal } from "@angular/core"; import { SyncServiceBase } from "src/app/shared/services/syncService.base"; import { TemplateService } from "./template.service"; import { FlowTypes } from "src/app/shared/model"; -import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; -import { distinctUntilChanged, filter, map, Subject, takeUntil } from "rxjs"; -import { Capacitor } from "@capacitor/core"; +import { Router } from "@angular/router"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { ngRouterMergedSnapshot$ } from "src/app/shared/utils/angular.utils"; +import { isEqual } from "packages/shared/src/utils/object-utils"; /** * Service responsible for handling metadata of the current top-level template, @@ -13,10 +14,15 @@ import { Capacitor } from "@capacitor/core"; @Injectable({ providedIn: "root", }) -export class TemplateMetadataService extends SyncServiceBase implements OnDestroy { - public parameterList = signal({}); - private enabled: boolean; - private destroy$ = new Subject(); +export class TemplateMetadataService extends SyncServiceBase { + /** Utility snapshot used to get router snapshot from service (outside render context) */ + private snapshot = toSignal(ngRouterMergedSnapshot$(this.router)); + + /** Name of current template provide by route param */ + private templateName = computed(() => this.snapshot().params.templateName); + + /** List of parameterList provided with current template */ + public parameterList = signal({}, { equal: isEqual }); constructor( private templateService: TemplateService, @@ -24,53 +30,16 @@ export class TemplateMetadataService extends SyncServiceBase implements OnDestro ) { super("TemplateMetadata"); - // Currently the only watched parameter is for screen orientation, - // which is only supported on native platforms - this.enabled = !Capacitor.isNativePlatform(); - this.initialise(); - } - - private initialise() { - if (this.enabled) { - this.watchRouteForTopLevelTemplate(); - } - } - - private watchRouteForTopLevelTemplate() { - this.router.events - .pipe( - filter((event) => event instanceof NavigationEnd), - map(() => this.router.routerState.root), - map((root) => this.extractTemplateNameFromRoute(root)), - distinctUntilChanged(), - takeUntil(this.destroy$) - ) - .subscribe(async (templateName: string | undefined) => { - await this.updateParameterList(templateName); - }); - } - - private extractTemplateNameFromRoute(root: ActivatedRoute): string | undefined { - let active = root; - while (active.firstChild) { - active = active.firstChild; - } - return active.snapshot.params["templateName"]; - } - - private async updateParameterList(templateName: string | undefined) { - if (!templateName) return this.parameterList.set({}); - try { - const parameterList = await this.templateService.getTemplateMetadata(templateName); - this.parameterList.set(parameterList || {}); - } catch (error) { - console.error("[TEMPLATE METADATA] Failed to fetch template parameter_list", error); - this.parameterList.set({}); - } - } - - ngOnDestroy() { - this.destroy$.next(true); - this.destroy$.complete(); + // subscribe to template name changes and load corresponding template parameter list on change + effect( + async () => { + const templateName = this.templateName(); + const parameterList = templateName + ? await this.templateService.getTemplateMetadata(templateName) + : {}; + this.parameterList.set(parameterList); + }, + { allowSignalWrites: true } + ); } } diff --git a/src/app/shared/components/template/services/template.service.ts b/src/app/shared/components/template/services/template.service.ts index f9854ab923..22b55f7dcb 100644 --- a/src/app/shared/components/template/services/template.service.ts +++ b/src/app/shared/components/template/services/template.service.ts @@ -166,11 +166,11 @@ export class TemplateService extends SyncServiceBase { } public async getTemplateMetadata(templateName: string) { - const template = (await this.appDataService.getSheet( + const template = (await this.appDataService.getSheet( "template", templateName )) as FlowTypes.Template; - return template?.parameter_list; + return template?.parameter_list || {}; } /** diff --git a/src/app/shared/services/screen-orientation/screen-orientation.service.ts b/src/app/shared/services/screen-orientation/screen-orientation.service.ts index 65505a3198..5c7828ce50 100644 --- a/src/app/shared/services/screen-orientation/screen-orientation.service.ts +++ b/src/app/shared/services/screen-orientation/screen-orientation.service.ts @@ -32,9 +32,9 @@ export class ScreenOrientationService extends SyncServiceBase { // Add handlers to set orientation on action this.registerTemplateActionHandlers(); // Set orientation when template parameter orientation changes - effect(() => { - const targetOrientation = this.templateMetadataService.parameterList().orientation; - this.setOrientation(targetOrientation); + effect(async () => { + const { orientation } = this.templateMetadataService.parameterList(); + this.setOrientation(orientation); }); } } diff --git a/src/app/shared/utils/angular.utils.ts b/src/app/shared/utils/angular.utils.ts new file mode 100644 index 0000000000..9746020e05 --- /dev/null +++ b/src/app/shared/utils/angular.utils.ts @@ -0,0 +1,37 @@ +import { NavigationEnd } from "@angular/router"; +import type { ActivatedRoute, ActivatedRouteSnapshot, Router } from "@angular/router"; +import { filter, map, startWith } from "rxjs"; + +/** + * When accessing ActivatedRoute from a provider router hierarchy includes all routers, not just + * current view router (as identified when using from within a component) + * + * Workaround to check all nested routers for params and combined. Adapted from: + * https://medium.com/simars/ngrx-router-store-reduce-select-route-params-6baff607dd9 + */ +function mergeRouterSnapshots(router: Router) { + const merged: Partial = { data: {}, params: {}, queryParams: {} }; + let route: ActivatedRoute | undefined = router.routerState.root; + while (route !== undefined) { + const { data, params, queryParams } = route.snapshot; + merged.data = { ...merged.data, ...data }; + merged.params = { ...merged.params, ...params }; + merged.queryParams = { ...merged.queryParams, ...queryParams }; + route = route.children.find((child) => child.outlet === "primary"); + } + return merged as ActivatedRouteSnapshot; +} + +/** + * Subscribe to snapshot across all active routers + * This may be useful in cases where a service wants to subscribe to route parameter changes + * (default behaviour would only detect changes to top-most route) + * Adapted from https://github.com/angular/angular/issues/46891#issuecomment-1190590046 + */ +export function ngRouterMergedSnapshot$(router: Router) { + return router.events.pipe( + filter((e) => e instanceof NavigationEnd), + map(() => mergeRouterSnapshots(router)), + startWith(mergeRouterSnapshots(router)) + ); +} From 72e5314e531f1638bed5837dad972d0fb145f9a8 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 7 Nov 2024 12:27:02 +0000 Subject: [PATCH 12/12] feat: add 'screen_orientation: unlock' action --- .../services/screen-orientation/screen-orientation.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/services/screen-orientation/screen-orientation.service.ts b/src/app/shared/services/screen-orientation/screen-orientation.service.ts index 5c7828ce50..eb399ebb47 100644 --- a/src/app/shared/services/screen-orientation/screen-orientation.service.ts +++ b/src/app/shared/services/screen-orientation/screen-orientation.service.ts @@ -7,7 +7,7 @@ import { SyncServiceBase } from "../syncService.base"; import { environment } from "src/environments/environment"; /** List of possible orientations provided by authors */ -const SCREEN_ORIENTATIONS = ["portrait", "landscape"] as const; +const SCREEN_ORIENTATIONS = ["portrait", "landscape", "unlock"] as const; type IScreenOrientation = (typeof SCREEN_ORIENTATIONS)[number]; @@ -54,7 +54,7 @@ export class ScreenOrientationService extends SyncServiceBase { this.lockedOrientation = orientation; - if (orientation) { + if (orientation && orientation !== "unlock") { if (SCREEN_ORIENTATIONS.includes(orientation)) { console.log(`[SCREEN ORIENTATION] - Lock ${orientation}`); return ScreenOrientation.lock({ orientation });