Skip to content

Commit

Permalink
Merge pull request #2706 from IDEMSInternational/fix/data-items-set-l…
Browse files Browse the repository at this point in the history
…ocal

fix: data-items set local
  • Loading branch information
esmeetewinkel authored Jan 15, 2025
2 parents 656c2a7 + 8aec9da commit f349709
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/data-models/flowTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ export namespace FlowTypes {
"screen_orientation",
"set_field",
"set_local",
"set_self",
"share",
"style",
"start_tour",
Expand Down
6 changes: 4 additions & 2 deletions src/app/shared/components/template/components/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,11 @@ export class TemplateBaseComponent implements ITemplateRowProps {
* @ignore
**/
setValue(value: any) {
// console.log("setting value", value);
// HACK - provide optimistic update so that data_items interceptor also can access updated row value
this._row.value = value;

const action: FlowTypes.TemplateRowAction = {
action_id: "set_local",
action_id: "set_self",
args: [this._row._nested_name, value],
trigger: "click",
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export class TmplDataItemsComponent extends TemplateBaseComponent {
await this.hackTriggerDataChangedActions(actions, itemRows);
}
});
effect(() => {
const { _nested_name } = this.rowSignal();
this.hackInterceptComponentActions(_nested_name);
});
}

/** Trigger a `data_changed` action and evaluate with items list context */
Expand All @@ -59,4 +63,18 @@ export class TmplDataItemsComponent extends TemplateBaseComponent {
const { templateRowMap } = this.parent;
return this.dataItemsService.getItemsObservable(row, templateRowMap);
}

/**
* Prevent child components triggering set_self actions that would update the value of a component
* within the data_items loop. These are not synced with the parent templateRowMap, and instead require
* the author to explicitly use a set_item action. This applies to any component that internally calls `setValue`
*/
private hackInterceptComponentActions(_nested_name: string) {
this.parent.templateActionService.registerActionsInterceptor(_nested_name, (action) => {
if (action.action_id === "set_self" && action._triggeredBy._evalContext?.itemContext) {
return undefined;
}
return this.dataItemsService.evaluateComponentAction(action);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ItemProcessor } from "../../processors/item";
import { updateItemMeta } from "./data-items.utils";
import { isEqual } from "packages/shared/src/utils/object-utils";
import { AppDataEvaluator } from "packages/shared/src/models/appDataEvaluator/appDataEvaluator";
import { JSEvaluator } from "packages/shared/src/models/jsEvaluator/jsEvaluator";

@Injectable({ providedIn: "root" })
export class DataItemsService {
Expand Down Expand Up @@ -84,6 +85,23 @@ export class DataItemsService {
});
}

/** Evaluate any self-references within triggered action params or args */
public evaluateComponentAction(action: FlowTypes.TemplateRowAction) {
const evaluator = new JSEvaluator();
for (const [key, value] of Object.entries(action.params || {})) {
if (typeof value === "string" && value.includes("this.value")) {
action.params[key] = evaluator.evaluate(value, action._triggeredBy);
}
}
action.args = action.args.map((arg) => {
if (typeof arg === "string" && arg.includes("this.value")) {
arg = evaluator.evaluate(arg, action._triggeredBy);
}
return arg;
});
return action;
}

/**
* If datalist referenced as @data.some_list it will already be parsed, so extract
* name from raw values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class="text-box-input"
[inputmode]="isNumberInput ? 'tel' : 'text'"
(ionBlur)="handleChange(input.value)"
[value]="prioritisePlaceholder ? '' : _row.value"
[value]="prioritisePlaceholder ? '' : value()"
[placeholder]="placeholder"
[maxlength]="maxLength"
[style.textAlign]="textAlign"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ export class TmplTextComponent extends TemplateBaseComponent implements OnInit {
params: Partial<ITextParams> = {};
hasTextValue: boolean;

constructor() {
super();
}

ngOnInit() {
this.getParams();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ let log_groupEnd = SHOW_DEBUG_LOGS ? console.groupEnd : () => null;
export class TemplateActionService extends SyncServiceBase {
private actionsQueue: FlowTypes.TemplateRowAction[] = [];
private actionsQueueProcessing$ = new BehaviorSubject<boolean>(false);
private actionsInterceptors = new Map();

constructor(
private injector: Injector,
Expand Down Expand Up @@ -98,9 +99,17 @@ export class TemplateActionService extends SyncServiceBase {
actions: FlowTypes.TemplateRowAction[] = [],
_triggeredBy?: FlowTypes.TemplateRow
) {
// Track action trigger source
// Will be undefined if triggered from service instead of row component
actions = actions.map((a) => {
a._triggeredBy = _triggeredBy;
return a;
});

await this.ensurePublicServicesReady();
// process any global action interceptors
const unhandledActions = await this.handleActionsInterceptor(actions);
unhandledActions.forEach((action) => this.actionsQueue.push({ ...action, _triggeredBy }));
unhandledActions.forEach((action) => this.actionsQueue.push({ ...action }));
const res = await this.processActionQueue();
await this.handleActionsCallback([...unhandledActions], res);
if (!this.container?.parent) {
Expand All @@ -114,13 +123,33 @@ export class TemplateActionService extends SyncServiceBase {
/** Optional method child component can add to handle post-action callback */
public async handleActionsCallback(actions: FlowTypes.TemplateRowAction[], results: any) {}

/** Optional method child component can filter action list to handle outside of default handlers */
/**
* @deprecated v0.18.0 - prefer to use `registerActionsInterceptor`
* Provide a single override method that will be applied to all actions triggered within the
* current container
* */
public async handleActionsInterceptor(
actions: FlowTypes.TemplateRowAction[]
): Promise<FlowTypes.TemplateRowAction[]> {
return actions;
}

/**
* Register an action interceptor to apply to all actions within a named scope
* @param scope namespace to apply actions. Any actions triggered by components starting with
* the same namespace will be intercepted, matched by the component nested name. Usually this
* will be the current component row name, to apply to all nested children
* @param handler function to apply to action. If action is returned from function then the action
* will continue to be piped through any more interceptors as well as final processing.
* Return undefined to prevent further action processing
* */
public async registerActionsInterceptor(
scope: string,
handler: (action: FlowTypes.TemplateRowAction) => FlowTypes.TemplateRowAction | undefined
) {
this.actionsInterceptors.set(scope, handler);
}

/**
* To avoid actions potentially trying to write to same db records at the same time,
* all actions are added to a queue and processed in order of addition
Expand All @@ -133,9 +162,13 @@ export class TemplateActionService extends SyncServiceBase {
this.actionsQueueProcessing$.next(true);
while (this.actionsQueue.length > 0) {
const action = this.actionsQueue[0];
await this.processAction(action);
// Pipe action through interceptors. Only process if interceptors return a value
const postInterceptAction = await this.processActionInterceptors(action);
if (postInterceptAction) {
await this.processAction(action);
processedActions.push(action);
}
this.actionsQueue.shift();
processedActions.push(action);
}
this.actionsQueueProcessing$.next(false);
log_groupEnd();
Expand All @@ -151,13 +184,26 @@ export class TemplateActionService extends SyncServiceBase {
const reprocessActions: FlowTypes.TemplateRowAction["action_id"][] = [
"set_field",
"set_local",
"set_self",
"trigger_actions",
];
if (processedActions.find((a) => reprocessActions.includes(a.action_id))) {
await this.container.templateRowService.processRowUpdates();
}
}
}

private async processActionInterceptors(action: FlowTypes.TemplateRowAction) {
const actionScope = action._triggeredBy?._nested_name;
if (!actionScope) return action;
for (const [scope, interceptor] of this.actionsInterceptors) {
if (action && actionScope.startsWith(scope)) {
action = await interceptor(action);
}
}
return action;
}

private async processAction(action: FlowTypes.TemplateRowAction) {
action.args = action.args.map((arg) => {
// HACK - update any self referenced values (see note from template.parser method)
Expand All @@ -184,6 +230,9 @@ export class TemplateActionService extends SyncServiceBase {
case "set_local":
console.log("[SET LOCAL]", { key, value });
return this.setLocalVariable(key, value);
case "set_self":
console.log("[SET LOCAL]", { key, value });
return this.setLocalVariable(key, value);
case "go_to":
return this.templateNavService.handleNavAction(action);
case "go_to_url":
Expand Down

0 comments on commit f349709

Please sign in to comment.