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

Spike: data items logic separation #2568

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 9 additions & 8 deletions src/app/shared/components/template/components/base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, signal } from "@angular/core";
import { Component, computed, Input, signal } from "@angular/core";
import { isEqual } from "packages/shared/src/utils/object-utils";
import { FlowTypes, ITemplateRowProps } from "../models";
import { TemplateContainerComponent } from "../template-container.component";
Expand All @@ -20,27 +20,28 @@ export class TemplateBaseComponent implements ITemplateRowProps {
/** @ignore */
_row: FlowTypes.TemplateRow;

value = signal<FlowTypes.TemplateRow["value"]>(undefined, { equal: isEqual });
parameterList = signal<FlowTypes.TemplateRow["parameter_list"]>({}, { equal: isEqual });
actionList = signal<FlowTypes.TemplateRow["action_list"]>([], { equal: isEqual });
// TODO - main row should just be an input.required and child code refactored to avoid set override
// TODO - could also consider whether setting parent required (is it template row map or services?), possibly merge with row
rowSignal = signal<FlowTypes.TemplateRow>(undefined, { equal: isEqual });
value = computed(() => this.rowSignal().value, { equal: isEqual });
parameterList = computed(() => this.rowSignal().parameter_list || {}, { equal: isEqual });
actionList = computed(() => this.rowSignal().action_list || [], { equal: isEqual });
rows = computed<FlowTypes.TemplateRow[]>(() => this.rowSignal().rows || [], { equal: isEqual });

/**
* @ignore
* specific data used in component rendering
**/
@Input() set row(row: FlowTypes.TemplateRow) {
this._row = row;
this.value.set(row.value);
this.parameterList.set(row.parameter_list);
this.actionList.set(row.action_list);
this.rowSignal.set(row);
}

/**
* @ignore
* reference to parent template container - does not have setter as should remain static
**/
@Input() parent: TemplateContainerComponent;
constructor() {}

/**
* Whenever actions are triggered handle in the parent template component
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<plh-template-component
*ngFor="let row of itemRows"
[row]="row"
[parent]="parent"
[attr.data-rowname]="row.name"
trackBy:row.name
></plh-template-component>
@for (row of itemRows(); track row._nested_name) {
<plh-template-component
[row]="row"
[parent]="parent"
[attr.data-rowname]="row.name"
></plh-template-component>
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Injector,
Input,
OnDestroy,
} from "@angular/core";
import { debounceTime, Subscription } from "rxjs";
import {
DynamicDataService,
ISetItemContext,
} from "src/app/shared/services/dynamic-data/dynamic-data.service";
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { FlowTypes } from "../../models";
import { ItemProcessor } from "../../processors/item";
import { TemplateRowService } from "../../services/instance/template-row.service";
import { TemplateVariablesService } from "../../services/template-variables.service";
import { TemplateBaseComponent } from "../base";
import { DataItemsService } from "./data-items.service";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { switchMap, filter } from "rxjs";

@Component({
selector: "plh-data-items",
Expand All @@ -32,176 +20,22 @@ import { TemplateBaseComponent } from "../base";
* - Should be refactored into structural container instead of display component
* - Could possibly refactor to feature module including services
*/
export class TmplDataItemsComponent extends TemplateBaseComponent implements OnDestroy {
public itemRows: FlowTypes.TemplateRow[] = [];

private dataListName: string;
private _parameterList: Record<string, string>;

private dataQuery$: Subscription;

@Input() set row(row: FlowTypes.TemplateRow) {
this._row = row;
this.dataListName = this.hackGetRawDataListName(row);
this._parameterList = row.parameter_list;
this.subscribeToData();
}

constructor(
private dynamicDataService: DynamicDataService,
private templateVariablesService: TemplateVariablesService,
private injector: Injector,
private cdr: ChangeDetectorRef
) {
export class TmplDataItemsComponent extends TemplateBaseComponent {
// HACK - create signal from combination of signals and observables
// https://github.com/angular/angular/issues/53519
public itemRows = toSignal(
toObservable(this.rowSignal).pipe(
filter((row) => row !== undefined),
switchMap((row) => this.subscribeToDynamicData(row))
)
);

constructor(private dataItemsService: DataItemsService) {
super();
}

private async subscribeToData() {
if (this.dataQuery$) {
this.dataQuery$.unsubscribe();
}
if (this.dataListName) {
await this.dynamicDataService.ready();
const query = await this.dynamicDataService.query$("data_list", this.dataListName);
this.dataQuery$ = query.pipe(debounceTime(50)).subscribe(async (data) => {
await this.renderItems(data, this._row.rows, this._parameterList);
});
} else {
await this.renderItems([], [], {});
}
}

private async renderItems(
itemDataList: any[],
rows: FlowTypes.TemplateRow[],
parameterList: any
) {
const parsedItemDataList = await this.parseDataList(itemDataList);
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
this.itemRows = parsedItemRows;
this.cdr.markForCheck();
}

/**
* Update item dynamic evaluation context and action lists to include relevant item data.
* @param templateRows List of template rows generated from itemData by item processor
* @param itemData List of original item data used to create item rows (post operations such as filter/sort)
* @param dataListName The name of the source data list (i.e. this.dataListName, extracted for ease of testing)
* */
private setItemMeta(
templateRows: FlowTypes.TemplateRow[],
itemData: FlowTypes.Data_listRow[],
dataListName: string
) {
const lastItemIndex = itemData.length - 1;
const itemDataIDs = itemData.map((item) => item.id);
// Reassign metadata fields previously assigned by item as rendered row count may have changed
return templateRows.map((r) => {
const itemId = r._evalContext.itemContext._id;
// Map the row item context to the original list of items rendered to know position in item list.
const itemIndex = itemDataIDs.indexOf(itemId);
// Update metadata fields as _first, _last and index may have changed based on dynamic updates
r._evalContext.itemContext = {
...r._evalContext.itemContext,
_index: itemIndex,
_first: itemIndex === 0,
_last: itemIndex === lastItemIndex,
};
// Update any action list set_item args to contain name of current data list and item id
// and set_items action to include all currently displayed rows
if (r.action_list) {
const setItemContext: ISetItemContext = {
flow_name: this.dataListName,
itemDataIDs,
currentItemId: itemId,
};
r.action_list = r.action_list.map((a) => {
if (a.action_id === "set_item") {
a.args = [setItemContext];
}
if (a.action_id === "set_items") {
// TODO - add a check for @item refs and replace parameter list with correct values
// for each individual item (default will be just to pick the first)
a.args = [setItemContext];
}
return a;
});
}

// Apply recursively to ensure item children with nested rows (e.g. display groups) also inherit item context
if (r.rows) {
r.rows = this.setItemMeta(r.rows, itemData, dataListName);
}

return r;
});
}

/**
* Ordinarily rows would be processed as part of the regular template processing,
* however this must be bypassed to allow multiple reprocessing on item updates
*/
private async hackProcessRows(rows: FlowTypes.TemplateRow[]) {
const processor = new TemplateRowService(this.injector, {
name: "",
template: {
rows,
},
row: {
rows: [],
},
} as any);
// HACK - still want to be able to use localContext from parent rows so copy to child processor
processor.templateRowMap = JSON.parse(
JSON.stringify(this.parent.templateRowService.templateRowMap)
);
await processor.processContainerTemplateRows();
return processor.renderedRows;
}

/**
* If datalist referenced as @data.some_list it will already be parsed, so extract
* name from raw values.
* Alternatively any list provided as a string value can be returned directly
* */
private hackGetRawDataListName(row: FlowTypes.TemplateRow) {
if (!row.value) return;
if (typeof row.value === "string") {
return row.value;
}
// HACK - if list name contains '_list' template.parser will parse as an array instead of string
if (Array.isArray(row.value)) {
return row.value[0];
}
// Extract raw name in case full datalist object supplied in place of name
return row._dynamicFields?.value?.[0]?.fieldName;
}

/** Copied from template-row service */
private async parseDataList(dataList: { [id: string]: any }) {
const parsed: { [id: string]: any } = {};
for (const [listKey, listValue] of Object.entries(dataList)) {
parsed[listKey] = listValue;
for (const [itemKey, itemValue] of Object.entries(listValue)) {
if (typeof itemValue === "string") {
parsed[listKey][itemKey] =
await this.templateVariablesService.evaluateConditionString(itemValue);
}
}
}
return parsed;
}

ngOnDestroy() {
if (this.dataQuery$) {
this.dataQuery$.unsubscribe();
}
private subscribeToDynamicData(row: FlowTypes.TemplateRow) {
const { templateRowMap } = this.parent;
return this.dataItemsService.getItemsObservable(row, templateRowMap);
}
}
Loading
Loading