Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: template-level app config overrides #2531

Merged
merged 25 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4d4b4c6
feat(wip): template-level layout properties
jfmcquade Nov 15, 2024
ba315c7
refactor: layout service -> template app config service
jfmcquade Nov 18, 2024
1bc9221
chore(wip): template level app config overrides
jfmcquade Nov 18, 2024
cf50f91
chore: revert unnecessary changes to skin service
jfmcquade Nov 18, 2024
084bef8
chore: code tidy
jfmcquade Nov 18, 2024
f23b773
chore: update row parser test
jfmcquade Nov 18, 2024
4f7dbf4
Merge branch 'master' into feat/template-level-layout-props
jfmcquade Nov 21, 2024
c5c661a
chore: remove template-app-config service
chrismclarke Nov 22, 2024
66a9198
refactor: app config service override hierarchy
chrismclarke Nov 22, 2024
7b6152c
chore: code tidying
chrismclarke Nov 22, 2024
b01c0a4
fix: signal writes
chrismclarke Nov 22, 2024
7cef6bc
test: add app config service test
jfmcquade Nov 25, 2024
c7d980e
Merge pull request #2549 from IDEMSInternational/review/2351
jfmcquade Nov 25, 2024
f31e5d3
refactor: use signals for template-container templatename
jfmcquade Nov 25, 2024
c25a107
Merge branch 'master' into feat/stacks
jfmcquade Nov 25, 2024
6dd4e73
chore: error handling
jfmcquade Nov 25, 2024
e0679c8
fix: duplicate template render
chrismclarke Nov 25, 2024
5751e8c
refactor: remove template container props type
chrismclarke Nov 25, 2024
0c75151
fix: template instance signal bindings
chrismclarke Nov 25, 2024
fa890e1
Merge branch 'master' into feat/template-level-layout-props
chrismclarke Nov 25, 2024
c499404
Merge branch 'master' into feat/template-level-layout-props
jfmcquade Nov 27, 2024
2a7fef8
Merge branch 'master' into feat/template-level-layout-props
chrismclarke Nov 27, 2024
fffa57a
Merge branch 'master' into feat/template-level-layout-props
esmeetewinkel Nov 28, 2024
caeb68f
Merge branch 'master' into feat/template-level-layout-props
esmeetewinkel Nov 28, 2024
ef374ed
Merge branch 'master' into feat/template-level-layout-props
esmeetewinkel Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe("Template Parser PostProcessor", () => {
});
expect(case4.value).toEqual([
{ key_1a: "textValue", key_1b: "@local.value" },
{ key_2a: "", key_2b: "5" },
{ key_2a: "", key_2b: 5 },
]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import BaseProcessor from "./base";
import { existsSync } from "fs-extra";
import { IContentsEntry, parseAppDataCollectionString } from "../utils";

const cacheVersion = 20230509.1;
const cacheVersion = 20241118.0;

export class XLSXWorkbookProcessor extends BaseProcessor<IContentsEntry> {
constructor(paths: IConverterPaths) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FlowTypes } from "data-models";
import { setNestedProperty, booleanStringToBoolean } from "../utils";
import { setNestedProperty } from "../utils";
import { parseStringValue } from "shared/src/utils";

/**
* Convert app data map string to object
Expand Down Expand Up @@ -50,9 +51,13 @@ export function parseAppDataCollectionString(
// handle keys that define deeper nesting, such as time.hours: 7
if (key.includes(".")) {
const [base, ...nested] = key.split(".");
collection[base] = setNestedProperty(nested.join("."), value, collection[base]);
collection[base] = setNestedProperty(
nested.join("."),
parseStringValue(value),
collection[base]
);
} else {
collection[key] = booleanStringToBoolean(value);
collection[key] = parseStringValue(value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 - I see why it would be good to parse strings from parameter lists, as long term it should help reduce some of the issues with string-only representations within parameter lists (and all the various workarounds required).

I assume cases where authors manually convert string to number via [email protected]_value will still work as expected
+"1" === +1 === 1, so most likely just some extra redundant code in the codebase to support conversion (e.g. getBooleanParamFromParameterList), which would also likely be easier to keep in for now (ideally tidy in the future)

Equally I can't think of any immediate knock-ons, although I think might still be good to flag as a potentially breaking change so authors are aware (add clear comment to top of PR, update pr title to include feat! and tag with breaking. Authors will be able to see from content sync what has changed.

Did you get a chance to quickly check sync against debug deployment to see if anything crops up? Might be easier to do once debug deployment next updated for cleaner diffs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes agreed. I've updated the PR description. It's worth noting that these changes are only used in the parsing of flow-level parameter lists (i.e. those set on flows in the contents list, not those that appear within templates) as far as I can tell, so the knock-ons are even more minimal than if it was universal (I think parameter lists in templates are already parsed more thoroughly)

Copy link
Member

@chrismclarke chrismclarke Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, all template row parameter_lists are already going through this function so makes a lot of sense to also pass the contents through it. So I think actually likelihood of knock-ons even less, but still good to flag these things for sure

}
});
return collection;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, Input } from "@angular/core";
import { ModalController } from "@ionic/angular";
import { FlowTypes, ITemplateContainerProps } from "../../../models";
import { FlowTypes } from "../../../models";
import { TemplateContainerComponent } from "../../../template-container.component";

@Component({
Expand Down Expand Up @@ -41,11 +41,15 @@ export class TemplatePopupComponent {
}
}

export interface ITemplatePopupComponentProps extends ITemplateContainerProps {
/** Required inputs to pass on to TemplateContainer component */
interface IContainerProps {
name: string;
templatename: string;
parent?: TemplateContainerComponent;
row?: FlowTypes.TemplateRow;
parent?: TemplateContainerComponent;
}

export interface ITemplatePopupComponentProps extends IContainerProps {
showCloseButton?: boolean;
/** Dismiss popup when completed or uncompleted is emitted from child template */
dismissOnEmit?: boolean;
Expand Down
14 changes: 0 additions & 14 deletions src/app/shared/components/template/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,6 @@ import { FlowTypes } from "src/app/shared/model";
import { TemplateContainerComponent } from "../template-container.component";
export { FlowTypes } from "src/app/shared/model";

/**
* Properties passed to a template container instance
* @param templateName - flow_name of template to lookup
* @param name - unique name for the template, in cases where multiple child templates of the same type may exist (e.g. multiple buttons)
* @param row - reference to the full row if template instantiated from a parent
* @param parent - reference to parent template (when nested)
*/
export interface ITemplateContainerProps {
templatename: string;
name: string;
parent?: { name: string; component?: any };
row?: FlowTypes.TemplateRow;
}

/**
* Properties passed to a template row instance
* @param row specific data used in component rendering
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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";
import { AppConfigService } from "src/app/shared/services/app-config/app-config.service";

/**
* Service responsible for handling metadata of the current top-level template,
Expand All @@ -26,6 +27,7 @@ export class TemplateMetadataService extends SyncServiceBase {

constructor(
private templateService: TemplateService,
private appConfigService: AppConfigService,
private router: Router
) {
super("TemplateMetadata");
Expand All @@ -42,5 +44,13 @@ export class TemplateMetadataService extends SyncServiceBase {
},
{ allowSignalWrites: true }
);
// apply any template-specific appConfig overrides on change
effect(
() => {
const templateAppConfig = this.parameterList().app_config;
this.appConfigService.setAppConfig(templateAppConfig, "template");
},
{ allowSignalWrites: true }
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
ITemplatePopupComponentProps,
TemplatePopupComponent,
} from "../components/layout/popup/popup.component";
import { ITemplateContainerProps } from "../models";
import { TemplateContainerComponent } from "../template-container.component";

// Toggle logs used across full service for debugging purposes (there's quite a few and tedious to comment)
Expand Down Expand Up @@ -40,7 +39,7 @@ export class TemplateNavService extends SyncServiceBase {
* unless specifically closed (e.g. nav triggered from modal)
*/
private openPopupsByName: {
[templatename: string]: { modal: HTMLIonModalElement; props: ITemplateContainerProps };
[templatename: string]: { modal: HTMLIonModalElement; props: ITemplatePopupComponentProps };
} = {};

public async handleQueryParamChange(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<ng-template [ngIf]="debugMode && !parent" [ngIfElse]="noDebugger">
<!-- Debugger Mode -->
<div class="debug-name">
<div>template: {{ templatename || "(undefined)" }}</div>
<div>template: {{ templatename() || "(undefined)" }}</div>
<div>name: {{ name || "(undefined)" }}</div>
</div>
<!-- Template Component-->
Expand Down
26 changes: 17 additions & 9 deletions src/app/shared/components/template/template-container.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
ChangeDetectorRef,
Component,
effect,
ElementRef,
EventEmitter,
HostBinding,
Injector,
input,
Input,
OnDestroy,
OnInit,
Expand All @@ -13,7 +15,7 @@ import {
import { ActivatedRoute } from "@angular/router";
import { takeUntil } from "rxjs/operators";
import { Subject } from "rxjs";
import { FlowTypes, ITemplateContainerProps } from "./models";
import { FlowTypes } from "./models";
import { TemplateActionService } from "./services/instance/template-action.service";
import { TemplateNavService } from "./services/template-nav.service";
import { TemplateRowService } from "./services/instance/template-row.service";
Expand All @@ -37,12 +39,14 @@ let log_groupEnd = SHOW_DEBUG_LOGS ? console.groupEnd : () => null;
* - Track dynamic variable dependency (to know when to trigger row change based on set_local/field/global events)
* - Consider case of template container re-render (some draft cached code exists, but not sure if this is a valid use-case or not)
*/
export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateContainerProps {
export class TemplateContainerComponent implements OnInit, OnDestroy {
/** unique instance_name of template if created as a child of another template */
@Input() name: string;
/** flow_name of template for lookup */
@Input() templatename: string;
templatename = input.required<string>();
/** reference to parent template (when nested) */
@Input() parent?: TemplateContainerComponent;
/** reference to the full row if template instantiated from a parent */
@Input() row?: FlowTypes.TemplateRow;
/** Allow parents to also see emitted value (note - currently responding to emit is done in service, not output bindings except for ) */
@Output() emittedValue = new EventEmitter<{ emit_value: string; emit_data: any }>();
Expand Down Expand Up @@ -73,12 +77,17 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC
public elRef?: ElementRef,
private route?: ActivatedRoute
) {
effect(() => {
// re-render template whenever input template name changes
const templatename = this.templatename();
this.renderTemplate(templatename);
});
this.templateActionService = new TemplateActionService(injector, this);
this.templateRowService = new TemplateRowService(injector, this);
}
/** Assign the templatename as metdaata on the component for easier debugging and testing */
@HostBinding("attr.data-templatename") get getTemplatename() {
return this.templatename;
return this.templatename();
}

async ngOnInit() {
Expand All @@ -90,7 +99,6 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC
this.setLogging(true);
this.templateRowService.setLogging(true);
}
await this.renderTemplate();
}

ngOnDestroy(): void {
Expand Down Expand Up @@ -136,7 +144,7 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC
this.templateRowService.renderedRows = [];
// allow time for other pending ops to finish
await _wait(50);
await this.renderTemplate();
await this.renderTemplate(this.templatename());
} else {
await this.templateRowService.processRowUpdates();
console.log("[Force Reprocess]", this.name, this.templateRowService.renderedRows);
Expand Down Expand Up @@ -174,13 +182,13 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC
* Template Initialisation
**************************************************************************************/

private async renderTemplate() {
private async renderTemplate(templatename: string) {
// Lookup template
const template = await this.templateService.getTemplateByName(
this.templatename,
templatename,
this.row?.is_override_target
);
this.name = this.name || this.templatename;
this.name = this.name || templatename;
this.templateBreadcrumbs = [...(this.parent?.templateBreadcrumbs || []), this.name];
this.template = template;
log_group("[Template Render Start]", this.name);
Expand Down
50 changes: 34 additions & 16 deletions src/app/shared/services/app-config/app-config.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ import { TestBed } from "@angular/core/testing";
import { AppConfigService } from "./app-config.service";
import { BehaviorSubject } from "rxjs/internal/BehaviorSubject";
import { IAppConfig } from "../../model";
import { signal } from "@angular/core";
import { signal, WritableSignal } from "@angular/core";
import { DeploymentService } from "../deployment/deployment.service";
import {
getDefaultAppConfig,
IAppConfigOverride,
IDeploymentRuntimeConfig,
} from "packages/data-models";
import { IAppConfigOverride, IDeploymentRuntimeConfig } from "packages/data-models";
import { deepMergeObjects } from "../../utils";
import { firstValueFrom } from "rxjs/internal/firstValueFrom";
import { MockDeploymentService } from "../deployment/deployment.service.spec";
Expand Down Expand Up @@ -46,6 +42,7 @@ const MOCK_DEPLOYMENT_CONFIG: Partial<IDeploymentRuntimeConfig> = {
*/
describe("AppConfigService", () => {
let service: AppConfigService;
let appConfigSetSpy: jasmine.Spy<WritableSignal<IAppConfig>>;

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -54,24 +51,20 @@ describe("AppConfigService", () => {
],
});
service = TestBed.inject(AppConfigService);
appConfigSetSpy = spyOn(service.appConfig, "set").and.callThrough();
});

it("applies default config overrides on init", () => {
expect(service.appConfig().APP_HEADER_DEFAULTS.title).toEqual(
getDefaultAppConfig().APP_HEADER_DEFAULTS.title
);
expect(service.appConfig().APP_HEADER_DEFAULTS.title).toEqual("App");
});

it("applies deployment-specific config overrides on init", () => {
expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("mock_footer");
});

it("applies overrides to app config", () => {
service.setAppConfig({ APP_HEADER_DEFAULTS: { title: "updated" } });
expect(service.appConfig().APP_HEADER_DEFAULTS).toEqual({
...getDefaultAppConfig().APP_HEADER_DEFAULTS,
title: "updated",
});
it("applies skin-level overrides to app config", () => {
service.setAppConfig({ APP_HEADER_DEFAULTS: { title: "updated" } }, "skin");
expect(service.appConfig().APP_HEADER_DEFAULTS.title).toEqual("updated");
// also ensure doesn't unset default deployment
expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("mock_footer");
});
Expand All @@ -80,7 +73,32 @@ describe("AppConfigService", () => {
firstValueFrom(service.changes$).then((v) => {
expect(v).toEqual({ APP_HEADER_DEFAULTS: { title: "partial changes" } });
});
service.setAppConfig({ APP_HEADER_DEFAULTS: { title: "partial changes" } }, "skin");
expect(appConfigSetSpy).toHaveBeenCalledTimes(1);
});

it("ignores lower-order updates when higher order exists", async () => {
service.setAppConfig({ APP_FOOTER_DEFAULTS: { templateName: "template_footer" } }, "template");
expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("template_footer");
service.setAppConfig({ APP_FOOTER_DEFAULTS: { templateName: "skin_footer" } }, "skin");
expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("template_footer");
// the second service set should not trigger any changes to appConfig signal (or observable)
expect(appConfigSetSpy).toHaveBeenCalledTimes(1);
});

it("reverts to initial config values when all overrides removed", async () => {
service.setAppConfig({ APP_FOOTER_DEFAULTS: { templateName: "template_footer" } }, "template");
expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("template_footer");
service.setAppConfig({}, "template");
expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("mock_footer");
});

service.setAppConfig({ APP_HEADER_DEFAULTS: { title: "partial changes" } });
it("reverts to lower order config values when higher order override removed", async () => {
service.setAppConfig({ APP_FOOTER_DEFAULTS: { templateName: "skin_footer" } }, "skin");
expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("skin_footer");
service.setAppConfig({ APP_FOOTER_DEFAULTS: { templateName: "template_footer" } }, "template");
expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("template_footer");
service.setAppConfig({}, "template");
expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("skin_footer");
});
});
Loading
Loading