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: user debug page dynamic updates #2839

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
18 changes: 11 additions & 7 deletions src/app/feature/user/pages/user-debug/user-debug.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,15 @@ <h2>Dynamic Data</h2>
<ion-select
#dynamicDataSelect
placeholder="Select Data"
[(ngModel)]="dynamicDataSelected"
(ionChange)="setDynamicEntryView(dynamicDataSelect.value)"
[value]="dynamicDataSelected()"
(ionChange)="dynamicDataSelected.set(dynamicDataSelect.value)"
style="margin-bottom: 1rem"
[compareWith]="dynamicEntryCompareFn"
>
@for(entry of dynamicDataEntries; track entry.id){
<ion-select-option [value]="entry">{{entry.flow_name}}</ion-select-option>
@for(flowName of dynamicDataSelectOptions(); track flowName){
<ion-select-option [value]="flowName">{{flowName}}</ion-select-option>
}
</ion-select>
@if(dynamicDataTable; as table){
@if(dynamicDataTableData(); as table){
<div class="table-container">
<table>
<thead>
Expand All @@ -117,7 +116,12 @@ <h2>Dynamic Data</h2>
@for(row of table.rows; track row.id){
<tr>
@for(header of table.headers; track header){
<td>{{row[header]}}</td>
<td
class="dynamic-data-value"
[class.isOverrideValue]="row._override_keys.includes(header)"
>
{{row[header]}}
</td>
}
</tr>
}
Expand Down
7 changes: 7 additions & 0 deletions src/app/feature/user/pages/user-debug/user-debug.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ th {
top: 0;
background-color: #f9f8f8;
}

td.dynamic-data-value {
opacity: 0.4;
}
td.isOverrideValue {
opacity: 1;
}
.table-container {
max-height: 300px;
overflow: auto;
Expand Down
127 changes: 79 additions & 48 deletions src/app/feature/user/pages/user-debug/user-debug.page.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,20 @@
import { Component, Injector, OnInit, signal } from "@angular/core";
import { uniqueObjectArrayKeys } from "packages/shared/src/utils/object-utils";
import { Component, computed, effect, Injector, OnInit, signal } from "@angular/core";
import { isEqual, uniqueObjectArrayKeys } from "packages/shared/src/utils/object-utils";
import { map } from "rxjs";
import { toSignal } from "@angular/core/rxjs-interop";
import { TemplateActionService } from "src/app/shared/components/template/services/instance/template-action.service";
import { TemplateFieldService } from "src/app/shared/components/template/services/template-field.service";
import { AuthService } from "src/app/shared/services/auth/auth.service";
import { DynamicDataService } from "src/app/shared/services/dynamic-data/dynamic-data.service";
import { LocalStorageService } from "src/app/shared/services/local-storage/local-storage.service";

interface IDynamicDataEntry {
id: string;
flow_type: string;
flow_name: string;
data: Record<string, any>;
}
import { AppDataService } from "src/app/shared/services/data/app-data.service";

@Component({
selector: "user-debug-page",
templateUrl: "user-debug.page.html",
styleUrl: "user-debug.page.scss",
})
export class UserDebugPage implements OnInit {
constructor(
private fieldService: TemplateFieldService,
private dynamicDataService: DynamicDataService,
private localStorageService: LocalStorageService,
public authService: AuthService,
private injector: Injector
) {}
/** Id of current user */
public userId = "";
/** ID of user to import */
Expand All @@ -34,15 +23,68 @@ export class UserDebugPage implements OnInit {
public contactFields = signal<{ key: string; value: string }[]>([]);
/** List of protected user contact fields */
public protectedFields = signal<{ key: string; value: string }[]>([]);
/** List of user dynamic_data entries */
public dynamicDataEntries: IDynamicDataEntry[] = [];
/** Active row selected from list of user dynamic_data entries */
public dynamicDataSelected: IDynamicDataEntry;

/** Live state of all dynamic data stored */
private dynamicDataState = toSignal(this.subscribeToDynamicDataState());

/** Name of flow selected to display dynamic data */
public dynamicDataSelected = signal("");

/** Live state of selected flow dynamic data sub-state */
private dynamicDataSelectedState = computed(
() => {
const state = this.dynamicDataState() || ({} as any);
const selected = this.dynamicDataSelected();
return { ...state.data_list?.[selected] };
},
{ equal: isEqual }
);

/** Table configuration to display data from active dynamic_data row */
public dynamicDataTable: { headers: string[]; rows: any[] };
public dynamicDataTableData = signal({ headers: [], rows: [] });

/** List of all flow names where dynamic data has been set */
public dynamicDataSelectOptions = computed(() => [
...Object.keys(this.dynamicDataState()?.data_list || {}),
]);

private actionService = new TemplateActionService(this.injector);

constructor(
private fieldService: TemplateFieldService,
private appDataService: AppDataService,
private dynamicDataService: DynamicDataService,
private localStorageService: LocalStorageService,
public authService: AuthService,
private injector: Injector
) {
// load first dynamic data option by default
effect(
() => {
const selected = this.dynamicDataSelected();
if (!selected) {
const options = this.dynamicDataSelectOptions();
if (options[0]) {
this.dynamicDataSelected.set(options[0]);
}
}
},
{ allowSignalWrites: true }
);
// load table data when selected flow or corresponding data sub-state changes
effect(
async () => {
const selected = this.dynamicDataSelected();
const dynamicData = this.dynamicDataSelectedState();
if (selected) {
const { headers, rows } = await this.loadDynamicDataTable(selected, dynamicData);
this.dynamicDataTableData.set({ headers, rows });
}
},
{ allowSignalWrites: true }
);
}

async ngOnInit() {
await this.fieldService.ready();
await this.dynamicDataService.ready();
Expand All @@ -55,10 +97,6 @@ export class UserDebugPage implements OnInit {
this.loadUserContactFields();
this.userId = this.fieldService.getField("_app_user_id");
this.importUserId = this.userId;
this.dynamicDataEntries = await this.getDynamicDataEntries();
// populate dynamic data table with first entry if available
this.dynamicDataSelected = this.dynamicDataEntries[0];
this.setDynamicEntryView(this.dynamicDataEntries[0]);
}

public async importUserData(id: string) {
Expand All @@ -81,19 +119,17 @@ export class UserDebugPage implements OnInit {
}

/** Prepare table data to display for provided dynamic data entry */
public setDynamicEntryView(entry?: IDynamicDataEntry) {
if (entry) {
// ensure all entries include an id column and put at start of table
const rows = Object.entries(entry.data).map(([id, data]) => ({ ...data, id }));
const uniqueKeys = uniqueObjectArrayKeys(rows);
const headers = ["id", ...uniqueKeys.filter((k) => k !== "id")];
this.dynamicDataTable = { headers, rows };
} else {
this.dynamicDataTable = { headers: [], rows: [] };
}
}
public dynamicEntryCompareFn(a: IDynamicDataEntry, b: IDynamicDataEntry) {
return a.id === b.id;
private async loadDynamicDataTable(flowName: string, flowDynamicData: any = {}) {
const sheetData = await this.appDataService.getSheet("data_list", flowName);
const allTableData = sheetData?.rows || [];
// generate list of all unique headers found across original data and overrides
const headers = uniqueObjectArrayKeys([...allTableData, ...Object.values(flowDynamicData)]);
// generate a list of merged initial + user override data, tracking what keys contain overridden values
const rows = allTableData.map((r) => {
const overrides = flowDynamicData[r.id];
return { ...r, ...overrides, _override_keys: Object.keys(overrides || {}) };
});
return { headers, rows };
}

/** Retrieve localStorage entries prefixed by field service prefix */
Expand All @@ -107,15 +143,10 @@ export class UserDebugPage implements OnInit {
this.protectedFields.set(contactFields.filter((v) => v.key.startsWith("_")));
}

/** Retrieve user dynamic data entries stored in IndexedDB */
private async getDynamicDataEntries() {
const dynamicData: IDynamicDataEntry[] = [];
const state = await this.dynamicDataService.getState();
for (const [flow_type, dataByFlow] of Object.entries(state)) {
for (const [flow_name, data] of Object.entries(dataByFlow)) {
dynamicData.push({ flow_type, flow_name, data, id: `${flow_type}__${flow_name}` });
}
}
return dynamicData;
/** Create a subscription that updates with any writeCache db changes and returns full state of cache */
private subscribeToDynamicDataState() {
const writeCache = this.dynamicDataService["writeCache"];
// return new object for writeCache state to ensure signal fires
return writeCache["collection"].find().$.pipe(map(() => ({ ...writeCache.state })));
}
}
Loading