From 24c4caa0a8fcd8fb0ba105d27504379000d92e7f Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 28 Oct 2024 11:28:59 +0000 Subject: [PATCH 01/45] feat(wip): youtube component --- packages/data-models/flowTypes.ts | 3 +- .../components/template/components/index.ts | 7 +-- .../components/youtube/youtube.component.html | 9 ++++ .../components/youtube/youtube.component.scss | 8 ++++ .../youtube/youtube.component.spec.ts | 24 ++++++++++ .../components/youtube/youtube.component.ts | 47 +++++++++++++++++++ 6 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/components/template/components/youtube/youtube.component.html create mode 100644 src/app/shared/components/template/components/youtube/youtube.component.scss create mode 100644 src/app/shared/components/template/components/youtube/youtube.component.spec.ts create mode 100644 src/app/shared/components/template/components/youtube/youtube.component.ts diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 5a08d1440..cd38ba4c7 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -316,7 +316,8 @@ export namespace FlowTypes { | "toggle_bar" | "update_action_list" | "video" - | "workshops_accordion"; + | "workshops_accordion" + | "youtube"; export interface TemplateRow extends Row_with_translations { type: TemplateRowType; diff --git a/src/app/shared/components/template/components/index.ts b/src/app/shared/components/template/components/index.ts index 2594b39dc..929e644ec 100644 --- a/src/app/shared/components/template/components/index.ts +++ b/src/app/shared/components/template/components/index.ts @@ -20,7 +20,6 @@ import { TemplateBaseComponent } from "./base"; import { TemplateDebuggerComponent } from "./debugger"; import { TemplateHTMLComponent } from "./html/html.component"; import { TemplatePopupComponent } from "./layout/popup/popup.component"; - import { TmplAccordionComponent } from "./accordion/accordion.component"; import { TmplAdvancedDashedBoxComponent } from "./layout/advanced-dashed-box/advanced-dashed-box.component"; import { TmplAnimatedSlidesComponent } from "./animated-slides/animated-slides.component"; @@ -56,14 +55,15 @@ import { TmplTaskProgressBarComponent } from "./task-progress-bar/task-progress- import { TmplTextAreaComponent } from "./text-area/text-area.component"; import { TmplTextBoxComponent } from "./text-box/text-box.component"; import { TmplTextComponent } from "./text/text.component"; +import { TmplTextBubbleComponent } from "./text-bubble/text-bubble.component"; import { TmplTileComponent } from "./tile-component/tile-component.component"; import { TmplTitleComponent } from "./title"; import { TmplTimerComponent } from "./timer/timer.component"; import { TmplToggleBarComponent } from "./toggle-bar/toggle-bar"; import { TmplVideoComponent } from "./video"; - import { WorkshopsComponent } from "./layout/workshops_accordion"; -import { TmplTextBubbleComponent } from "./text-bubble/text-bubble.component"; +import { YoutubeComponent } from "./youtube/youtube.component"; + import { DEMO_COMPONENT_MAPPING } from "packages/components/demo"; /** All components should be exported as a single array for easy module import */ @@ -194,6 +194,7 @@ const CORE_COMPONENT_MAPPING: Record + + @if (params.allowFullScreen) { + + } @else { + + } + diff --git a/src/app/shared/components/template/components/youtube/youtube.component.scss b/src/app/shared/components/template/components/youtube/youtube.component.scss new file mode 100644 index 000000000..ac820947a --- /dev/null +++ b/src/app/shared/components/template/components/youtube/youtube.component.scss @@ -0,0 +1,8 @@ +.youtube-container { + width: 100%; + aspect-ratio: 16 / 9; // see https://caniuse.com/mdn-css_properties_aspect-ratio + iframe { + width: 100%; + height: 100%; + } +} diff --git a/src/app/shared/components/template/components/youtube/youtube.component.spec.ts b/src/app/shared/components/template/components/youtube/youtube.component.spec.ts new file mode 100644 index 000000000..c1b73443d --- /dev/null +++ b/src/app/shared/components/template/components/youtube/youtube.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { IonicModule } from "@ionic/angular"; + +import { YoutubeComponent } from "./youtube.component"; + +describe("YoutubeComponent", () => { + let component: YoutubeComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [YoutubeComponent], + imports: [IonicModule.forRoot()], + }).compileComponents(); + + fixture = TestBed.createComponent(YoutubeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/template/components/youtube/youtube.component.ts b/src/app/shared/components/template/components/youtube/youtube.component.ts new file mode 100644 index 000000000..90285ab60 --- /dev/null +++ b/src/app/shared/components/template/components/youtube/youtube.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from "@angular/core"; +import { TemplateBaseComponent } from "../base"; +import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; +import { + getBooleanParamFromTemplateRow, + getStringParamFromTemplateRow, +} from "src/app/shared/utils"; + +interface IYoutubeParams { + /** TEMPLATE PARAMETER: video_id */ + videoId: string; + /** TEMPLATE PARAMETER: allow_full_screen. Default true */ + allowFullScreen?: boolean; +} + +@Component({ + selector: "youtube", + templateUrl: "./youtube.component.html", + styleUrls: ["./youtube.component.scss"], +}) +export class YoutubeComponent extends TemplateBaseComponent implements OnInit { + params: Partial = {}; + src: SafeResourceUrl; + + constructor(private domSanitizer: DomSanitizer) { + super(); + } + + ngOnInit() { + this.getParams(); + } + + getParams() { + this.params.videoId = + this._row.value || getStringParamFromTemplateRow(this._row, "video_id", ""); + // The `?rel=0` param prevents showing related videos after the original video finishes + // see https://developers.google.com/youtube/player_parameters + this.src = this.domSanitizer.bypassSecurityTrustResourceUrl( + `https://www.youtube.com/embed/${this.params.videoId}?rel=0` + ); + this.params.allowFullScreen = getBooleanParamFromTemplateRow( + this._row, + "allow_fullscreen", + true + ); + } +} From e38953724910e24422e2d017ce936b29f87a562c Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 28 Oct 2024 17:19:32 +0000 Subject: [PATCH 02/45] chore: add custom query params to youtube embed url --- .../components/youtube/youtube.component.ts | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/app/shared/components/template/components/youtube/youtube.component.ts b/src/app/shared/components/template/components/youtube/youtube.component.ts index 90285ab60..61204a556 100644 --- a/src/app/shared/components/template/components/youtube/youtube.component.ts +++ b/src/app/shared/components/template/components/youtube/youtube.component.ts @@ -5,6 +5,7 @@ import { getBooleanParamFromTemplateRow, getStringParamFromTemplateRow, } from "src/app/shared/utils"; +import { TemplateTranslateService } from "../../services/template-translate.service"; interface IYoutubeParams { /** TEMPLATE PARAMETER: video_id */ @@ -22,26 +23,53 @@ export class YoutubeComponent extends TemplateBaseComponent implements OnInit { params: Partial = {}; src: SafeResourceUrl; - constructor(private domSanitizer: DomSanitizer) { + constructor( + private domSanitizer: DomSanitizer, + private templateTranslateService: TemplateTranslateService + ) { super(); } ngOnInit() { this.getParams(); + this.getYoutubeSrc(); } - getParams() { + private getParams() { this.params.videoId = this._row.value || getStringParamFromTemplateRow(this._row, "video_id", ""); - // The `?rel=0` param prevents showing related videos after the original video finishes - // see https://developers.google.com/youtube/player_parameters - this.src = this.domSanitizer.bypassSecurityTrustResourceUrl( - `https://www.youtube.com/embed/${this.params.videoId}?rel=0` - ); this.params.allowFullScreen = getBooleanParamFromTemplateRow( this._row, "allow_fullscreen", true ); } + + private getYoutubeSrc() { + const baseYoutubeUrl = `https://www.youtube.com/embed/${this.params.videoId}`; + + // See https://developers.google.com/youtube/player_parameters + const youtubeQueryParams = { + // Favour white over red for more theme compatibility + color: "white", + // hide the fullscreen button if allow_fullscreen is false + fs: this.params.allowFullScreen ? "1" : "0", + // Attempt to set the player's interface language to match the app language + hl: this.templateTranslateService.app_language$.value, + // Disable related videos (at least those from external channels) + rel: "0", + }; + + const youtubeUrl = this.addQueryParamsToUrl(baseYoutubeUrl, youtubeQueryParams); + this.src = this.domSanitizer.bypassSecurityTrustResourceUrl(youtubeUrl); + } + + /** Add key/value query params to a url string */ + private addQueryParamsToUrl(url: string, params: { [key: string]: string }): string { + const urlObj = new URL(url); + Object.keys(params).forEach((key) => { + urlObj.searchParams.set(key, params[key]); + }); + return urlObj.toString(); + } } From 00a53040077aa6e8e0f9991790120fc05e0badb3 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 29 Oct 2024 12:45:21 +0000 Subject: [PATCH 03/45] chore: youtube component now takes full youtube URL rather than video ID --- .../components/youtube/youtube.component.html | 20 +++++----- .../components/youtube/youtube.component.ts | 39 +++++++++++-------- src/app/shared/utils/utils.ts | 15 +++++++ 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/app/shared/components/template/components/youtube/youtube.component.html b/src/app/shared/components/template/components/youtube/youtube.component.html index 225e19cc6..b69ce2e3b 100644 --- a/src/app/shared/components/template/components/youtube/youtube.component.html +++ b/src/app/shared/components/template/components/youtube/youtube.component.html @@ -1,9 +1,11 @@ -
- - @if (params.allowFullScreen) { - - } @else { - - } -
+@if (src) { +
+ + @if (params.allowFullScreen) { + + } @else { + + } +
+} diff --git a/src/app/shared/components/template/components/youtube/youtube.component.ts b/src/app/shared/components/template/components/youtube/youtube.component.ts index 61204a556..00afadcad 100644 --- a/src/app/shared/components/template/components/youtube/youtube.component.ts +++ b/src/app/shared/components/template/components/youtube/youtube.component.ts @@ -3,17 +3,19 @@ import { TemplateBaseComponent } from "../base"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; import { getBooleanParamFromTemplateRow, - getStringParamFromTemplateRow, + addQueryParamsToUrl, + getQueryParam, } from "src/app/shared/utils"; import { TemplateTranslateService } from "../../services/template-translate.service"; interface IYoutubeParams { - /** TEMPLATE PARAMETER: video_id */ - videoId: string; /** TEMPLATE PARAMETER: allow_full_screen. Default true */ allowFullScreen?: boolean; } +/** The name of the query param in a YouTube URL that is used to specify the video ID */ +const YOUTUBE_VIDEO_ID_QUERY_PARAM = "v"; + @Component({ selector: "youtube", templateUrl: "./youtube.component.html", @@ -22,6 +24,7 @@ interface IYoutubeParams { export class YoutubeComponent extends TemplateBaseComponent implements OnInit { params: Partial = {}; src: SafeResourceUrl; + videoId: string; constructor( private domSanitizer: DomSanitizer, @@ -32,12 +35,23 @@ export class YoutubeComponent extends TemplateBaseComponent implements OnInit { ngOnInit() { this.getParams(); - this.getYoutubeSrc(); + if (this.videoId) { + this.getYoutubeSrc(); + } } private getParams() { - this.params.videoId = - this._row.value || getStringParamFromTemplateRow(this._row, "video_id", ""); + if (this.value()) { + try { + // Extract the video ID from the YouTube URL – this means any query params from + // original URL are ignored (generally desirable) + this.videoId = getQueryParam(this.value(), YOUTUBE_VIDEO_ID_QUERY_PARAM); + } catch { + console.error(`[YouTube Component] "${this.value()}" is not a valid YouTube URL`); + } + } else { + console.error("[YouTube Component] A valid YouTube URL must be provided as value"); + } this.params.allowFullScreen = getBooleanParamFromTemplateRow( this._row, "allow_fullscreen", @@ -46,7 +60,7 @@ export class YoutubeComponent extends TemplateBaseComponent implements OnInit { } private getYoutubeSrc() { - const baseYoutubeUrl = `https://www.youtube.com/embed/${this.params.videoId}`; + const baseYoutubeUrl = `https://www.youtube.com/embed/${this.videoId}`; // See https://developers.google.com/youtube/player_parameters const youtubeQueryParams = { @@ -60,16 +74,7 @@ export class YoutubeComponent extends TemplateBaseComponent implements OnInit { rel: "0", }; - const youtubeUrl = this.addQueryParamsToUrl(baseYoutubeUrl, youtubeQueryParams); + const youtubeUrl = addQueryParamsToUrl(baseYoutubeUrl, youtubeQueryParams); this.src = this.domSanitizer.bypassSecurityTrustResourceUrl(youtubeUrl); } - - /** Add key/value query params to a url string */ - private addQueryParamsToUrl(url: string, params: { [key: string]: string }): string { - const urlObj = new URL(url); - Object.keys(params).forEach((key) => { - urlObj.searchParams.set(key, params[key]); - }); - return urlObj.toString(); - } } diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 23b7ded27..148bcdf37 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -525,3 +525,18 @@ export function convertBlobToBase64(blob: Blob): Promise { reader.readAsDataURL(blob); }); } + +/** Add key/value query params to a url string */ +export function addQueryParamsToUrl(url: string, params: { [key: string]: string }): string { + const urlObj = new URL(url); + Object.keys(params).forEach((key) => { + urlObj.searchParams.set(key, params[key]); + }); + return urlObj.toString(); +} + +/** Extract the value of a query param from a URL string */ +export function getQueryParam(url: string, param: string): string | null { + const urlObj = new URL(url); + return urlObj.searchParams.get(param); +} From c8c5ba804fd0ba30526308202c7e7fccd5caf126 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 29 Oct 2024 13:03:43 +0000 Subject: [PATCH 04/45] chore: youtube component extracts lan guage code from app_language string to set interface lang --- .../components/youtube/youtube.component.ts | 3 ++- src/app/shared/utils/utils.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/youtube/youtube.component.ts b/src/app/shared/components/template/components/youtube/youtube.component.ts index 00afadcad..21ac3e806 100644 --- a/src/app/shared/components/template/components/youtube/youtube.component.ts +++ b/src/app/shared/components/template/components/youtube/youtube.component.ts @@ -5,6 +5,7 @@ import { getBooleanParamFromTemplateRow, addQueryParamsToUrl, getQueryParam, + extractTwoLetterLanguageCode, } from "src/app/shared/utils"; import { TemplateTranslateService } from "../../services/template-translate.service"; @@ -69,7 +70,7 @@ export class YoutubeComponent extends TemplateBaseComponent implements OnInit { // hide the fullscreen button if allow_fullscreen is false fs: this.params.allowFullScreen ? "1" : "0", // Attempt to set the player's interface language to match the app language - hl: this.templateTranslateService.app_language$.value, + hl: extractTwoLetterLanguageCode(this.templateTranslateService.app_language$.value), // Disable related videos (at least those from external channels) rel: "0", }; diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 148bcdf37..889a2eedb 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -540,3 +540,19 @@ export function getQueryParam(url: string, param: string): string | null { const urlObj = new URL(url); return urlObj.searchParams.get(param); } + +/** + * Extracts the two-letter language code from a given country language string. + * Two-letter ISO 639-1 Codes: https://www.loc.gov/standards/iso639-2/php/code_list.php + * @param {string} languageCode Country language code from in `ab_ab` or `ab_abc` format + * @returns {string} The extracted two-letter language code, e.g. `ab`, or the original country language code if the format is invalid + */ +export function extractTwoLetterLanguageCode(languageCode: string): string { + const parts = languageCode.split("_"); + + if (parts.length === 2 && parts[1].length === 2) { + return parts[1]; + } + // Return original language code if the format is invalid + return languageCode; +} From 599d1cab40025dbe9660d739b663d25c5190b965 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 29 Oct 2024 13:13:11 +0000 Subject: [PATCH 05/45] chore: fix comment --- src/app/shared/utils/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 889a2eedb..0a3f22ab7 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -544,8 +544,8 @@ export function getQueryParam(url: string, param: string): string | null { /** * Extracts the two-letter language code from a given country language string. * Two-letter ISO 639-1 Codes: https://www.loc.gov/standards/iso639-2/php/code_list.php - * @param {string} languageCode Country language code from in `ab_ab` or `ab_abc` format - * @returns {string} The extracted two-letter language code, e.g. `ab`, or the original country language code if the format is invalid + * @param {string} languageCode Country language code from in `xx_yy` format, where `yy` is the two-letter language code + * @returns {string} The extracted two-letter language code, e.g. `yy`, or the original country language code if the format is invalid */ export function extractTwoLetterLanguageCode(languageCode: string): string { const parts = languageCode.split("_"); From 1dce6703a0636065c6c9ff00af361f53190cd38d Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 29 Oct 2024 15:06:59 +0000 Subject: [PATCH 06/45] feat: add screen_orientation action --- package.json | 1 + packages/data-models/flowTypes.ts | 1 + src/app/app.component.ts | 5 +- .../screen-orientation.service.spec.ts | 16 ++++++ .../screen-orientation.service.ts | 57 +++++++++++++++++++ yarn.lock | 10 ++++ 6 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/services/screen-orientation/screen-orientation.service.spec.ts create mode 100644 src/app/shared/services/screen-orientation/screen-orientation.service.ts diff --git a/package.json b/package.json index 5a45d798f..2692461ab 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@capacitor/ios": "^6.0.0", "@capacitor/local-notifications": "^6.0.0", "@capacitor/push-notifications": "^6.0.0", + "@capacitor/screen-orientation": "^6.0.2", "@capacitor/share": "^6.0.0", "@capacitor/splash-screen": "^6.0.0", "@capawesome/capacitor-app-update": "^6.0.0", diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index eff2b0ff4..aba26f02f 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -414,6 +414,7 @@ export namespace FlowTypes { "process_template", "reset_app", "save_to_device", + "screen_orientation", "set_field", "set_item", "set_items", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b16b1adf0..1f09d1f52 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -42,6 +42,7 @@ import { FeedbackService } from "./feature/feedback/feedback.service"; 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"; @Component({ selector: "app-root", @@ -111,7 +112,8 @@ export class AppComponent { private appUpdateService: AppUpdateService, private remoteAssetService: RemoteAssetService, private shareService: ShareService, - private fileManagerService: FileManagerService + private fileManagerService: FileManagerService, + private screenOrientationService: ScreenOrientationService ) { this.initializeApp(); } @@ -249,6 +251,7 @@ export class AppComponent { this.feedbackService, this.shareService, this.fileManagerService, + this.screenOrientationService, ], deferred: [this.analyticsService], implicit: [ diff --git a/src/app/shared/services/screen-orientation/screen-orientation.service.spec.ts b/src/app/shared/services/screen-orientation/screen-orientation.service.spec.ts new file mode 100644 index 000000000..6d13da74d --- /dev/null +++ b/src/app/shared/services/screen-orientation/screen-orientation.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from "@angular/core/testing"; + +import { ScreenOrientationService } from "./screen-orientation.service"; + +describe("ScreenOrientationService", () => { + let service: ScreenOrientationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ScreenOrientationService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/services/screen-orientation/screen-orientation.service.ts b/src/app/shared/services/screen-orientation/screen-orientation.service.ts new file mode 100644 index 000000000..6fe1ba1c9 --- /dev/null +++ b/src/app/shared/services/screen-orientation/screen-orientation.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from "@angular/core"; +import { SyncServiceBase } from "../syncService.base"; +import { + ScreenOrientation, + OrientationLockType, + OrientationLockOptions, +} from "@capacitor/screen-orientation"; +import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; + +const ORIENTATION_TYPES: OrientationLockType[] = ["portrait", "landscape"]; + +@Injectable({ + providedIn: "root", +}) +export class ScreenOrientationService extends SyncServiceBase { + constructor(private templateActionRegistry: TemplateActionRegistry) { + super("Screen Orientation Service"); + this.initialise(); + } + + initialise() { + 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}`); + } + }, + }); + } + + private async setPortrait() { + return await this.setOrientation("portrait"); + } + + private async setLandscape() { + return await this.setOrientation("landscape"); + } + + private async getOrientation() { + return await ScreenOrientation.orientation(); + } + + private async setOrientation(orientation: OrientationLockType) { + return await ScreenOrientation.lock({ orientation }); + } + + private async unlockOrientation() { + return await ScreenOrientation.unlock(); + } +} diff --git a/yarn.lock b/yarn.lock index ed95ca825..3b5b5bf32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2196,6 +2196,15 @@ __metadata: languageName: node linkType: hard +"@capacitor/screen-orientation@npm:^6.0.2": + version: 6.0.2 + resolution: "@capacitor/screen-orientation@npm:6.0.2" + peerDependencies: + "@capacitor/core": ^6.0.0 + checksum: d7e431f9c42373d71d2d6b062ba3fc2fc1e4b034b02166b13c7ba4366e932e61c7558ae6e0429a84a7273f246ea42b23d3ae673ca8a8e7e60517f85d9bd41b22 + languageName: node + linkType: hard + "@capacitor/share@npm:^6.0.0": version: 6.0.2 resolution: "@capacitor/share@npm:6.0.2" @@ -14955,6 +14964,7 @@ __metadata: "@capacitor/ios": ^6.0.0 "@capacitor/local-notifications": ^6.0.0 "@capacitor/push-notifications": ^6.0.0 + "@capacitor/screen-orientation": ^6.0.2 "@capacitor/share": ^6.0.0 "@capacitor/splash-screen": ^6.0.0 "@capawesome/capacitor-app-update": ^6.0.0 From 6498b46047619a95d7a4ea1a1d91eed65b945439 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 29 Oct 2024 15:19:32 +0000 Subject: [PATCH 07/45] chore(screen_orienation): update native build files --- android/app/capacitor.build.gradle | 1 + android/app/src/main/assets/capacitor.plugins.json | 4 ++++ android/capacitor.settings.gradle | 3 +++ ios/App/Podfile | 1 + ios/App/Podfile.lock | 8 +++++++- 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 56f11f9da..7b94932b6 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -19,6 +19,7 @@ dependencies { implementation project(':capacitor-filesystem') implementation project(':capacitor-local-notifications') implementation project(':capacitor-push-notifications') + implementation project(':capacitor-screen-orientation') implementation project(':capacitor-share') implementation project(':capacitor-splash-screen') implementation project(':capawesome-capacitor-app-update') diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json index 0b58a1de1..7842b6c0b 100644 --- a/android/app/src/main/assets/capacitor.plugins.json +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -39,6 +39,10 @@ "pkg": "@capacitor/push-notifications", "classpath": "com.capacitorjs.plugins.pushnotifications.PushNotificationsPlugin" }, + { + "pkg": "@capacitor/screen-orientation", + "classpath": "com.capacitorjs.plugins.screenorientation.ScreenOrientationPlugin" + }, { "pkg": "@capacitor/share", "classpath": "com.capacitorjs.plugins.share.SharePlugin" diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 1e3c51244..d4599d757 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -32,6 +32,9 @@ project(':capacitor-local-notifications').projectDir = new File('../node_modules include ':capacitor-push-notifications' project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android') +include ':capacitor-screen-orientation' +project(':capacitor-screen-orientation').projectDir = new File('../node_modules/@capacitor/screen-orientation/android') + include ':capacitor-share' project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index 264b8690f..9d884d6eb 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -21,6 +21,7 @@ def capacitor_pods pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem' pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications' pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications' + pod 'CapacitorScreenOrientation', :path => '../../node_modules/@capacitor/screen-orientation' pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' pod 'CapawesomeCapacitorAppUpdate', :path => '../../node_modules/@capawesome/capacitor-app-update' diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 28b9c62be..ca0e5025b 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -32,6 +32,8 @@ PODS: - Capacitor - CapacitorPushNotifications (6.0.2): - Capacitor + - CapacitorScreenOrientation (6.0.2): + - Capacitor - CapacitorShare (6.0.2): - Capacitor - CapacitorSplashScreen (6.0.2): @@ -162,6 +164,7 @@ DEPENDENCIES: - "CapacitorFirebasePerformance (from `../../node_modules/@capacitor-firebase/performance`)" - "CapacitorLocalNotifications (from `../../node_modules/@capacitor/local-notifications`)" - "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)" + - "CapacitorScreenOrientation (from `../../node_modules/@capacitor/screen-orientation`)" - "CapacitorShare (from `../../node_modules/@capacitor/share`)" - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)" - "CapawesomeCapacitorAppUpdate (from `../../node_modules/@capawesome/capacitor-app-update`)" @@ -218,6 +221,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/local-notifications" CapacitorPushNotifications: :path: "../../node_modules/@capacitor/push-notifications" + CapacitorScreenOrientation: + :path: "../../node_modules/@capacitor/screen-orientation" CapacitorShare: :path: "../../node_modules/@capacitor/share" CapacitorSplashScreen: @@ -239,6 +244,7 @@ SPEC CHECKSUMS: CapacitorFirebasePerformance: c806ce7f8270295465c050210d8e8a4ae2dc282e CapacitorLocalNotifications: 6bac9e948b2b8852506c6d74abb2cde140250f86 CapacitorPushNotifications: ccd797926c030acad3d5498ef452c735c90a2c89 + CapacitorScreenOrientation: 6039dc2ea4a8596b79316709d5727e8bb7e32845 CapacitorShare: 591ae4693d85686ceb590db8e8b44aa014ec6490 CapacitorSplashScreen: 250df9ef8014fac5c7c1fd231f0f8b1d8f0b5624 CapawesomeCapacitorAppUpdate: 3c05b5c8e42f9c6a88d666093406e9336d9bfdb1 @@ -264,6 +270,6 @@ SPEC CHECKSUMS: PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 -PODFILE CHECKSUM: a6362e50008bd5aea904cdd9ec858da94a7f155c +PODFILE CHECKSUM: 131ec70809fe3dec63113a003a42e6009560dcef COCOAPODS: 1.15.2 From b37ca0d0ac06582b671b7efdd7c278bd8cca02ae Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 29 Oct 2024 20:37:07 +0000 Subject: [PATCH 08/45] refactor: youtube component --- cspell.config.yml | 1 + .../components/youtube/youtube.component.html | 8 +- .../components/youtube/youtube.component.ts | 104 +++++++++--------- 3 files changed, 60 insertions(+), 53 deletions(-) diff --git a/cspell.config.yml b/cspell.config.yml index 991142f97..1c82afca4 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -37,6 +37,7 @@ "tmpl", "venv", "viewbox", + "youtube", ] # flagWords - list of words to be always considered incorrect # This is useful for offensive words and common spelling errors. diff --git a/src/app/shared/components/template/components/youtube/youtube.component.html b/src/app/shared/components/template/components/youtube/youtube.component.html index b69ce2e3b..6a1817319 100644 --- a/src/app/shared/components/template/components/youtube/youtube.component.html +++ b/src/app/shared/components/template/components/youtube/youtube.component.html @@ -1,11 +1,11 @@ -@if (src) { +@if (src()) {
- @if (params.allowFullScreen) { - + @if (params().allowFullScreen) { + } @else { - + }
} diff --git a/src/app/shared/components/template/components/youtube/youtube.component.ts b/src/app/shared/components/template/components/youtube/youtube.component.ts index 21ac3e806..f107348cf 100644 --- a/src/app/shared/components/template/components/youtube/youtube.component.ts +++ b/src/app/shared/components/template/components/youtube/youtube.component.ts @@ -1,14 +1,13 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, computed } from "@angular/core"; import { TemplateBaseComponent } from "../base"; -import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; -import { - getBooleanParamFromTemplateRow, - addQueryParamsToUrl, - getQueryParam, - extractTwoLetterLanguageCode, -} from "src/app/shared/utils"; +import { DomSanitizer } from "@angular/platform-browser"; +import { extractTwoLetterLanguageCode, getBooleanParamFromTemplateRow } from "src/app/shared/utils"; import { TemplateTranslateService } from "../../services/template-translate.service"; +interface ITemplateParams { + allow_full_screen?: string; +} + interface IYoutubeParams { /** TEMPLATE PARAMETER: allow_full_screen. Default true */ allowFullScreen?: boolean; @@ -22,11 +21,7 @@ const YOUTUBE_VIDEO_ID_QUERY_PARAM = "v"; templateUrl: "./youtube.component.html", styleUrls: ["./youtube.component.scss"], }) -export class YoutubeComponent extends TemplateBaseComponent implements OnInit { - params: Partial = {}; - src: SafeResourceUrl; - videoId: string; - +export class YoutubeComponent extends TemplateBaseComponent { constructor( private domSanitizer: DomSanitizer, private templateTranslateService: TemplateTranslateService @@ -34,48 +29,59 @@ export class YoutubeComponent extends TemplateBaseComponent implements OnInit { super(); } - ngOnInit() { - this.getParams(); - if (this.videoId) { - this.getYoutubeSrc(); + public params = computed(() => this.parseParams(this.parameterList())); + + public src = computed(() => { + const url = this.parseValue(this.value()); + if (url) { + const urlWithParams = this.setUrlParams(url, this.params()); + return this.domSanitizer.bypassSecurityTrustResourceUrl(urlWithParams.toString()); } + return undefined; + }); + + private parseParams(parameterList: ITemplateParams): IYoutubeParams { + // NOTE - param parsing takes full row not just parameterList + // Still included as function arg to prompt re-evaluate if parameters change + return { allowFullScreen: getBooleanParamFromTemplateRow(this._row, "allow_fullscreen", true) }; } - private getParams() { - if (this.value()) { - try { - // Extract the video ID from the YouTube URL – this means any query params from - // original URL are ignored (generally desirable) - this.videoId = getQueryParam(this.value(), YOUTUBE_VIDEO_ID_QUERY_PARAM); - } catch { - console.error(`[YouTube Component] "${this.value()}" is not a valid YouTube URL`); + /** Validate template value field and convert to URL object **/ + private parseValue(value: any) { + // Expect valid url. Don't specify domain as could start youtube.com, youtu.be, m.youtube.com, etc. + // https://stackoverflow.com/a/70512384/5693245 + if (value && typeof value === "string" && value.startsWith("https://")) { + const url = new URL(value); + // only support urls that include video id through parameter (e.g. not youtu.be/12345678901) + const videoId = url.searchParams.get(YOUTUBE_VIDEO_ID_QUERY_PARAM); + if (videoId) { + url.searchParams.delete(YOUTUBE_VIDEO_ID_QUERY_PARAM); + // rewrite host and pathname to use youtube embed version + url.host = "youtube.com"; + url.pathname = `/embed/${videoId}`; + return url; } - } else { - console.error("[YouTube Component] A valid YouTube URL must be provided as value"); } - this.params.allowFullScreen = getBooleanParamFromTemplateRow( - this._row, - "allow_fullscreen", - true - ); + console.error("[Youtube] Invalid value:", value); } - private getYoutubeSrc() { - const baseYoutubeUrl = `https://www.youtube.com/embed/${this.videoId}`; - - // See https://developers.google.com/youtube/player_parameters - const youtubeQueryParams = { - // Favour white over red for more theme compatibility - color: "white", - // hide the fullscreen button if allow_fullscreen is false - fs: this.params.allowFullScreen ? "1" : "0", - // Attempt to set the player's interface language to match the app language - hl: extractTwoLetterLanguageCode(this.templateTranslateService.app_language$.value), - // Disable related videos (at least those from external channels) - rel: "0", - }; - - const youtubeUrl = addQueryParamsToUrl(baseYoutubeUrl, youtubeQueryParams); - this.src = this.domSanitizer.bypassSecurityTrustResourceUrl(youtubeUrl); + /** + * Update player parameters from authored + * See https://developers.google.com/youtube/player_parameters + * NOTE - these will be merged with any params passed with the url itself + */ + private setUrlParams(url: URL, params: IYoutubeParams) { + // Favour white over red for more theme compatibility + url.searchParams.set("color", "white"); + // hide the fullscreen button if allow_fullscreen is false + url.searchParams.set("fs", params.allowFullScreen ? "1" : "0"); + // Attempt to set the player's interface language to match the app language + const languageCode = extractTwoLetterLanguageCode( + this.templateTranslateService.app_language$.value + ); + url.searchParams.set("hl", languageCode); + // Disable related videos (at least those from external channels) + url.searchParams.set("rel", "0"); + return url; } } From df9a9d15af6dbe191ec8bf809142e9f3738d0b5b Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 30 Oct 2024 11:00:59 +0000 Subject: [PATCH 09/45] chore: add type-checking to youtube query params setting --- .../components/youtube/youtube.component.ts | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/app/shared/components/template/components/youtube/youtube.component.ts b/src/app/shared/components/template/components/youtube/youtube.component.ts index f107348cf..e8f1ea322 100644 --- a/src/app/shared/components/template/components/youtube/youtube.component.ts +++ b/src/app/shared/components/template/components/youtube/youtube.component.ts @@ -5,16 +5,34 @@ import { extractTwoLetterLanguageCode, getBooleanParamFromTemplateRow } from "sr import { TemplateTranslateService } from "../../services/template-translate.service"; interface ITemplateParams { - allow_full_screen?: string; + allow_fullscreen?: string; } interface IYoutubeParams { - /** TEMPLATE PARAMETER: allow_full_screen. Default true */ + /** TEMPLATE PARAMETER: allow_fullscreen. Default true */ allowFullScreen?: boolean; } -/** The name of the query param in a YouTube URL that is used to specify the video ID */ -const YOUTUBE_VIDEO_ID_QUERY_PARAM = "v"; +/** + * The names of the Youtube-specific query params that will be added to the url + * For a full list and explanation, see https://developers.google.com/youtube/player_parameters + * */ +const YOUTUBE_URL_QUERY_PARAMS: { [K in keyof YouTubeUrlQueryParamValues]: string } = { + videoId: "v", + color: "color", + showFullscreenButton: "fs", + interfaceLanguage: "hl", + showRelatedVideos: "rel", +}; + +/** Possible values of the supported query params */ +interface YouTubeUrlQueryParamValues { + videoId: string; + color: "red" | "white"; + showFullscreenButton: "0" | "1"; + interfaceLanguage: string; // 2-letter ISO 639-1 code + showRelatedVideos: "0" | "1"; +} @Component({ selector: "youtube", @@ -53,9 +71,9 @@ export class YoutubeComponent extends TemplateBaseComponent { if (value && typeof value === "string" && value.startsWith("https://")) { const url = new URL(value); // only support urls that include video id through parameter (e.g. not youtu.be/12345678901) - const videoId = url.searchParams.get(YOUTUBE_VIDEO_ID_QUERY_PARAM); + const videoId = url.searchParams.get(YOUTUBE_URL_QUERY_PARAMS.videoId); if (videoId) { - url.searchParams.delete(YOUTUBE_VIDEO_ID_QUERY_PARAM); + url.searchParams.delete(YOUTUBE_URL_QUERY_PARAMS.videoId); // rewrite host and pathname to use youtube embed version url.host = "youtube.com"; url.pathname = `/embed/${videoId}`; @@ -72,16 +90,25 @@ export class YoutubeComponent extends TemplateBaseComponent { */ private setUrlParams(url: URL, params: IYoutubeParams) { // Favour white over red for more theme compatibility - url.searchParams.set("color", "white"); + this.setYouTubeParam(url, "color", "white"); // hide the fullscreen button if allow_fullscreen is false - url.searchParams.set("fs", params.allowFullScreen ? "1" : "0"); + this.setYouTubeParam(url, "showFullscreenButton", params.allowFullScreen ? "1" : "0"); // Attempt to set the player's interface language to match the app language const languageCode = extractTwoLetterLanguageCode( this.templateTranslateService.app_language$.value ); - url.searchParams.set("hl", languageCode); + this.setYouTubeParam(url, "interfaceLanguage", languageCode); // Disable related videos (at least those from external channels) - url.searchParams.set("rel", "0"); + this.setYouTubeParam(url, "showRelatedVideos", "0"); return url; } + + private setYouTubeParam = ( + url: URL, + key: K, + value: YouTubeUrlQueryParamValues[K] + ) => { + const paramName = YOUTUBE_URL_QUERY_PARAMS[key]; + url.searchParams.set(paramName, value); + }; } From c9ec12019dfded910bc4d42e1657c4afceafe4d0 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 30 Oct 2024 11:38:36 +0000 Subject: [PATCH 10/45] chore: tidy up dynamic prefix types --- packages/data-models/appConfig.ts | 13 ------------- packages/data-models/flowTypes.ts | 26 ++++++++++++++++++++++++-- packages/data-models/functions.ts | 2 +- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/data-models/appConfig.ts b/packages/data-models/appConfig.ts index 6b9b6781f..6e3d690c5 100644 --- a/packages/data-models/appConfig.ts +++ b/packages/data-models/appConfig.ts @@ -14,18 +14,6 @@ import { IAppSkin } from "./skin.model"; * special use case for relative paths ********************************************************************************************/ -const DYNAMIC_PREFIXES = [ - "local", - "field", - "fields", - "global", - "data", - "campaign", - "calc", - "item", - "raw", -] as const; - const APP_LANGUAGES = { /** Language used during first load. If translations do not exist will default to source strings (gb_en) */ default: "gb_en", @@ -216,7 +204,6 @@ const APP_CONFIG = { APP_THEMES, APP_UPDATES, ASSET_PACKS, - DYNAMIC_PREFIXES, FEEDBACK_MODULE_DEFAULTS, NOTIFICATIONS_SYNC_FREQUENCY_MS, NOTIFICATION_DEFAULTS, diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index eff2b0ff4..ae4871478 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -339,7 +339,7 @@ export namespace FlowTypes { /** Keep a list of dynamic dependencies used within a template, by reference (e.g. {@local.var1 : ["text_1"]}) */ _dynamicDependencies?: { [reference: string]: string[] }; _translatedFields?: { [field: string]: any }; - _evalContext?: { itemContext: TemplateRowItemEvalContext }; // force specific context variables when calculating eval statements (such as loop items) + _evalContext?: any; // force specific context variables when calculating eval statements (such as loop items) __EMPTY?: any; // empty cells (can be removed after pr 679 merged) } @@ -361,7 +361,26 @@ export namespace FlowTypes { [key: string]: any; }; - type IDynamicPrefix = IAppConfig["DYNAMIC_PREFIXES"][number]; + const DYNAMIC_PREFIXES_COMPILER = ["gen", "row"] as const; + + const DYNAMIC_PREFIXES_RUNTIME = [ + "local", + "field", + "fields", + "global", + "data", + "campaign", + "calc", + "item", + "raw", + ] as const; + + export const DYNAMIC_PREFIXES = [ + ...DYNAMIC_PREFIXES_COMPILER, + ...DYNAMIC_PREFIXES_RUNTIME, + ] as const; + + export type IDynamicPrefix = (typeof DYNAMIC_PREFIXES)[number]; /** Data passed back from regex match, e.g. expression @local.someField => type:local, fieldName: someField */ export interface TemplateRowDynamicEvaluator { @@ -415,8 +434,11 @@ export namespace FlowTypes { "reset_app", "save_to_device", "set_field", + /** NOTE - only available from with data_items loop */ "set_item", + /** NOTE - only available from with data_items loop */ "set_items", + "set_data", "set_local", "share", "style", diff --git a/packages/data-models/functions.ts b/packages/data-models/functions.ts index 9131065e8..64b1d08b8 100644 --- a/packages/data-models/functions.ts +++ b/packages/data-models/functions.ts @@ -90,7 +90,7 @@ export function extractDynamicEvaluators( type = "raw"; } // cross-check to ensure lookup matches one of the pre-defined dynamic field types (e.g. not email@domain.com) - if (!appConfigDefault.DYNAMIC_PREFIXES.includes(type)) { + if (!FlowTypes.DYNAMIC_PREFIXES.includes(type)) { return undefined; } return { fullExpression, matchedExpression, type, fieldName }; From 9d1ae497bf0357d60c1eb89e970f446f6efde6b5 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 30 Oct 2024 11:49:04 +0000 Subject: [PATCH 11/45] chore: items code tidying --- .../data-items/data-items.component.ts | 9 +- .../components/template/processors/item.ts | 83 +++++-------------- .../template/processors/itemPipe.spec.ts | 1 + .../template/processors/itemPipe.ts | 41 +++++++++ .../services/instance/template-row.service.ts | 17 ++-- 5 files changed, 78 insertions(+), 73 deletions(-) create mode 100644 src/app/shared/components/template/processors/itemPipe.spec.ts create mode 100644 src/app/shared/components/template/processors/itemPipe.ts diff --git a/src/app/shared/components/template/components/data-items/data-items.component.ts b/src/app/shared/components/template/components/data-items/data-items.component.ts index f6700a986..e7bfe8ddd 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.ts @@ -77,10 +77,11 @@ export class TmplDataItemsComponent extends TemplateBaseComponent implements OnD parameterList: any ) { const parsedItemDataList = await this.parseDataList(itemDataList); - const { itemRows, itemData } = new ItemProcessor(parsedItemDataList, parameterList).process( - rows - ); - const itemRowsWithMeta = this.setItemMeta(itemRows, itemData, this.dataListName); + const { itemTemplateRows, itemData } = new ItemProcessor( + Object.values(parsedItemDataList), + parameterList + ).process(rows); + const itemRowsWithMeta = this.setItemMeta(itemTemplateRows, itemData, this.dataListName); const parsedItemRows = await this.hackProcessRows(itemRowsWithMeta); // TODO - deep diff and only update changed diff --git a/src/app/shared/components/template/processors/item.ts b/src/app/shared/components/template/processors/item.ts index b7805d383..55eb1e8e3 100644 --- a/src/app/shared/components/template/processors/item.ts +++ b/src/app/shared/components/template/processors/item.ts @@ -1,27 +1,28 @@ -// NOTE - importing from 'shared' will fail as contains non-browser packages and -// name conflicts with local 'shared' folder. Import full path from packages instead -import { JSEvaluator } from "packages/shared/src/models/jsEvaluator/jsEvaluator"; import { TemplatedData } from "packages/shared/src/models/templatedData/templatedData"; - -import { shuffleArray } from "src/app/shared/utils"; import { FlowTypes } from "../models"; -import { objectToArray } from "../utils"; +import { ItemDataPipe } from "./itemPipe"; + +type IItemEvalContext = FlowTypes.TemplateRowItemEvalContextMetadata; + +interface ITemplateRowWithItemContext extends FlowTypes.TemplateRow { + _evalContext: { itemContext: IItemEvalContext }; // force specific context variables when calculating eval statements (such as loop items) +} export class ItemProcessor { constructor( - private dataList: any, - private parameterList?: any + private dataList: FlowTypes.Data_listRow[] = [], + private parameterList: any = {} ) {} public process(templateRows: any) { - const data = objectToArray(this.dataList); - const pipedData = this.pipeData(data, this.parameterList); - const itemRows = this.generateLoopItemRows(templateRows, pipedData); - const parsedItemRows = this.hackSetNestedName(itemRows); + const pipedData = this.pipeData(this.dataList, this.parameterList); + const itemTemplateRows = this.generateLoopItemRows(templateRows, pipedData); + const parsedItemTemplatedRows = this.hackSetNestedName(itemTemplateRows); // Return both rows for rendering and list of itemData used (post pipe operations) - return { itemRows: parsedItemRows, itemData: pipedData }; + return { itemTemplateRows: parsedItemTemplatedRows, itemData: pipedData }; } + /** Process all item list operators, such as filter, sort and limit */ private pipeData(data: any[], parameter_list: any) { if (parameter_list) { const operations = Object.entries(parameter_list).map(([name, arg]) => ({ @@ -43,7 +44,7 @@ export class ItemProcessor { templateRows: FlowTypes.TemplateRow[], itemData: FlowTypes.Data_listRow[] ) { - const loopItemRows: FlowTypes.TemplateRow[] = []; + const loopItemRows: ITemplateRowWithItemContext[] = []; const lastItemIndex = itemData.length - 1; for (const [indexKey, item] of Object.entries(itemData)) { const _index = Number(indexKey); @@ -53,7 +54,7 @@ export class ItemProcessor { _first: _index === 0, _last: _index === lastItemIndex, }; - const evalContext: FlowTypes.TemplateRow["_evalContext"] = { + const evalContext: ITemplateRowWithItemContext["_evalContext"] = { itemContext: { ...item, ...itemContextMeta, @@ -71,11 +72,11 @@ export class ItemProcessor { /** Update the evaluation context of a row and recursively any nested rows */ private setRecursiveRowEvalContext( row: FlowTypes.TemplateRow, - evalContext: FlowTypes.TemplateRow["_evalContext"] - ) { + evalContext: ITemplateRowWithItemContext["_evalContext"] + ): ITemplateRowWithItemContext { // Workaround destructure for memory allocation issues (applying click action of last item only) const { rows, ...rest } = JSON.parse(JSON.stringify(row)); - const rowWithEvalContext: FlowTypes.TemplateRow = { ...rest, _evalContext: evalContext }; + const rowWithEvalContext: ITemplateRowWithItemContext = { ...rest, _evalContext: evalContext }; // handle child rows independently to avoid accidental property leaks if (row.rows) { rowWithEvalContext.rows = []; @@ -92,59 +93,17 @@ export class ItemProcessor { * so use delimited syntax and parse via newer TemplatedData processor * @see https://github.com/IDEMSInternational/parenting-app-ui/issues/1765 */ - private hackSetNestedName(itemRows: FlowTypes.TemplateRow[]) { + private hackSetNestedName(itemRows: ITemplateRowWithItemContext[]) { const parsedRows = []; for (const row of itemRows) { const parser = new TemplatedData({ context: { item: row._evalContext.itemContext } }); const { rows, _nested_name } = row; row._nested_name = parser.parse(_nested_name); if (rows) { - row.rows = this.hackSetNestedName(rows); + row.rows = this.hackSetNestedName(rows as ITemplateRowWithItemContext[]); } parsedRows.push(row); } return parsedRows; } } - -/** - * - */ -class ItemDataPipe { - public process(data: any[], operations: { name: string; arg?: string }[]) { - for (const { name, arg } of operations) { - const operator = this.operations[name]; - if (operator) { - data = operator(data, arg); - } else { - console.error("No item pipeline operation found", name); - } - } - return data; - } - - private operations = { - shuffle: (items: any[] = []) => { - return shuffleArray(items); - }, - sort: (items: any[] = [], sortField: string) => { - if (!sortField) return items; - return items.sort((a, b) => (a[sortField] > b[sortField] ? 1 : -1)); - }, - filter: (items: any[] = [], expression: string) => { - if (!expression) return; - return items.filter((item) => { - // NOTE - expects all non-item condition to be evaluated - // e.g. `@item.field > @local.some_value` already be evaluated to `this.item.field > "local value"` - const evaluator = new JSEvaluator(); - const evaluated = evaluator.evaluate(expression, { item }); - return evaluated; - }); - }, - reverse: (items: any[] = []) => items.reverse(), - limit: (items: any[] = [], value: string) => { - if (!value) return items; - return items.slice(0, Number(value)); - }, - }; -} diff --git a/src/app/shared/components/template/processors/itemPipe.spec.ts b/src/app/shared/components/template/processors/itemPipe.spec.ts new file mode 100644 index 000000000..968acf8c8 --- /dev/null +++ b/src/app/shared/components/template/processors/itemPipe.spec.ts @@ -0,0 +1 @@ +// TODO - add spec test diff --git a/src/app/shared/components/template/processors/itemPipe.ts b/src/app/shared/components/template/processors/itemPipe.ts new file mode 100644 index 000000000..63fcfc471 --- /dev/null +++ b/src/app/shared/components/template/processors/itemPipe.ts @@ -0,0 +1,41 @@ +import { JSEvaluator } from "packages/shared/src/models/jsEvaluator/jsEvaluator"; +import { shuffleArray } from "src/app/shared/utils"; + +export class ItemDataPipe { + public process(data: any[], operations: { name: string; arg?: string }[]) { + for (const { name, arg } of operations) { + const operator = this.operations[name]; + if (operator) { + data = operator(data, arg); + } else { + console.error("No item pipeline operation found", name); + } + } + return data; + } + + private operations = { + shuffle: (items: any[] = []) => { + return shuffleArray(items); + }, + sort: (items: any[] = [], sortField: string) => { + if (!sortField) return items; + return items.sort((a, b) => (a[sortField] > b[sortField] ? 1 : -1)); + }, + filter: (items: any[] = [], expression: string) => { + if (!expression) return; + return items.filter((item) => { + // NOTE - expects all non-item condition to be evaluated + // e.g. `@item.field > @local.some_value` already be evaluated to `this.item.field > "local value"` + const evaluator = new JSEvaluator(); + const evaluated = evaluator.evaluate(expression, { item }); + return evaluated; + }); + }, + reverse: (items: any[] = []) => items.reverse(), + limit: (items: any[] = [], value: string) => { + if (!value) return items; + return items.slice(0, Number(value)); + }, + }; +} diff --git a/src/app/shared/components/template/services/instance/template-row.service.ts b/src/app/shared/components/template/services/instance/template-row.service.ts index 3d953a33f..460c1ab0a 100644 --- a/src/app/shared/components/template/services/instance/template-row.service.ts +++ b/src/app/shared/components/template/services/instance/template-row.service.ts @@ -286,12 +286,15 @@ export class TemplateRowService extends SyncServiceBase { // Instead of returning themselves items looped child rows if (type === "items") { - // extract raw parameter list - const itemDataList: { [id: string]: any } = row.value; + // items have their data lists already parsed as hashmap. convert back to array and process + const itemDataList = row.value as Record; const parsedItemDataList = await this.parseDataList(itemDataList); + const parsedItemDataRows = Object.values(parsedItemDataList); const { parameter_list, rows } = row; - const { itemRows } = new ItemProcessor(parsedItemDataList, parameter_list).process(rows); - const parsedItemRows = await this.processRows(itemRows, isNestedTemplate, row.name); + const { itemTemplateRows } = new ItemProcessor(parsedItemDataRows, parameter_list).process( + rows + ); + const parsedItemRows = await this.processRows(itemTemplateRows, isNestedTemplate, row.name); return parsedItemRows; } @@ -358,9 +361,9 @@ export class TemplateRowService extends SyncServiceBase { * Utils **************************************************************************************/ - private async parseDataList(dataList: { [id: string]: any }) { - const parsed: { [id: string]: any } = {}; - for (const [listKey, listValue] of Object.entries(dataList)) { + private async parseDataList(dataListHashmap: Record) { + const parsed: Record = {}; + for (const [listKey, listValue] of Object.entries(dataListHashmap)) { parsed[listKey] = listValue; for (const [itemKey, itemValue] of Object.entries(listValue)) { if (typeof itemValue === "string") { From 5ea35ef3e411b55ddce47c0db61a56b98d72bb22 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 30 Oct 2024 12:20:16 +0000 Subject: [PATCH 12/45] chore: code tidying --- .../components/youtube/youtube.component.ts | 6 ++-- .../services/template-translate.service.ts | 15 +++++++++ src/app/shared/utils/utils.ts | 31 ------------------- 3 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/app/shared/components/template/components/youtube/youtube.component.ts b/src/app/shared/components/template/components/youtube/youtube.component.ts index e8f1ea322..fbc37c433 100644 --- a/src/app/shared/components/template/components/youtube/youtube.component.ts +++ b/src/app/shared/components/template/components/youtube/youtube.component.ts @@ -1,7 +1,7 @@ import { Component, computed } from "@angular/core"; import { TemplateBaseComponent } from "../base"; import { DomSanitizer } from "@angular/platform-browser"; -import { extractTwoLetterLanguageCode, getBooleanParamFromTemplateRow } from "src/app/shared/utils"; +import { getBooleanParamFromTemplateRow } from "src/app/shared/utils"; import { TemplateTranslateService } from "../../services/template-translate.service"; interface ITemplateParams { @@ -94,9 +94,7 @@ export class YoutubeComponent extends TemplateBaseComponent { // hide the fullscreen button if allow_fullscreen is false this.setYouTubeParam(url, "showFullscreenButton", params.allowFullScreen ? "1" : "0"); // Attempt to set the player's interface language to match the app language - const languageCode = extractTwoLetterLanguageCode( - this.templateTranslateService.app_language$.value - ); + const languageCode = this.templateTranslateService.app_language_code; this.setYouTubeParam(url, "interfaceLanguage", languageCode); // Disable related videos (at least those from external channels) this.setYouTubeParam(url, "showRelatedVideos", "0"); diff --git a/src/app/shared/components/template/services/template-translate.service.ts b/src/app/shared/components/template/services/template-translate.service.ts index 5c8d86092..921f77147 100644 --- a/src/app/shared/components/template/services/template-translate.service.ts +++ b/src/app/shared/components/template/services/template-translate.service.ts @@ -52,6 +52,21 @@ export class TemplateTranslateService extends AsyncServiceBase { return this.app_language$.value; } + /** + * Extracts the two-letter language code from a given country language string. + * Two-letter ISO 639-1 Codes: https://www.loc.gov/standards/iso639-2/php/code_list.php + * @param {string} languageCode Country language code from in `xx_yy` format, where `yy` is the two-letter language code + * @returns {string} The extracted two-letter language code, e.g. `yy`, or the original country language code if the format is invalid + */ + get app_language_code() { + const parts = this.app_language.split("_"); + if (parts.length === 2 && parts[1].length === 2) { + return parts[1]; + } + // Return original language code if the format is invalid + return this.app_language; + } + /** Set the local storage variable that tracks the app language */ async setLanguage(code: string, updateDB = true) { if (code) { diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 0a3f22ab7..23b7ded27 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -525,34 +525,3 @@ export function convertBlobToBase64(blob: Blob): Promise { reader.readAsDataURL(blob); }); } - -/** Add key/value query params to a url string */ -export function addQueryParamsToUrl(url: string, params: { [key: string]: string }): string { - const urlObj = new URL(url); - Object.keys(params).forEach((key) => { - urlObj.searchParams.set(key, params[key]); - }); - return urlObj.toString(); -} - -/** Extract the value of a query param from a URL string */ -export function getQueryParam(url: string, param: string): string | null { - const urlObj = new URL(url); - return urlObj.searchParams.get(param); -} - -/** - * Extracts the two-letter language code from a given country language string. - * Two-letter ISO 639-1 Codes: https://www.loc.gov/standards/iso639-2/php/code_list.php - * @param {string} languageCode Country language code from in `xx_yy` format, where `yy` is the two-letter language code - * @returns {string} The extracted two-letter language code, e.g. `yy`, or the original country language code if the format is invalid - */ -export function extractTwoLetterLanguageCode(languageCode: string): string { - const parts = languageCode.split("_"); - - if (parts.length === 2 && parts[1].length === 2) { - return parts[1]; - } - // Return original language code if the format is invalid - return languageCode; -} From af59326837b34d571fc0612d99556e87118795fd Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 30 Oct 2024 14:55:35 +0000 Subject: [PATCH 13/45] chore: code tidy --- .../screen-orientation.service.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 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 6fe1ba1c9..1c08a99d8 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,6 @@ import { Injectable } from "@angular/core"; import { SyncServiceBase } from "../syncService.base"; -import { - ScreenOrientation, - OrientationLockType, - OrientationLockOptions, -} from "@capacitor/screen-orientation"; +import { ScreenOrientation, OrientationLockType } from "@capacitor/screen-orientation"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; const ORIENTATION_TYPES: OrientationLockType[] = ["portrait", "landscape"]; @@ -35,6 +31,10 @@ export class ScreenOrientationService extends SyncServiceBase { }); } + private async setOrientation(orientation: OrientationLockType) { + return await ScreenOrientation.lock({ orientation }); + } + private async setPortrait() { return await this.setOrientation("portrait"); } @@ -47,10 +47,6 @@ export class ScreenOrientationService extends SyncServiceBase { return await ScreenOrientation.orientation(); } - private async setOrientation(orientation: OrientationLockType) { - return await ScreenOrientation.lock({ orientation }); - } - private async unlockOrientation() { return await ScreenOrientation.unlock(); } From 10e15a2f31959848935a64474da6ab59342a09b3 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 31 Oct 2024 09:52:13 +0000 Subject: [PATCH 14/45] chore(screen-orientation): remove unused methods --- .../screen-orientation.service.ts | 16 ---------------- 1 file changed, 16 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 1c08a99d8..f0bcc103f 100644 --- a/src/app/shared/services/screen-orientation/screen-orientation.service.ts +++ b/src/app/shared/services/screen-orientation/screen-orientation.service.ts @@ -34,20 +34,4 @@ export class ScreenOrientationService extends SyncServiceBase { private async setOrientation(orientation: OrientationLockType) { return await ScreenOrientation.lock({ orientation }); } - - private async setPortrait() { - return await this.setOrientation("portrait"); - } - - private async setLandscape() { - return await this.setOrientation("landscape"); - } - - private async getOrientation() { - return await ScreenOrientation.orientation(); - } - - private async unlockOrientation() { - return await ScreenOrientation.unlock(); - } } From 504106a1adad51d67d2f3a5a6c2d6c2c8ab403ea Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 31 Oct 2024 11:53:35 +0000 Subject: [PATCH 15/45] fix: text bubble component handles markdown --- .../text-bubble/text-bubble.component.html | 6 +++--- .../text-bubble/text-bubble.component.scss | 17 +++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/app/shared/components/template/components/text-bubble/text-bubble.component.html b/src/app/shared/components/template/components/text-bubble/text-bubble.component.html index 41b01cb9c..ccad63c9b 100644 --- a/src/app/shared/components/template/components/text-bubble/text-bubble.component.html +++ b/src/app/shared/components/template/components/text-bubble/text-bubble.component.html @@ -5,9 +5,9 @@ >
@if (_row.value) { -

- {{ _row.value }} -

+ } @for (childRow of _row.rows | filterDisplayComponent; track trackByRow($index, childRow)) { Date: Thu, 31 Oct 2024 14:35:54 +0000 Subject: [PATCH 16/45] 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 d0695df13..63f2a2e76 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 d6cca5f06..1916242b6 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 17/45] 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 1916242b6..1f5805f59 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 af2b72244..d3a4b0750 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 f171fcb27..f9854ab92 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 1fe517044..3c24dc6fe 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 18/45] 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 1f5805f59..847c02afb 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 568f32269..8bb1aa807 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 000000000..8673fab60 --- /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 000000000..7addf8b21 --- /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 d3a4b0750..556e22dcf 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 3c24dc6fe..b783e5d1a 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 19/45] 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 f0bcc103f..aa9bf9be0 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 20/45] 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 1f09d1f52..0848663fd 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 aa9bf9be0..0be266130 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 baca954389e7eb15ce0515233a1521eba5b42458 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 4 Nov 2024 14:52:49 +0000 Subject: [PATCH 21/45] style: new display group variant, 'box_white' --- .../layout/display-group/display-group.component.scss | 8 +++++++- .../layout/display-group/display-group.component.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss index 210125a5b..2026550ad 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss @@ -45,7 +45,8 @@ .display-group-wrapper { &[data-variant~="box_gray"], &[data-variant~="box_primary"], - &[data-variant~="box_secondary"] { + &[data-variant~="box_secondary"], + &[data-variant~="box_white"] { margin-top: var(--regular-margin); padding: var(--regular-padding); border-radius: var(--ion-border-radius-secondary); @@ -68,4 +69,9 @@ --background-color: var(--ion-color-secondary-200); --border-color: var(--ion-color-secondary-500); } + + &[data-variant~="box_white"] { + --background-color: white; + --border-color: var(--ion-color-gray-300); + } } diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.ts b/src/app/shared/components/template/components/layout/display-group/display-group.component.ts index 2eadf8583..64f4d13ec 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.ts +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.ts @@ -4,7 +4,7 @@ import { getNumberParamFromTemplateRow, getStringParamFromTemplateRow } from ".. interface IDisplayGroupParams { /** TEMPLATE PARAMETER: "variant" */ - variant: "box_gray" | "box_primary" | "box_secondary" | "dashed_box"; + variant: "box_gray" | "box_primary" | "box_secondary" | "box_white" | "dashed_box"; /** TEMPLATE PARAMETER: "style". TODO: Various additional legacy styles, review and convert some to variants */ style: "form" | "default" | string | null; /** TEMPLATE PARAMETER: "offset". Add a custom bottom margin */ From 6827cdc01d7b2d9380f1f545eb8daddc84b249cd Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 4 Nov 2024 15:36:17 +0000 Subject: [PATCH 22/45] style: make border default gray for professional and plh_kids_kw themes --- .../components/accordion/accordion.component.scss | 2 +- .../tile-component/tile-component.component.scss | 2 +- src/theme/themes/default.scss | 8 ++++++-- src/theme/themes/early_family_math.scss | 5 ++++- src/theme/themes/pfr.scss | 5 ++++- src/theme/themes/plh_facilitator_mx.scss | 5 ++++- src/theme/themes/plh_kids_kw/_index.scss | 5 ++++- src/theme/themes/professional.scss | 7 +++++-- src/theme/themes/utils/generate-theme.scss | 4 ---- 9 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/app/shared/components/template/components/accordion/accordion.component.scss b/src/app/shared/components/template/components/accordion/accordion.component.scss index ba927b6af..85166bb46 100644 --- a/src/app/shared/components/template/components/accordion/accordion.component.scss +++ b/src/app/shared/components/template/components/accordion/accordion.component.scss @@ -1,7 +1,7 @@ // Create overlapping effect ion-accordion { background: white; - border: 1px solid var(--ion-color-primary); + border: var(--ion-border-standard); border-radius: 10px; margin-top: -12px; padding-top: 8px; diff --git a/src/app/shared/components/template/components/tile-component/tile-component.component.scss b/src/app/shared/components/template/components/tile-component/tile-component.component.scss index 383f025ba..d8eed1d10 100644 --- a/src/app/shared/components/template/components/tile-component/tile-component.component.scss +++ b/src/app/shared/components/template/components/tile-component/tile-component.component.scss @@ -50,7 +50,7 @@ $background-secondary-light: var( // Note - CC 2021-12-21 - Currently not in use in any templates, but keeping in case we want // to expose as a parameter option in the future .circle-border { - border: var(--ion-border-light-thicker); + border: var(--ion-border-standard); --border-radius: var(--ion-border-radius-rounded); border-radius: var(--ion-border-radius-rounded); } diff --git a/src/theme/themes/default.scss b/src/theme/themes/default.scss index 3e1e6e4da..0162282af 100644 --- a/src/theme/themes/default.scss +++ b/src/theme/themes/default.scss @@ -8,10 +8,14 @@ $color-secondary: hsl(31, 100%, 57%); // #fa9529 $page-background: null; - /** Authoring component overrides **/ + /** Global and component variables **/ $variable-overrides: ( - demo-variable: red, // an example variable for illustration purposes + demo-variable: red, + + // BORDERS + // ion-border-standard: 2px solid var(--ion-color-primary), + // ion-border-thin-standard: 1px solid var(--ion-color-primary), gradient-yellow-vertical: linear-gradient(175deg, var(--ion-color-yellow-200) 30%, var(--ion-color-yellow-500)), gradient-yellow-horizontal: diff --git a/src/theme/themes/early_family_math.scss b/src/theme/themes/early_family_math.scss index c2e3895a7..a9528623e 100644 --- a/src/theme/themes/early_family_math.scss +++ b/src/theme/themes/early_family_math.scss @@ -8,8 +8,11 @@ $color-secondary: #b53f94; $page-background: white; - /** Authoring component overrides **/ + /** Global and component variables **/ $variable-overrides: ( + // BORDERS + // ion-border-standard: 2px solid var(--ion-color-primary), + // ion-border-thin-standard: 1px solid var(--ion-color-primary), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/pfr.scss b/src/theme/themes/pfr.scss index 38649f354..2a978b890 100644 --- a/src/theme/themes/pfr.scss +++ b/src/theme/themes/pfr.scss @@ -9,8 +9,11 @@ $page-background: white; $green: #289b4c; - /** Authoring component overrides **/ + /** Global and component variables **/ $variable-overrides: ( + // BORDERS + // ion-border-standard: 2px solid var(--ion-color-primary), + // ion-border-thin-standard: 1px solid var(--ion-color-primary), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/plh_facilitator_mx.scss b/src/theme/themes/plh_facilitator_mx.scss index 9e823d425..bd06bf90b 100644 --- a/src/theme/themes/plh_facilitator_mx.scss +++ b/src/theme/themes/plh_facilitator_mx.scss @@ -8,8 +8,11 @@ $color-secondary: #ff5e00; $page-background: white; - /** Authoring component overrides **/ + /** Global and component variables **/ $variable-overrides: ( + // BORDERS + // ion-border-standard: 2px solid var(--ion-color-primary), + // ion-border-thin-standard: 1px solid var(--ion-color-primary), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/plh_kids_kw/_index.scss b/src/theme/themes/plh_kids_kw/_index.scss index 19fcc4fc7..8952da894 100644 --- a/src/theme/themes/plh_kids_kw/_index.scss +++ b/src/theme/themes/plh_kids_kw/_index.scss @@ -10,7 +10,7 @@ $color-secondary: hsl(199 100% 41.6%); // #0092D4 $page-background: white; - /** Authoring component overrides **/ + /** Global and component variables **/ $variable-overrides: ( ion-font-family: "Nunito", font-weight-standard: 500, @@ -48,6 +48,9 @@ buttons-full-width: 100%, buttons-full-height: 100%, + // BORDERS + ion-border-standard: 2px solid var(--ion-color-gray-200), + ion-border-thin-standard: 1px solid var(--ion-color-gray-200), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/professional.scss b/src/theme/themes/professional.scss index 2668bc2cb..7870de547 100644 --- a/src/theme/themes/professional.scss +++ b/src/theme/themes/professional.scss @@ -8,8 +8,11 @@ $color-secondary: hsl(31, 100%, 57%); // #fa9529 $page-background: white; - /** Authoring component overrides **/ + /** Global and component variables **/ $variable-overrides: ( + // BORDERS + ion-border-standard: 2px solid var(--ion-color-gray-200), + ion-border-thin-standard: 1px solid var(--ion-color-gray-200), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), @@ -48,7 +51,7 @@ ion-item-background: var(--ion-color-gray-light), task-progress-bar-color: var(--ion-color-green), // checkbox-background-color: white, - progress-path-line-background: var(--ion-color-gray-100), + progress-path-line-background: var(--ion-color-gray-100) ); @include utils.generateTheme($color-primary, $color-secondary, $page-background); @each $name, $value in $variable-overrides { diff --git a/src/theme/themes/utils/generate-theme.scss b/src/theme/themes/utils/generate-theme.scss index 2c42aee4b..bcbfadf8a 100644 --- a/src/theme/themes/utils/generate-theme.scss +++ b/src/theme/themes/utils/generate-theme.scss @@ -129,10 +129,6 @@ // BORDERS --ion-border-standard: 2px solid #{map.get($colorPalette, "color-primary")}; --ion-border-thin-standard: 1px solid #{map.get($colorPalette, "color-primary")}; - --ion-border-color-secondary: 2px solid #{map.get($colorPalette, "color-secondary")}; - --ion-border-light: 1px solid #{map.get($colorPalette, "light")}; - --ion-border-light-thicker: 2px solid #{map.get($colorPalette, "light")}; - --border-dashed: 2px dashed #{map.get($colorPalette, "color-primary")}; // GRADIENTS //Gradient direction From 6d00e305682b4d76cc5b07de14e2059f463790de Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 4 Nov 2024 16:53:17 +0000 Subject: [PATCH 23/45] chore: expose 'border-color-default' theme variable --- src/theme/deployment/components/_modal.scss | 2 +- src/theme/themes/default.scss | 5 +++-- src/theme/themes/early_family_math.scss | 5 +++-- src/theme/themes/pfr.scss | 5 +++-- src/theme/themes/plh_facilitator_mx.scss | 5 +++-- src/theme/themes/plh_kids_kw/_index.scss | 5 +++-- src/theme/themes/professional.scss | 5 +++-- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/theme/deployment/components/_modal.scss b/src/theme/deployment/components/_modal.scss index 05f0e55b5..4333731ba 100644 --- a/src/theme/deployment/components/_modal.scss +++ b/src/theme/deployment/components/_modal.scss @@ -1,7 +1,7 @@ ion-modal.combo-box-modal { --height: auto; --max-height: 90vh; - --border-color: var(--ion-border-standard); + --border-color: var(--border-color-default); --border-radius: var(--ion-border-radius-secondary); --border-style: solid; --border-width: 2px; diff --git a/src/theme/themes/default.scss b/src/theme/themes/default.scss index 0162282af..2ae9b1716 100644 --- a/src/theme/themes/default.scss +++ b/src/theme/themes/default.scss @@ -14,8 +14,9 @@ demo-variable: red, // BORDERS - // ion-border-standard: 2px solid var(--ion-color-primary), - // ion-border-thin-standard: 1px solid var(--ion-color-primary), + border-color-default: var(--ion-color-primary), + ion-border-standard: 2px solid var(--border-color-default), + ion-border-thin-standard: 1px solid var(--border-color-default), gradient-yellow-vertical: linear-gradient(175deg, var(--ion-color-yellow-200) 30%, var(--ion-color-yellow-500)), gradient-yellow-horizontal: diff --git a/src/theme/themes/early_family_math.scss b/src/theme/themes/early_family_math.scss index a9528623e..a12810a9b 100644 --- a/src/theme/themes/early_family_math.scss +++ b/src/theme/themes/early_family_math.scss @@ -11,8 +11,9 @@ /** Global and component variables **/ $variable-overrides: ( // BORDERS - // ion-border-standard: 2px solid var(--ion-color-primary), - // ion-border-thin-standard: 1px solid var(--ion-color-primary), + // border-color-default: var(--ion-color-primary), + // ion-border-standard: 2px solid var(--border-color-default), + // ion-border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/pfr.scss b/src/theme/themes/pfr.scss index 2a978b890..9c4ba7bd4 100644 --- a/src/theme/themes/pfr.scss +++ b/src/theme/themes/pfr.scss @@ -12,8 +12,9 @@ /** Global and component variables **/ $variable-overrides: ( // BORDERS - // ion-border-standard: 2px solid var(--ion-color-primary), - // ion-border-thin-standard: 1px solid var(--ion-color-primary), + // border-color-default: var(--ion-color-primary), + // ion-border-standard: 2px solid var(--border-color-default), + // ion-border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/plh_facilitator_mx.scss b/src/theme/themes/plh_facilitator_mx.scss index bd06bf90b..0ae26aa97 100644 --- a/src/theme/themes/plh_facilitator_mx.scss +++ b/src/theme/themes/plh_facilitator_mx.scss @@ -11,8 +11,9 @@ /** Global and component variables **/ $variable-overrides: ( // BORDERS - // ion-border-standard: 2px solid var(--ion-color-primary), - // ion-border-thin-standard: 1px solid var(--ion-color-primary), + // border-color-default: var(--ion-color-primary), + // ion-border-standard: 2px solid var(--border-color-default), + // ion-border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/plh_kids_kw/_index.scss b/src/theme/themes/plh_kids_kw/_index.scss index 8952da894..ea28e95d7 100644 --- a/src/theme/themes/plh_kids_kw/_index.scss +++ b/src/theme/themes/plh_kids_kw/_index.scss @@ -49,8 +49,9 @@ buttons-full-height: 100%, // BORDERS - ion-border-standard: 2px solid var(--ion-color-gray-200), - ion-border-thin-standard: 1px solid var(--ion-color-gray-200), + border-color-default: var(--ion-color-gray-200), + ion-border-standard: 2px solid var(--border-color-default), + ion-border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/professional.scss b/src/theme/themes/professional.scss index 7870de547..4f009a2a1 100644 --- a/src/theme/themes/professional.scss +++ b/src/theme/themes/professional.scss @@ -11,8 +11,9 @@ /** Global and component variables **/ $variable-overrides: ( // BORDERS - ion-border-standard: 2px solid var(--ion-color-gray-200), - ion-border-thin-standard: 1px solid var(--ion-color-gray-200), + border-color-default: var(--ion-color-gray-200), + ion-border-standard: 2px solid var(--border-color-default), + ion-border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), From 85f06f6b1a8ebfd8b5a982caba26b96462f7535e Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 4 Nov 2024 17:12:53 +0000 Subject: [PATCH 24/45] style: tweak display group white variatn border colour --- .../layout/display-group/display-group.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss index 2026550ad..0ebc6a67e 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss @@ -72,6 +72,6 @@ &[data-variant~="box_white"] { --background-color: white; - --border-color: var(--ion-color-gray-300); + --border-color: var(--ion-color-gray-200); } } From a486514e0acfa06abbc1a4b8b53cd2c368068634 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 4 Nov 2024 12:21:18 -0800 Subject: [PATCH 25/45] chore: deployment clone progress --- packages/scripts/src/tasks/providers/git.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/scripts/src/tasks/providers/git.ts b/packages/scripts/src/tasks/providers/git.ts index 18d461b52..2ae738bb2 100644 --- a/packages/scripts/src/tasks/providers/git.ts +++ b/packages/scripts/src/tasks/providers/git.ts @@ -2,12 +2,14 @@ import chalk from "chalk"; import fs from "fs-extra"; import path from "path"; import semver from "semver"; +import logUpdate from "log-update"; import simpleGit, { ResetMode } from "simple-git"; import type { SimpleGit, FileStatusResult } from "simple-git"; import { Project, SyntaxKind } from "ts-morph"; import { ActiveDeployment } from "../../commands/deployment/get"; import { Logger, logOutput, openUrl, promptOptions } from "../../utils"; import type { IDeploymentConfigJson } from "../../commands/deployment/common"; +import { PATHS } from "shared"; class GitProvider { private git: SimpleGit; @@ -20,8 +22,13 @@ class GitProvider { /** Access git clone methods directly independent of deployment */ public async cloneRepo(repoPath: string, localPath: string) { - const git = simpleGit(); - return git.clone(repoPath, localPath); + logUpdate(); + const git = simpleGit(PATHS.DEPLOYMENTS_PATH, { + progress: ({ processed, total }) => logUpdate(chalk.gray(`${processed}/${total}`)), + }); + const res = await git.clone(repoPath, localPath, ["--progress"]); + logUpdate.done(); + return res; } /** Pull latest content from remote repo into local branch. Attempt to resolve any conflicts */ From 9025a2500ce38191485cc264b25bee871a5daaa0 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 4 Nov 2024 12:32:47 -0800 Subject: [PATCH 26/45] chore: code tidying --- packages/scripts/src/tasks/providers/git.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/scripts/src/tasks/providers/git.ts b/packages/scripts/src/tasks/providers/git.ts index 2ae738bb2..abdbd4462 100644 --- a/packages/scripts/src/tasks/providers/git.ts +++ b/packages/scripts/src/tasks/providers/git.ts @@ -9,7 +9,7 @@ import { Project, SyntaxKind } from "ts-morph"; import { ActiveDeployment } from "../../commands/deployment/get"; import { Logger, logOutput, openUrl, promptOptions } from "../../utils"; import type { IDeploymentConfigJson } from "../../commands/deployment/common"; -import { PATHS } from "shared"; +import { pad, PATHS } from "shared"; class GitProvider { private git: SimpleGit; @@ -22,11 +22,12 @@ class GitProvider { /** Access git clone methods directly independent of deployment */ public async cloneRepo(repoPath: string, localPath: string) { - logUpdate(); + console.log("\n"); const git = simpleGit(PATHS.DEPLOYMENTS_PATH, { - progress: ({ processed, total }) => logUpdate(chalk.gray(`${processed}/${total}`)), + progress: ({ stage, progress, processed, total }) => + logUpdate(chalk.gray(`${stage} | ${pad(progress, 2)}% | ${processed}/${total}`)), }); - const res = await git.clone(repoPath, localPath, ["--progress"]); + const res = await git.clone(repoPath, localPath); logUpdate.done(); return res; } From de1d276417238cefff7662afe810181d3c785d73 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 4 Nov 2024 12:35:53 -0800 Subject: [PATCH 27/45] chore: fix linebreaks --- packages/scripts/src/commands/app-data/convert/index.ts | 2 +- .../commands/app-data/convert/utils/app-data-string.utils.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/index.ts b/packages/scripts/src/commands/app-data/convert/index.ts index 23e87d4be..f08096ecd 100644 --- a/packages/scripts/src/commands/app-data/convert/index.ts +++ b/packages/scripts/src/commands/app-data/convert/index.ts @@ -60,7 +60,7 @@ export default program */ export class AppDataConverter { /** Change version to invalidate all underlying caches */ - public version = 20231002.0; + public version = 20241104.0; public activeDeployment = ActiveDeployment.get(); diff --git a/packages/scripts/src/commands/app-data/convert/utils/app-data-string.utils.ts b/packages/scripts/src/commands/app-data/convert/utils/app-data-string.utils.ts index 4190d78f0..ed5c22410 100644 --- a/packages/scripts/src/commands/app-data/convert/utils/app-data-string.utils.ts +++ b/packages/scripts/src/commands/app-data/convert/utils/app-data-string.utils.ts @@ -97,6 +97,7 @@ export function extractDynamicDependencies(dynamicFields: FlowTypes.TemplateRow[ } // Standardise newline characters within a string (i.e. replace "\r\n" (CRLF) with "\n" (LF)) +// also replace any remaining \r with \n (https://github.com/IDEMSInternational/open-app-builder/issues/2499) export function standardiseNewlines(str: string) { - return str.replace(/\\r\\n/g, "\\n"); + return str.replace(/\\r\\n/g, "\\n").replace(/\\r/g, "\\n"); } From 569234b99d0a19553250a2f32aadde77d7328ef2 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 4 Nov 2024 14:11:45 -0800 Subject: [PATCH 28/45] chore: code tidying --- packages/data-models/flowTypes.ts | 2 +- src/app/shared/components/template/processors/item.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index b3fd415cc..3999bc9f2 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -362,7 +362,7 @@ export namespace FlowTypes { [key: string]: any; }; - const DYNAMIC_PREFIXES_COMPILER = ["gen", "row"] as const; + const DYNAMIC_PREFIXES_COMPILER = ["gen", "row", "default"] as const; const DYNAMIC_PREFIXES_RUNTIME = [ "local", diff --git a/src/app/shared/components/template/processors/item.ts b/src/app/shared/components/template/processors/item.ts index 55eb1e8e3..43a84821c 100644 --- a/src/app/shared/components/template/processors/item.ts +++ b/src/app/shared/components/template/processors/item.ts @@ -10,12 +10,12 @@ interface ITemplateRowWithItemContext extends FlowTypes.TemplateRow { export class ItemProcessor { constructor( - private dataList: FlowTypes.Data_listRow[] = [], + private dataListRows: FlowTypes.Data_listRow[] = [], private parameterList: any = {} ) {} public process(templateRows: any) { - const pipedData = this.pipeData(this.dataList, this.parameterList); + const pipedData = this.pipeData(this.dataListRows, this.parameterList); const itemTemplateRows = this.generateLoopItemRows(templateRows, pipedData); const parsedItemTemplatedRows = this.hackSetNestedName(itemTemplateRows); // Return both rows for rendering and list of itemData used (post pipe operations) From 32c5dde4410b2106daac70a57eab682b74ef1e6f Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 5 Nov 2024 11:59:13 +0000 Subject: [PATCH 29/45] refactor(text-bubble): better handling of padding around child text component --- .../text-bubble/text-bubble.component.scss | 15 +++++---------- .../template/components/text/text.component.html | 2 +- .../template/components/text/text.component.scss | 5 +++++ .../template/components/text/text.component.ts | 2 +- src/theme/_typography.scss | 3 ++- 5 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 src/app/shared/components/template/components/text/text.component.scss diff --git a/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss b/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss index cc6d6c672..18ff5272f 100644 --- a/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss +++ b/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss @@ -48,17 +48,12 @@ $imageSize: 64px; } .text-bubble { - padding: var(--small-padding) var(--large-padding); + padding: var(--large-padding) var(--large-padding); - // HACK: The `text` component already includes a substantial margin, but when the `text-bubble` - // component contains child rows rather than a `value` string, apply a margin around these rows. - plh-template-component { - &:first-child { - margin-top: var(--regular-padding); - } - &:last-child { - margin-bottom: var(--regular-padding); - } + // Override default margins of child text component: + // handle with padding on this parent element instead + plh-tmpl-text { + --margin-text: 0; } } diff --git a/src/app/shared/components/template/components/text/text.component.html b/src/app/shared/components/template/components/text/text.component.html index fc5752791..688686f45 100644 --- a/src/app/shared/components/template/components/text/text.component.html +++ b/src/app/shared/components/template/components/text/text.component.html @@ -1,6 +1,6 @@ @if (this._row.value) {
= {}; diff --git a/src/theme/_typography.scss b/src/theme/_typography.scss index 4ff308ecd..437ed3849 100644 --- a/src/theme/_typography.scss +++ b/src/theme/_typography.scss @@ -8,7 +8,8 @@ h3 { } p { - margin: 0.75em 0; + margin-top: var(--margin-text); + margin-bottom: var(--margin-text); } .standard { From 72d51ebefc41aa648822c710019a2f59dd653584 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 5 Nov 2024 12:01:02 +0000 Subject: [PATCH 30/45] chore: code tidy --- .../template/components/text-bubble/text-bubble.component.scss | 2 +- src/theme/_typography.scss | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss b/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss index 18ff5272f..ce2838f3c 100644 --- a/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss +++ b/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss @@ -48,7 +48,7 @@ $imageSize: 64px; } .text-bubble { - padding: var(--large-padding) var(--large-padding); + padding: var(--large-padding); // Override default margins of child text component: // handle with padding on this parent element instead diff --git a/src/theme/_typography.scss b/src/theme/_typography.scss index 437ed3849..ec3891fd1 100644 --- a/src/theme/_typography.scss +++ b/src/theme/_typography.scss @@ -8,8 +8,7 @@ h3 { } p { - margin-top: var(--margin-text); - margin-bottom: var(--margin-text); + margin: var(--margin-text, 0.75em) 0; } .standard { From 213a88ac64fab1e3a024721b718e73005d75543a Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 5 Nov 2024 12:17:58 +0000 Subject: [PATCH 31/45] chore: rename theme css variable '--ion-border-*' -> '--border-*' --- .../components/accordion/accordion.component.scss | 2 +- .../template/components/audio/audio.component.scss | 8 ++++---- .../combo-box-modal/combo-box-modal.component.scss | 4 ++-- .../components/combo-box/combo-box.component.scss | 2 +- .../accordion-section/accordion-section.component.scss | 4 ++-- .../layout/display-grid/display-grid.component.scss | 2 +- .../number-selector/number-selector.component.scss | 4 ++-- .../radio-button-grid/radio-button-grid.component.scss | 2 +- .../components/radio-group/radio-group.component.scss | 4 ++-- .../template/components/slider/slider.component.scss | 2 +- .../components/text-area/text-area.component.scss | 2 +- .../template/components/text-box/text-box.component.scss | 2 +- .../tile-component/tile-component.component.scss | 4 ++-- .../template/components/timer/timer.component.scss | 2 +- src/global.scss | 4 ++-- src/theme/deployment/components/_display-group.scss | 2 +- src/theme/themes/default.scss | 4 ++-- src/theme/themes/early_family_math.scss | 4 ++-- src/theme/themes/pfr.scss | 4 ++-- src/theme/themes/plh_facilitator_mx.scss | 4 ++-- src/theme/themes/plh_kids_kw/_index.scss | 4 ++-- src/theme/themes/professional.scss | 4 ++-- src/theme/themes/utils/generate-theme.scss | 4 ++-- 23 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/app/shared/components/template/components/accordion/accordion.component.scss b/src/app/shared/components/template/components/accordion/accordion.component.scss index 85166bb46..c07861760 100644 --- a/src/app/shared/components/template/components/accordion/accordion.component.scss +++ b/src/app/shared/components/template/components/accordion/accordion.component.scss @@ -1,7 +1,7 @@ // Create overlapping effect ion-accordion { background: white; - border: var(--ion-border-standard); + border: var(--border-standard); border-radius: 10px; margin-top: -12px; padding-top: 8px; diff --git a/src/app/shared/components/template/components/audio/audio.component.scss b/src/app/shared/components/template/components/audio/audio.component.scss index 6746dec7d..742224441 100644 --- a/src/app/shared/components/template/components/audio/audio.component.scss +++ b/src/app/shared/components/template/components/audio/audio.component.scss @@ -4,7 +4,7 @@ $control-background: var(--audio-control-background, var(--ion-color-primary-500 .container-player[data-variant~="compact"] { background: white; - border: var(--ion-border-standard); + border: var(--border-standard); box-sizing: border-box; box-shadow: var(--ion-default-box-shadow); border-radius: var(--ion-border-radius-standard); @@ -87,7 +87,7 @@ $control-background: var(--audio-control-background, var(--ion-color-primary-500 .container-player[data-variant~="large"] { background: var(--ion-color-primary-contrast); - border: var(--ion-border-standard); + border: var(--border-standard); box-sizing: border-box; box-shadow: var(--ion-default-box-shadow); border-radius: var(--ion-border-radius-standard); @@ -134,7 +134,7 @@ $control-background: var(--audio-control-background, var(--ion-color-primary-500 } ion-range::part(bar) { - border: var(--ion-border-thin-standard); + border: var(--border-thin-standard); height: var(--audio-bar-height); } @@ -187,4 +187,4 @@ $control-background: var(--audio-control-background, var(--ion-color-primary-500 } .disabled { pointer-events: none; -} \ No newline at end of file +} diff --git a/src/app/shared/components/template/components/combo-box/combo-box-modal/combo-box-modal.component.scss b/src/app/shared/components/template/components/combo-box/combo-box-modal/combo-box-modal.component.scss index c34e86d0f..cc95b5381 100644 --- a/src/app/shared/components/template/components/combo-box/combo-box-modal/combo-box-modal.component.scss +++ b/src/app/shared/components/template/components/combo-box/combo-box-modal/combo-box-modal.component.scss @@ -13,7 +13,7 @@ $background-with-value: var( padding: 0; .text-box-input { background: var(--ion-color-primary-contrast); - border: var(--ion-border-standard); + border: var(--border-standard); border-radius: var(--ion-border-radius-secondary); width: 100%; min-height: 55px; @@ -55,7 +55,7 @@ $background-with-value: var( min-height: 55px; width: 100%; background: var(--ion-color-primary-contrast); - border: var(--ion-border-standard); + border: var(--border-standard); border-radius: var(--ion-border-radius-secondary); box-shadow: var(--ion-default-box-shadow); @include mixins.flex-centered; diff --git a/src/app/shared/components/template/components/combo-box/combo-box.component.scss b/src/app/shared/components/template/components/combo-box/combo-box.component.scss index f58191545..ccf308eb9 100644 --- a/src/app/shared/components/template/components/combo-box/combo-box.component.scss +++ b/src/app/shared/components/template/components/combo-box/combo-box.component.scss @@ -8,7 +8,7 @@ $background-with-value: var( .open-combobox { outline: none; width: 100%; - border: var(--ion-border-standard); + border: var(--border-standard); box-sizing: border-box; filter: drop-shadow(var(--ion-default-box-shadow)); border-radius: var(--ion-border-radius-secondary); diff --git a/src/app/shared/components/template/components/layout/accordion-section/accordion-section.component.scss b/src/app/shared/components/template/components/layout/accordion-section/accordion-section.component.scss index 29617ede1..049d630d2 100644 --- a/src/app/shared/components/template/components/layout/accordion-section/accordion-section.component.scss +++ b/src/app/shared/components/template/components/layout/accordion-section/accordion-section.component.scss @@ -14,7 +14,7 @@ $background-highlight: var( .accordion-status { @include mixins.medium-square; border-radius: var(--ion-border-radius-rounded); - border: var(--ion-border-thin-standard); + border: var(--border-thin-standard); padding: var(--tiny-padding); background: var(--ion-color-primary-contrast); display: flex; @@ -43,7 +43,7 @@ $background-highlight: var( border-radius: var(--ion-border-radius-standard); overflow: hidden; padding: var(--small-padding); - border: var(--ion-border-thin-standard); + border: var(--border-thin-standard); transition: max-height 0.4s; overflow-y: hidden; &.completed { diff --git a/src/app/shared/components/template/components/layout/display-grid/display-grid.component.scss b/src/app/shared/components/template/components/layout/display-grid/display-grid.component.scss index 0920063e8..66e19cae0 100644 --- a/src/app/shared/components/template/components/layout/display-grid/display-grid.component.scss +++ b/src/app/shared/components/template/components/layout/display-grid/display-grid.component.scss @@ -1,4 +1,4 @@ -$border: var(--ion-border-standard); +$border: var(-border-standard); $border-radius: var(--ion-border-radius-standard, 15px); .display-grid { diff --git a/src/app/shared/components/template/components/number-selector/number-selector.component.scss b/src/app/shared/components/template/components/number-selector/number-selector.component.scss index 87798059a..3375da2c6 100644 --- a/src/app/shared/components/template/components/number-selector/number-selector.component.scss +++ b/src/app/shared/components/template/components/number-selector/number-selector.component.scss @@ -2,7 +2,7 @@ .container { width: calc(100% - 10px); background: var(--ion-color-primary-contrast); - border: var(--ion-border-standard); + border: var(--border-standard); box-shadow: var(--ion-default-box-shadow); border-radius: var(--ion-border-radius-secondary); @include mixins.flex-space-between; @@ -24,7 +24,7 @@ --background: transparent; --box-shadow: none; border-radius: var(--ion-border-radius-rounded); - border: var(--ion-border-standard); + border: var(--border-standard); @include mixins.tiny-square; @include mixins.flex-centered; .line { diff --git a/src/app/shared/components/template/components/radio-button-grid/radio-button-grid.component.scss b/src/app/shared/components/template/components/radio-button-grid/radio-button-grid.component.scss index 2dbb8e94a..be668e2b7 100644 --- a/src/app/shared/components/template/components/radio-button-grid/radio-button-grid.component.scss +++ b/src/app/shared/components/template/components/radio-button-grid/radio-button-grid.component.scss @@ -4,7 +4,7 @@ $background-selected: var( --radio-group-background-selected, var(--gradient-primary-light-vertical) ); -$border: var(--ion-border-standard); +$border: var(--border-standard); $border-radius: var(--ion-border-radius-standard); $text-size: var(--radio-button-font-size, 1.25rem); $text-color: var(--radio-button-font-color, var(--ion-color-primary)); diff --git a/src/app/shared/components/template/components/radio-group/radio-group.component.scss b/src/app/shared/components/template/components/radio-group/radio-group.component.scss index 6ebe3ff50..89de72bb1 100644 --- a/src/app/shared/components/template/components/radio-group/radio-group.component.scss +++ b/src/app/shared/components/template/components/radio-group/radio-group.component.scss @@ -96,7 +96,7 @@ img { } .checked { transition: 0.3s linear; - border: var(--ion-border-standard); + border: var(--border-standard); box-shadow: var(--ion-default-box-shadow); } } @@ -110,7 +110,7 @@ img { max-width: fit-content; min-width: 100%; box-shadow: var(--ion-default-box-shadow); - border: var(--ion-border-standard); + border: var(--border-standard); color: var(--ion-color-primary); background: var(--ion-color-primary-contrast); } diff --git a/src/app/shared/components/template/components/slider/slider.component.scss b/src/app/shared/components/template/components/slider/slider.component.scss index 2ea1c660e..3fe5c8a58 100644 --- a/src/app/shared/components/template/components/slider/slider.component.scss +++ b/src/app/shared/components/template/components/slider/slider.component.scss @@ -6,7 +6,7 @@ $ui-color: var(--slider-ui-color, var(--ion-color-primary-700)); min-width: 100%; padding: var(--regular-padding) 0 var(--regular-padding) var(--regular-padding); background: var(--ion-color-primary-contrast); - border: var(--ion-border-standard); + border: var(--border-standard); box-sizing: border-box; box-shadow: var(--ion-default-box-shadow); border-radius: var(--ion-border-radius-secondary); diff --git a/src/app/shared/components/template/components/text-area/text-area.component.scss b/src/app/shared/components/template/components/text-area/text-area.component.scss index 91c712840..4154e0d4b 100644 --- a/src/app/shared/components/template/components/text-area/text-area.component.scss +++ b/src/app/shared/components/template/components/text-area/text-area.component.scss @@ -1,7 +1,7 @@ .wrapper { .text_area { background: var(--ion-color-primary-contrast); - border: var(--ion-border-standard); + border: var(--border-standard); font-size: var(--text-box-font-size); color: var(--ion-color-primary); font-weight: var(--font-weight-bold); diff --git a/src/app/shared/components/template/components/text-box/text-box.component.scss b/src/app/shared/components/template/components/text-box/text-box.component.scss index 1d278aa41..99e2b173f 100644 --- a/src/app/shared/components/template/components/text-box/text-box.component.scss +++ b/src/app/shared/components/template/components/text-box/text-box.component.scss @@ -1,7 +1,7 @@ .wrapper { .text-box-input { background: var(--ion-color-primary-contrast); - border: var(--ion-border-standard); + border: var(--border-standard); width: 100%; height: var(--text-box-height); font-size: var(--text-box-font-size); diff --git a/src/app/shared/components/template/components/tile-component/tile-component.component.scss b/src/app/shared/components/template/components/tile-component/tile-component.component.scss index d8eed1d10..68fc6629b 100644 --- a/src/app/shared/components/template/components/tile-component/tile-component.component.scss +++ b/src/app/shared/components/template/components/tile-component/tile-component.component.scss @@ -50,7 +50,7 @@ $background-secondary-light: var( // Note - CC 2021-12-21 - Currently not in use in any templates, but keeping in case we want // to expose as a parameter option in the future .circle-border { - border: var(--ion-border-standard); + border: var(--border-standard); --border-radius: var(--ion-border-radius-rounded); border-radius: var(--ion-border-radius-rounded); } @@ -75,7 +75,7 @@ $background-secondary-light: var( width: 8rem; min-height: 7rem; border-radius: var(--ion-border-radius-standard); - border: var(--ion-border-thin-standard); + border: var(--border-thin-standard); background: var(--ion-color-primary-contrast); text-align: center; padding: var(--tiny-padding); diff --git a/src/app/shared/components/template/components/timer/timer.component.scss b/src/app/shared/components/template/components/timer/timer.component.scss index 40e0e8a95..a25ab1f42 100644 --- a/src/app/shared/components/template/components/timer/timer.component.scss +++ b/src/app/shared/components/template/components/timer/timer.component.scss @@ -62,7 +62,7 @@ $button-background: var(--timer-button-background, var(--ion-color-primary-500)) width: 100%; background-color: var(--ion-color-primary-contrast); box-sizing: border-box; - border: var(--ion-border-standard); + border: var(--border-standard); box-shadow: var(--ion-default-box-shadow); border-radius: var(--ion-border-radius-secondary); diff --git a/src/global.scss b/src/global.scss index c36436084..98277740a 100644 --- a/src/global.scss +++ b/src/global.scss @@ -160,7 +160,7 @@ $tour-next-button-background: var( transform: translate(-50%, -5%); } .introjs-bullets ul li a { - border: var(--ion-border-thin-standard); + border: var(--border-thin-standard); background: var(--ion-color-primary-contrast); } .introjs-bullets ul li a.active { @@ -169,7 +169,7 @@ $tour-next-button-background: var( } } .buttonClass { - border: var(--ion-border-thin-standard); + border: var(--border-thin-standard); padding: 10px 15px; border-radius: var(--ion-border-radius-small); box-shadow: var(--ion-default-box-shadow); diff --git a/src/theme/deployment/components/_display-group.scss b/src/theme/deployment/components/_display-group.scss index 205b0f7ed..af52c07a6 100644 --- a/src/theme/deployment/components/_display-group.scss +++ b/src/theme/deployment/components/_display-group.scss @@ -224,5 +224,5 @@ plh-tmpl-display-group .display-group-wrapper[data-param-style~="parent_point"] .display-group-wrapper[data-param-style~="white_box"] { @include essential-tool; background: var(--ion-color-primary-contrast); - border: var(--ion-border-standard); + border: var(--border-standard); } diff --git a/src/theme/themes/default.scss b/src/theme/themes/default.scss index 2ae9b1716..8d0994f16 100644 --- a/src/theme/themes/default.scss +++ b/src/theme/themes/default.scss @@ -15,8 +15,8 @@ // BORDERS border-color-default: var(--ion-color-primary), - ion-border-standard: 2px solid var(--border-color-default), - ion-border-thin-standard: 1px solid var(--border-color-default), + border-standard: 2px solid var(--border-color-default), + border-thin-standard: 1px solid var(--border-color-default), gradient-yellow-vertical: linear-gradient(175deg, var(--ion-color-yellow-200) 30%, var(--ion-color-yellow-500)), gradient-yellow-horizontal: diff --git a/src/theme/themes/early_family_math.scss b/src/theme/themes/early_family_math.scss index a12810a9b..a54654d5f 100644 --- a/src/theme/themes/early_family_math.scss +++ b/src/theme/themes/early_family_math.scss @@ -12,8 +12,8 @@ $variable-overrides: ( // BORDERS // border-color-default: var(--ion-color-primary), - // ion-border-standard: 2px solid var(--border-color-default), - // ion-border-thin-standard: 1px solid var(--border-color-default), + // border-standard: 2px solid var(--border-color-default), + // border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/pfr.scss b/src/theme/themes/pfr.scss index 9c4ba7bd4..3d80b8027 100644 --- a/src/theme/themes/pfr.scss +++ b/src/theme/themes/pfr.scss @@ -13,8 +13,8 @@ $variable-overrides: ( // BORDERS // border-color-default: var(--ion-color-primary), - // ion-border-standard: 2px solid var(--border-color-default), - // ion-border-thin-standard: 1px solid var(--border-color-default), + // border-standard: 2px solid var(--border-color-default), + // border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/plh_facilitator_mx.scss b/src/theme/themes/plh_facilitator_mx.scss index 0ae26aa97..025e9e41c 100644 --- a/src/theme/themes/plh_facilitator_mx.scss +++ b/src/theme/themes/plh_facilitator_mx.scss @@ -12,8 +12,8 @@ $variable-overrides: ( // BORDERS // border-color-default: var(--ion-color-primary), - // ion-border-standard: 2px solid var(--border-color-default), - // ion-border-thin-standard: 1px solid var(--border-color-default), + // border-standard: 2px solid var(--border-color-default), + // border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/plh_kids_kw/_index.scss b/src/theme/themes/plh_kids_kw/_index.scss index ea28e95d7..efd80b16e 100644 --- a/src/theme/themes/plh_kids_kw/_index.scss +++ b/src/theme/themes/plh_kids_kw/_index.scss @@ -50,8 +50,8 @@ // BORDERS border-color-default: var(--ion-color-gray-200), - ion-border-standard: 2px solid var(--border-color-default), - ion-border-thin-standard: 1px solid var(--border-color-default), + border-standard: 2px solid var(--border-color-default), + border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/professional.scss b/src/theme/themes/professional.scss index 4f009a2a1..0842d9d43 100644 --- a/src/theme/themes/professional.scss +++ b/src/theme/themes/professional.scss @@ -12,8 +12,8 @@ $variable-overrides: ( // BORDERS border-color-default: var(--ion-color-gray-200), - ion-border-standard: 2px solid var(--border-color-default), - ion-border-thin-standard: 1px solid var(--border-color-default), + border-standard: 2px solid var(--border-color-default), + border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), button-background-option: var(--ion-color-primary-800), diff --git a/src/theme/themes/utils/generate-theme.scss b/src/theme/themes/utils/generate-theme.scss index bcbfadf8a..ebfe24364 100644 --- a/src/theme/themes/utils/generate-theme.scss +++ b/src/theme/themes/utils/generate-theme.scss @@ -127,8 +127,8 @@ --ion-item-background: #{map.get($colorPalette, "color-primary-100")}; // BORDERS - --ion-border-standard: 2px solid #{map.get($colorPalette, "color-primary")}; - --ion-border-thin-standard: 1px solid #{map.get($colorPalette, "color-primary")}; + --border-standard: 2px solid #{map.get($colorPalette, "color-primary")}; + --border-thin-standard: 1px solid #{map.get($colorPalette, "color-primary")}; // GRADIENTS //Gradient direction From 40791dd6b1ba6d8eb151049eeb775143d532e18e Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 5 Nov 2024 12:33:44 +0000 Subject: [PATCH 32/45] chore: expose '--border-width-default' theme variable; make border-width-default 1px for professional and plh_kids_kw themes --- src/theme/deployment/components/_modal.scss | 2 +- src/theme/themes/default.scss | 3 ++- src/theme/themes/early_family_math.scss | 3 ++- src/theme/themes/pfr.scss | 3 ++- src/theme/themes/plh_facilitator_mx.scss | 3 ++- src/theme/themes/plh_kids_kw/_index.scss | 3 ++- src/theme/themes/professional.scss | 3 ++- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/theme/deployment/components/_modal.scss b/src/theme/deployment/components/_modal.scss index 4333731ba..27a5978bc 100644 --- a/src/theme/deployment/components/_modal.scss +++ b/src/theme/deployment/components/_modal.scss @@ -4,7 +4,7 @@ ion-modal.combo-box-modal { --border-color: var(--border-color-default); --border-radius: var(--ion-border-radius-secondary); --border-style: solid; - --border-width: 2px; + --border-width: var(--border-width-default); --background: var(--ion-color-primary-contrast); --max-width: 90vw; } diff --git a/src/theme/themes/default.scss b/src/theme/themes/default.scss index 8d0994f16..c062eed09 100644 --- a/src/theme/themes/default.scss +++ b/src/theme/themes/default.scss @@ -15,7 +15,8 @@ // BORDERS border-color-default: var(--ion-color-primary), - border-standard: 2px solid var(--border-color-default), + border-width-default: 2px, + border-standard: var(--border-width-default) solid var(--border-color-default), border-thin-standard: 1px solid var(--border-color-default), gradient-yellow-vertical: linear-gradient(175deg, var(--ion-color-yellow-200) 30%, var(--ion-color-yellow-500)), diff --git a/src/theme/themes/early_family_math.scss b/src/theme/themes/early_family_math.scss index a54654d5f..0f5b9cdd0 100644 --- a/src/theme/themes/early_family_math.scss +++ b/src/theme/themes/early_family_math.scss @@ -12,7 +12,8 @@ $variable-overrides: ( // BORDERS // border-color-default: var(--ion-color-primary), - // border-standard: 2px solid var(--border-color-default), + border-width-default: 2px, + // border-standard: var(--border-width-default) solid var(--border-color-default), // border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), diff --git a/src/theme/themes/pfr.scss b/src/theme/themes/pfr.scss index 3d80b8027..257efb3f2 100644 --- a/src/theme/themes/pfr.scss +++ b/src/theme/themes/pfr.scss @@ -13,7 +13,8 @@ $variable-overrides: ( // BORDERS // border-color-default: var(--ion-color-primary), - // border-standard: 2px solid var(--border-color-default), + border-width-default: 2px, + // border-standard: var(--border-width-default) solid var(--border-color-default), // border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), diff --git a/src/theme/themes/plh_facilitator_mx.scss b/src/theme/themes/plh_facilitator_mx.scss index 025e9e41c..3d974150b 100644 --- a/src/theme/themes/plh_facilitator_mx.scss +++ b/src/theme/themes/plh_facilitator_mx.scss @@ -12,7 +12,8 @@ $variable-overrides: ( // BORDERS // border-color-default: var(--ion-color-primary), - // border-standard: 2px solid var(--border-color-default), + border-width-default: 2px, + // border-standard: var(--border-width-default) solid var(--border-color-default), // border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), diff --git a/src/theme/themes/plh_kids_kw/_index.scss b/src/theme/themes/plh_kids_kw/_index.scss index efd80b16e..e695bba0e 100644 --- a/src/theme/themes/plh_kids_kw/_index.scss +++ b/src/theme/themes/plh_kids_kw/_index.scss @@ -50,7 +50,8 @@ // BORDERS border-color-default: var(--ion-color-gray-200), - border-standard: 2px solid var(--border-color-default), + border-width-default: 1px, + border-standard: var(--border-width-default) solid var(--border-color-default), border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), diff --git a/src/theme/themes/professional.scss b/src/theme/themes/professional.scss index 0842d9d43..3fc1addd1 100644 --- a/src/theme/themes/professional.scss +++ b/src/theme/themes/professional.scss @@ -12,7 +12,8 @@ $variable-overrides: ( // BORDERS border-color-default: var(--ion-color-gray-200), - border-standard: 2px solid var(--border-color-default), + border-width-default: 1px, + border-standard: var(--border-width-default) solid var(--border-color-default), border-thin-standard: 1px solid var(--border-color-default), button-background-primary: var(--ion-color-primary-500), button-background-secondary: var(--ion-color-secondary), From f0087ad58bd440f6ea739f983fa2ea2861dcffc7 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 5 Nov 2024 13:08:23 +0000 Subject: [PATCH 33/45] chore: fix typo --- .../components/layout/display-grid/display-grid.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/layout/display-grid/display-grid.component.scss b/src/app/shared/components/template/components/layout/display-grid/display-grid.component.scss index 66e19cae0..c981de79c 100644 --- a/src/app/shared/components/template/components/layout/display-grid/display-grid.component.scss +++ b/src/app/shared/components/template/components/layout/display-grid/display-grid.component.scss @@ -1,4 +1,4 @@ -$border: var(-border-standard); +$border: var(--border-standard); $border-radius: var(--ion-border-radius-standard, 15px); .display-grid { From e47f9c48b1ae37f20f2de310b41ff6f0bc1b1955 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 5 Nov 2024 15:45:42 +0000 Subject: [PATCH 34/45] 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 35/45] 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 0848663fd..03c698138 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 847c02afb..ea07eff9e 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 8bb1aa807..568f32269 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 7addf8b21..778e0860d 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 556e22dcf..af2b72244 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 b783e5d1a..1fe517044 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 0be266130..7549f1599 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 36/45] 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 778e0860d..db84cc655 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 37/45] 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 63f2a2e76..6c88a5fd2 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 ea07eff9e..16f3a57bf 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 38/45] 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 7549f1599..65505a319 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 39/45] 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 3999bc9f2..51f8f6fab 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 db84cc655..80d450cfe 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 f9854ab92..22b55f7dc 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 65505a319..5c7828ce5 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 000000000..9746020e0 --- /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 40/45] 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 5c7828ce5..eb399ebb4 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 }); From b2d3105779d8128b28c3e9ee943c8f6c1d6bc08f Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 7 Nov 2024 13:01:01 +0000 Subject: [PATCH 41/45] style: use theme variables for styling display group box variants' border --- .../layout/display-group/display-group.component.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss index 0ebc6a67e..79b2c22cd 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss @@ -52,7 +52,8 @@ border-radius: var(--ion-border-radius-secondary); flex: 1; background-color: var(--background-color, transparent); - border: 2px solid var(--border-color, transparent); + border: var(--border-standard); + border-color: var(--border-color); } &[data-variant~="box_gray"] { @@ -72,6 +73,6 @@ &[data-variant~="box_white"] { --background-color: white; - --border-color: var(--ion-color-gray-200); + --border-color: var(--border-color-default, var(--ion-color-primary)); } } From d597e3ea7e45f417109902f473850227aef461fb Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 7 Nov 2024 19:35:45 +0000 Subject: [PATCH 42/45] ci: bump version to 0.16.38 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2692461ab..8f751d1de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "0.16.37", + "version": "0.16.38", "author": "IDEMS International", "license": "See LICENSE", "homepage": "https://idems.international/", From 840da1c50211dcc47430ac37823f2c6a4c186061 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 7 Nov 2024 18:35:46 -0800 Subject: [PATCH 43/45] feat: full screen template screen orientation tracking --- .../template/services/template-metadata.service.ts | 3 ++- .../components/template/services/template.service.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 2 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 80d450cfe..a62b7ba52 100644 --- a/src/app/shared/components/template/services/template-metadata.service.ts +++ b/src/app/shared/components/template/services/template-metadata.service.ts @@ -33,7 +33,8 @@ export class TemplateMetadataService extends SyncServiceBase { // subscribe to template name changes and load corresponding template parameter list on change effect( async () => { - const templateName = this.templateName(); + // use full screen popup template if exists, or current template page if not + const templateName = this.templateService.standaloneTemplateName() || this.templateName(); const parameterList = templateName ? await this.templateService.getTemplateMetadata(templateName) : {}; diff --git a/src/app/shared/components/template/services/template.service.ts b/src/app/shared/components/template/services/template.service.ts index 22b55f7dc..8beeb78a0 100644 --- a/src/app/shared/components/template/services/template.service.ts +++ b/src/app/shared/components/template/services/template.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import { Injectable, signal } from "@angular/core"; import { LocalStorageService } from "src/app/shared/services/local-storage/local-storage.service"; import { AppDataService } from "src/app/shared/services/data/app-data.service"; import { DbService } from "src/app/shared/services/db/db.service"; @@ -19,6 +19,9 @@ import { SyncServiceBase } from "src/app/shared/services/syncService.base"; providedIn: "root", }) export class TemplateService extends SyncServiceBase { + /** Name of any template running in fullscreen standalone mode */ + public standaloneTemplateName = signal(undefined); + constructor( private localStorageService: LocalStorageService, private appDataService: AppDataService, @@ -80,6 +83,11 @@ export class TemplateService extends SyncServiceBase { componentProps: { props }, }); await modal.present(); + + // track standalone template name so that template-meta.service can update loaded template params + this.standaloneTemplateName.set(templatename); + modal.onDidDismiss().then(() => this.standaloneTemplateName.set(undefined)); + let dismissData: { emit_value?: string; emit_data?: any } = {}; if (props.waitForDismiss) { const { data } = await modal.onDidDismiss(); From d19042cd4f8b2356b9e19ce4f78805653a2779f8 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 9 Nov 2024 11:17:14 -0800 Subject: [PATCH 44/45] fix: seo service config --- src/app/shared/services/seo/seo.service.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/shared/services/seo/seo.service.ts b/src/app/shared/services/seo/seo.service.ts index 6e9b254d1..81f00f1cb 100644 --- a/src/app/shared/services/seo/seo.service.ts +++ b/src/app/shared/services/seo/seo.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@angular/core"; import { SyncServiceBase } from "../syncService.base"; import { DeploymentService } from "../deployment/deployment.service"; +import { AppConfigService } from "../app-config/app-config.service"; interface ISEOMeta { title: string; @@ -21,7 +22,10 @@ type IMetaName = providedIn: "root", }) export class SeoService extends SyncServiceBase { - constructor(private deploymentService: DeploymentService) { + constructor( + private deploymentService: DeploymentService, + private appConfigService: AppConfigService + ) { super("SEO Service"); // call after init to apply defaults this.updateMeta({}); @@ -65,12 +69,14 @@ export class SeoService extends SyncServiceBase { private getDefaultSEOTags(): ISEOMeta { const PUBLIC_URL = location.origin; let faviconUrl = `${PUBLIC_URL}/assets/icon/favicon.svg`; - const { web, app_config } = this.deploymentService.config; + const { web } = this.deploymentService.config; + const { title } = this.appConfigService.appConfig().APP_HEADER_DEFAULTS; + if (web?.favicon_asset) { faviconUrl = `${PUBLIC_URL}/assets/app_data/assets/${web.favicon_asset}`; } return { - title: app_config.APP_HEADER_DEFAULTS.title, + title, description: "", faviconUrl, imageUrl: ``, From b2dbc9424d0b4a40f3db042844dd1f6a1c4cd8e9 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 9 Nov 2024 11:26:02 -0800 Subject: [PATCH 45/45] chore: update local demo sheet --- .idems_app/deployments/local/sheets/demo.xlsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.idems_app/deployments/local/sheets/demo.xlsx b/.idems_app/deployments/local/sheets/demo.xlsx index 203e4162f..6f86a46fa 100644 --- a/.idems_app/deployments/local/sheets/demo.xlsx +++ b/.idems_app/deployments/local/sheets/demo.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d7204d678b3ca8780caef25ae5f1a01de1fbdd724eb47624664864babc349b1 -size 51549 +oid sha256:fe1560492d104a68a3f66c825b894adf572979366b2464bceb1b29ddc34cb4d4 +size 47357