Skip to content

Commit

Permalink
Merge pull request #2501 from IDEMSInternational/feat/landscape-mode
Browse files Browse the repository at this point in the history
Feat: landscape mode
  • Loading branch information
chrismclarke authored Nov 7, 2024
2 parents a01830c + 28188cc commit cdff496
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 34 deletions.
1 change: 0 additions & 1 deletion packages/data-models/flowTypes.ts
Original file line number Diff line number Diff line change
@@ -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";

/*********************************************************************************************
Expand Down
3 changes: 3 additions & 0 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -251,6 +253,7 @@ export class AppComponent {
this.feedbackService,
this.shareService,
this.fileManagerService,
this.templateMetadataService,
this.screenOrientationService,
],
deferred: [this.analyticsService],
Expand Down
18 changes: 8 additions & 10 deletions src/app/feature/template/template.page.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
<ion-content [scrollEvents]="shouldEmitScrollEvents">
<plh-template-container
*ngIf="templateName"
[templatename]="templateName"
></plh-template-container>
<div *ngIf="!templateName" class="ion-padding">
@if (templateName) {
<plh-template-container [templatename]="templateName"></plh-template-container>
} @else {
<div class="ion-padding">
<h3>Select a Template</h3>
<ion-searchbar [(ngModel)]="filterTerm" (ionInput)="search()"></ion-searchbar>
<ion-list>
<ion-item
*ngFor="let template of filteredTemplates; trackBy: trackByFn"
[routerLink]="template.flow_name"
>{{template.flow_name}}</ion-item
>
@for(template of filteredTemplates; track $index) {
<ion-item [routerLink]="template.flow_name">{{template.flow_name}}</ion-item>
}
</ion-list>
</div>
}
</ion-content>
17 changes: 10 additions & 7 deletions src/app/feature/template/template.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class TemplatePage implements OnInit, OnDestroy {
filteredTemplates: FlowTypes.FlowTypeBase[] = [];
appConfigChanges$: Subscription;
shouldEmitScrollEvents: boolean = false;

constructor(
private route: ActivatedRoute,
private appDataService: AppDataService,
Expand All @@ -26,21 +27,23 @@ 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) {
this.listTemplates();
}
this.subscribeToAppConfigChanges();
}

search() {
this.allTemplates = this.allTemplates;
public search() {
this.filteredTemplates = this.allTemplates.filter(
(i) => i.flow_name.toLocaleLowerCase().indexOf(this.filterTerm.toLowerCase()) > -1
);
}

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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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 { 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,
* i.e. parameters authored through the template's parameter_list in a contents list
*/
@Injectable({
providedIn: "root",
})
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<string | undefined>(() => this.snapshot().params.templateName);

/** List of parameterList provided with current template */
public parameterList = signal<FlowTypes.Template["parameter_list"]>({}, { equal: isEqual });

constructor(
private templateService: TemplateService,
private router: Router
) {
super("TemplateMetadata");

// 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 }
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,69 @@
import { Injectable } from "@angular/core";
import { SyncServiceBase } from "../syncService.base";
import { ScreenOrientation, OrientationLockType } from "@capacitor/screen-orientation";
import { effect, Injectable } from "@angular/core";
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";

const ORIENTATION_TYPES: OrientationLockType[] = ["portrait", "landscape"];
/** List of possible orientations provided by authors */
const SCREEN_ORIENTATIONS = ["portrait", "landscape", "unlock"] as const;

type IScreenOrientation = (typeof SCREEN_ORIENTATIONS)[number];

@Injectable({
providedIn: "root",
})
export class ScreenOrientationService extends SyncServiceBase {
constructor(private templateActionRegistry: TemplateActionRegistry) {
/** Actively locked screen orientation */
private lockedOrientation: IScreenOrientation | undefined;

constructor(
private templateActionRegistry: TemplateActionRegistry,
private templateMetadataService: TemplateMetadataService
) {
super("Screen Orientation Service");
this.initialise();
}

initialise() {
this.registerTemplateActionHandlers();
// 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?
const isEnabled = Capacitor.isNativePlatform() || !environment.production;

if (isEnabled) {
// Add handlers to set orientation on action
this.registerTemplateActionHandlers();
// Set orientation when template parameter orientation changes
effect(async () => {
const { orientation } = this.templateMetadataService.parameterList();
this.setOrientation(orientation);
});
}
}

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);
},
});
}

private async setOrientation(orientation: OrientationLockType) {
return await ScreenOrientation.lock({ orientation });
private async setOrientation(orientation: IScreenOrientation) {
// avoid re-locking same orientation
if (orientation === this.lockedOrientation) return;

this.lockedOrientation = orientation;

if (orientation && orientation !== "unlock") {
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();
}
}
}
37 changes: 37 additions & 0 deletions src/app/shared/utils/angular.utils.ts
Original file line number Diff line number Diff line change
@@ -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<ActivatedRouteSnapshot> = { 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))
);
}

0 comments on commit cdff496

Please sign in to comment.