From dc5c5cbfeba61fe57f531a3775343a56f1ab4cc6 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 10 Jun 2024 17:53:53 +0100 Subject: [PATCH 001/204] feat: add new 'task: evaluate_group_completion' action --- packages/data-models/flowTypes.ts | 1 + .../dynamic-data/dynamic-data.service.ts | 4 +- .../shared/services/task/task.service.spec.ts | 19 +++ src/app/shared/services/task/task.service.ts | 154 +++++++++++++++++- 4 files changed, 172 insertions(+), 6 deletions(-) diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 9b568bfe7..f0e657cb5 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -419,6 +419,7 @@ export namespace FlowTypes { "share", "style", "start_tour", + "task", "task_group_set_highlighted", "toggle_field", "track_event", diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index a63635ee5..5fd7c6138 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -118,8 +118,8 @@ export class DynamicDataService extends AsyncServiceBase { } /** Take a snapshot of the current state of a table */ - public async snapshot(flow_type: FlowTypes.FlowType, flow_name: string) { - const obs = await this.query$(flow_type, flow_name); + public async snapshot(flow_type: FlowTypes.FlowType, flow_name: string) { + const obs = await this.query$(flow_type, flow_name); return firstValueFrom(obs); } diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index 8d51cc23a..38b5580de 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -204,4 +204,23 @@ describe("TaskService", () => { // and again on the evaluation called above expect(scheduleCampaignNotificationsSpy).toHaveBeenCalledTimes(2); }); + it("parses task group refs to TaskGroup array", () => { + const taskGroupRefs = [ + "debug_task_group_c", + "debug_task_group_b.b_2", + "debug_task_group_a.a_1", + ]; + const taskGroups = service.parseTaskGroupRefsToTaskGroups(taskGroupRefs); + expect(taskGroups).toEqual({ + subtasksDataListName: "debug_task_group_c", + rowId: "b_2", + parentDataListName: "debug_task_group_b", + parentTaskGroup: { + subtasksDataListName: "debug_task_group_b", + rowId: "a_1", + parentDataListName: "debug_task_group_a", + parentTaskGroup: undefined, + }, + }); + }); }); diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index a5707e2a2..8b2fee35a 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -4,10 +4,28 @@ import { AppDataService } from "../data/app-data.service"; import { arrayToHashmap } from "../../utils"; import { AsyncServiceBase } from "../asyncService.base"; import { AppConfigService } from "../app-config/app-config.service"; -import { IAppConfig } from "../../model"; +import { FlowTypes, IAppConfig } from "../../model"; import { CampaignService } from "../../../feature/campaign/campaign.service"; +import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; +import { DynamicDataService } from "../dynamic-data/dynamic-data.service"; export type IProgressStatus = "notStarted" | "inProgress" | "completed"; +// This is the definition of a task: a row of a data list that has a "completed" column +export type TaskRow = FlowTypes.Data_listRow<{ completed: boolean }>; +// A task group consists of a task row (identified here by a parentDataListName and rowId) and a list of subtasks +interface TaskGroup { + /** The name of the data list that contains the subtasks that constitute the task group */ + subtasksDataListName: string; + /** The row id that identifies the task row corresponding to the task group within its parent data list */ + rowId: string; + /** + * The name of the data list that contains the task row corresponding to the task group + * NB if that data list is itself a task group, this will be identical to the parentTaskGroup's subtasksDataListName + */ + parentDataListName: string; + /** If the parent data list is itself a task group, include its reference */ + parentTaskGroup?: TaskGroup; +} @Injectable({ providedIn: "root", @@ -22,13 +40,16 @@ export class TaskService extends AsyncServiceBase { tasksFeatureEnabled: boolean; constructor( - private templateFieldService: TemplateFieldService, - private appDataService: AppDataService, private appConfigService: AppConfigService, - private campaignService: CampaignService + private appDataService: AppDataService, + private campaignService: CampaignService, + private dynamicDataService: DynamicDataService, + private templateFieldService: TemplateFieldService, + private templateActionRegistry: TemplateActionRegistry ) { super("Task"); this.registerInitFunction(this.initialise); + this.registerTemplateActionHandlers(); } /** @@ -200,6 +221,115 @@ export class TaskService extends AsyncServiceBase { }); } + private registerTemplateActionHandlers() { + this.templateActionRegistry.register({ + task: async ({ args, params }) => { + const [actionId] = args; + const childActions = { + /** + * HACK: evaluate completion status of task groups. ... + */ + evaluate_group_completion: async () => { + const taskGroupRefs = params.task_groups.split(" "); + console.log("taskGroupRefs", taskGroupRefs); + const nestedTaskGroups = this.parseTaskGroupRefsToTaskGroups(taskGroupRefs); + await this.evaluateTaskGroupCompletion(nestedTaskGroups); + }, + }; + if (!(actionId in childActions)) { + console.error("task does not have action", actionId); + return; + } + return childActions[actionId](); + }, + }); + } + + /** + * Parses an array of task group references into a nested TaskGroup object structure. + * + * Each task group reference is expected to be in the format "parentDataListName.rowId", where + * The function processes the references in reverse order to build the nested TaskGroup + * objects, linking each task group to its parent appropriately. + * + * @param {string[]} taskGroupRefs - An array of task group references in the format "parentDataListName.rowId", + * starting with the initial subtasks data list name + * @returns {TaskGroup} - The TaskGroup object corresponding to the last child task group, with anyu parent task groups nested within it. + * + * @example + * For taskGroupRefs = ["debug_task_group_c", "debug_task_group_b.b_2", "debug_task_group_a.a_1"]; + * the method will return: + * { + * subtasksDataListName: "debug_task_group_c", + * rowId: "b_2", + * parentDataListName: "debug_task_group_b", + * parentTaskGroup: { + * subtasksDataListName: "debug_task_group_b", + * rowId: "a_1", + * parentDataListName: "debug_task_group_a", + * parentTaskGroup: undefined + * } + * } + */ + public parseTaskGroupRefsToTaskGroups(taskGroupRefs: string[]): TaskGroup { + let parentTaskGroup: TaskGroup | undefined; + + // Iterate over the references in reverse order, stopping before first entry + for (let i = taskGroupRefs.length - 1; i > 0; i--) { + const ref = taskGroupRefs[i]; + const [parentDataListName, rowId] = ref.split("."); + + // Create the TaskGroup object + const taskGroup: TaskGroup = { + subtasksDataListName: taskGroupRefs[i - 1].split(".")[0], + rowId, + parentDataListName, + parentTaskGroup, + }; + + // Update the parentTaskGroup for the next iteration + parentTaskGroup = taskGroup; + } + + // The last child TaskGroup is the last one created + return parentTaskGroup; + } + + /** + * Evaluates the completion status of a given task group, including its nested parent task groups + * @param {TaskGroup} taskGroup The task group to evaluate, included any parent task groups nested within it + * @return {Promise} A boolean indicating whether the the last child task group (the top level of the TaskGroup object) is completed. + */ + private async evaluateTaskGroupCompletion(taskGroup: TaskGroup): Promise { + const { subtasksDataListName, rowId, parentDataListName, parentTaskGroup } = taskGroup; + const taskGroupData = await this.dynamicDataService.snapshot( + "data_list", + subtasksDataListName + ); + const taskGroupCompleted = taskGroupData.every((row) => row.completed); + + // Update completion of task group in dynamic data + await this.dynamicDataService.update("data_list", parentDataListName, rowId, { + completed: taskGroupCompleted, + }); + + // Support legacy task group implementation, where task completion is tracked in fields + const taskGroupRow = ( + await this.dynamicDataService.snapshot("data_list", subtasksDataListName) + ).find((row) => row.id === rowId); + const taskGroupCompletedField = taskGroupRow?.["completed_field"]; + if (taskGroupCompletedField) { + await this.setTaskGroupCompletedStatus(taskGroupCompletedField, taskGroupCompleted); + this.evaluateHighlightedTaskGroup(); + } + + // recursively evaluate any parent task groups + if (parentTaskGroup) { + this.evaluateTaskGroupCompletion(parentTaskGroup); + } + return taskGroupCompleted; + } + /** * TODO: this is not currently implemented, and should likely be reworked as part of a broader overhaul of the task system * @@ -228,3 +358,19 @@ export class TaskService extends AsyncServiceBase { // return this.evaluateHighlightedTaskGroup(); // } } + +const taskGroupA: TaskGroup = { + subtasksDataListName: "debug_task_group_b", + parentDataListName: "debug_task_group_a", + rowId: "a_1", +}; + +// e.g. +const taskGroupB: TaskGroup = { + subtasksDataListName: "debug_task_group_c", + rowId: "b_2", + parentDataListName: "debug_task_group_b", + parentTaskGroup: taskGroupA, +}; + +// Alternatively, could remove the subtasksDataListName property and somehow read it in at the bottom level... From 74576da3a41f048c0a783f5224bfd18890d54851 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 11 Jun 2024 14:55:20 +0100 Subject: [PATCH 002/204] chore: reverse order of task_groups input param; improve comments --- .../shared/services/task/task.service.spec.ts | 4 +-- src/app/shared/services/task/task.service.ts | 33 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index 38b5580de..24f420878 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -206,9 +206,9 @@ describe("TaskService", () => { }); it("parses task group refs to TaskGroup array", () => { const taskGroupRefs = [ - "debug_task_group_c", - "debug_task_group_b.b_2", "debug_task_group_a.a_1", + "debug_task_group_b.b_2", + "debug_task_group_c", ]; const taskGroups = service.parseTaskGroupRefsToTaskGroups(taskGroupRefs); expect(taskGroups).toEqual({ diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index 8b2fee35a..98e09d155 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -227,12 +227,20 @@ export class TaskService extends AsyncServiceBase { const [actionId] = args; const childActions = { /** - * HACK: evaluate completion status of task groups. ... + * HACK: evaluate completion status of task groups. Pending an overhaul of the task system, + * this action offers a way to evaluate task group completion status over multiple nested task groups. + * + * @param {string} task_groups A space-separated list of task group references in the format "parentDataListName.rowId", + * starting with the highest level task group and working down to the lowest level. + * The final entry is just a data list name corresponding the the list of subtasks of the last child task group */ evaluate_group_completion: async () => { const taskGroupRefs = params.task_groups.split(" "); - console.log("taskGroupRefs", taskGroupRefs); const nestedTaskGroups = this.parseTaskGroupRefsToTaskGroups(taskGroupRefs); + console.log( + "[TASK] Debug - evaluating task group completion status. Nested task groups object:", + nestedTaskGroups + ); await this.evaluateTaskGroupCompletion(nestedTaskGroups); }, }; @@ -248,16 +256,16 @@ export class TaskService extends AsyncServiceBase { /** * Parses an array of task group references into a nested TaskGroup object structure. * - * Each task group reference is expected to be in the format "parentDataListName.rowId", where - * The function processes the references in reverse order to build the nested TaskGroup - * objects, linking each task group to its parent appropriately. + * Each task group reference is expected to be in the format "parentDataListName.rowId". + * This method processes the references in order to build the nested TaskGroup object, + * linking each task group to its parent appropriately. * * @param {string[]} taskGroupRefs - An array of task group references in the format "parentDataListName.rowId", - * starting with the initial subtasks data list name - * @returns {TaskGroup} - The TaskGroup object corresponding to the last child task group, with anyu parent task groups nested within it. + * where the final entry is just a data list name corresponding the the list of subtasks of the last child task group. + * @returns {TaskGroup} - The TaskGroup object corresponding to the last child task group, including any parent task groups nested within it. * * @example - * For taskGroupRefs = ["debug_task_group_c", "debug_task_group_b.b_2", "debug_task_group_a.a_1"]; + * For taskGroupRefs = ["debug_task_group_a.a_1","debug_task_group_b.b_2", "debug_task_group_c"]; * the method will return: * { * subtasksDataListName: "debug_task_group_c", @@ -274,14 +282,13 @@ export class TaskService extends AsyncServiceBase { public parseTaskGroupRefsToTaskGroups(taskGroupRefs: string[]): TaskGroup { let parentTaskGroup: TaskGroup | undefined; - // Iterate over the references in reverse order, stopping before first entry - for (let i = taskGroupRefs.length - 1; i > 0; i--) { + // Iterate over the references in order, stopping before last entry + for (let i = 0; i < taskGroupRefs.length - 1; i++) { const ref = taskGroupRefs[i]; const [parentDataListName, rowId] = ref.split("."); - // Create the TaskGroup object const taskGroup: TaskGroup = { - subtasksDataListName: taskGroupRefs[i - 1].split(".")[0], + subtasksDataListName: taskGroupRefs[i + 1].split(".")[0], rowId, parentDataListName, parentTaskGroup, @@ -291,7 +298,7 @@ export class TaskService extends AsyncServiceBase { parentTaskGroup = taskGroup; } - // The last child TaskGroup is the last one created + // The TaskGroup representing the last child, including its nested parents, is the last one created return parentTaskGroup; } From 2f0343ed89169fa6e5b032b577c99ac64b5eca34 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 11 Jun 2024 15:39:35 +0100 Subject: [PATCH 003/204] chore: code tidy --- src/app/shared/services/task/task.service.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index 98e09d155..cc286daa8 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -235,6 +235,10 @@ export class TaskService extends AsyncServiceBase { * The final entry is just a data list name corresponding the the list of subtasks of the last child task group */ evaluate_group_completion: async () => { + if (!params.task_groups) + return console.warn( + "[TASK] evaluate_group_completion action - To evaluate task group completion, a list of task group references must be provided via the task_groups param" + ); const taskGroupRefs = params.task_groups.split(" "); const nestedTaskGroups = this.parseTaskGroupRefsToTaskGroups(taskGroupRefs); console.log( @@ -365,19 +369,3 @@ export class TaskService extends AsyncServiceBase { // return this.evaluateHighlightedTaskGroup(); // } } - -const taskGroupA: TaskGroup = { - subtasksDataListName: "debug_task_group_b", - parentDataListName: "debug_task_group_a", - rowId: "a_1", -}; - -// e.g. -const taskGroupB: TaskGroup = { - subtasksDataListName: "debug_task_group_c", - rowId: "b_2", - parentDataListName: "debug_task_group_b", - parentTaskGroup: taskGroupA, -}; - -// Alternatively, could remove the subtasksDataListName property and somehow read it in at the bottom level... From 7ed477c14bf4866e893a06b8a9725e3d049ac8be Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 24 Jun 2024 10:26:52 +0100 Subject: [PATCH 004/204] wip: refactor task group completion action --- src/app/shared/services/task/task.service.ts | 80 ++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index cc286daa8..a6034a239 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -12,6 +12,9 @@ import { DynamicDataService } from "../dynamic-data/dynamic-data.service"; export type IProgressStatus = "notStarted" | "inProgress" | "completed"; // This is the definition of a task: a row of a data list that has a "completed" column export type TaskRow = FlowTypes.Data_listRow<{ completed: boolean }>; +// Or "TaskGroupRow?" +export type TaskRowWithChildTasks = TaskRow & { task_child: string }; + // A task group consists of a task row (identified here by a parentDataListName and rowId) and a list of subtasks interface TaskGroup { /** The name of the data list that contains the subtasks that constitute the task group */ @@ -247,6 +250,18 @@ export class TaskService extends AsyncServiceBase { ); await this.evaluateTaskGroupCompletion(nestedTaskGroups); }, + evaluate: async () => { + const { data_list_name, row_id } = params; + if (!data_list_name) { + return console.warn( + "[TASK] evaluate action - To evaluate task completion, a data list name must be provided via the data_list_name param" + ); + } + if (row_id) { + await this.evaluateTaskCompletion(data_list_name, row_id); + } else { + } + }, }; if (!(actionId in childActions)) { console.error("task does not have action", actionId); @@ -257,6 +272,71 @@ export class TaskService extends AsyncServiceBase { }); } + private async bulkEvaluateTaskCompletion(dataListName: string, rowId?: string) { + if (rowId) { + await this.evaluateTaskCompletion(dataListName, rowId); + } else { + const parentDataList = await this.dynamicDataService.snapshot( + "data_list", + dataListName + ); + for (const taskRow of parentDataList) { + await this.evaluateTaskCompletion(dataListName, taskRow.id, taskRow); + } + } + } + + /** + * Evaluate the completion status of a task based on the provided dataListName and rowId. + * Expects the task to have a "task_child" column that contains the name of the data list of subtasks + * + * @param {string} dataListName - The name of the data list that contains the task row + * @param {string} rowId - The ID of the task row to evaluate + * @param {TaskRowWithChildTasks} [taskRow] - Optionally provide task row explicitly to avoid duplicate dynamic data query + * @return {boolean} The completion status of the task group + */ + private async evaluateTaskCompletion( + dataListName: string, + rowId: string, + taskRow?: TaskRowWithChildTasks + ): Promise { + if (!taskRow) { + const parentDataList = await this.dynamicDataService.snapshot( + "data_list", + dataListName + ); + taskRow = parentDataList.find((row) => row.id === rowId); + } + + let taskGroupCompleted = taskRow.completed; + + const subtasksDataListName = taskRow?.task_child; + if (!subtasksDataListName) { + console.warn( + `[TASK] evaluate - row "${rowId}" in "${dataListName}" has no child tasks to evaluate` + ); + } else { + const taskGroupData = await this.dynamicDataService.snapshot( + "data_list", + subtasksDataListName + ); + taskGroupCompleted = taskGroupData.every((row) => row.completed); + + // Update completion of task group in dynamic data + await this.dynamicDataService.update("data_list", dataListName, rowId, { + completed: taskGroupCompleted, + }); + + // Support legacy task group implementation, where task completion is tracked in fields + const taskGroupCompletedField = taskRow?.["completed_field"]; + if (taskGroupCompletedField) { + await this.setTaskGroupCompletedStatus(taskGroupCompletedField, taskGroupCompleted); + this.evaluateHighlightedTaskGroup(); + } + } + return taskGroupCompleted; + } + /** * Parses an array of task group references into a nested TaskGroup object structure. * From 36a9eaebedb14dfd3544327d752299a3631d7474 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 24 Jun 2024 11:21:46 +0100 Subject: [PATCH 005/204] chore: code tidy --- src/app/shared/services/task/task.service.ts | 27 ++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index a6034a239..082d3fb8f 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -260,6 +260,7 @@ export class TaskService extends AsyncServiceBase { if (row_id) { await this.evaluateTaskCompletion(data_list_name, row_id); } else { + await this.bulkEvaluateTaskCompletion(data_list_name); } }, }; @@ -272,17 +273,13 @@ export class TaskService extends AsyncServiceBase { }); } - private async bulkEvaluateTaskCompletion(dataListName: string, rowId?: string) { - if (rowId) { - await this.evaluateTaskCompletion(dataListName, rowId); - } else { - const parentDataList = await this.dynamicDataService.snapshot( - "data_list", - dataListName - ); - for (const taskRow of parentDataList) { - await this.evaluateTaskCompletion(dataListName, taskRow.id, taskRow); - } + private async bulkEvaluateTaskCompletion(dataListName: string) { + const parentDataList = await this.dynamicDataService.snapshot( + "data_list", + dataListName + ); + for (const taskRow of parentDataList) { + await this.evaluateTaskCompletion(dataListName, taskRow.id, taskRow); } } @@ -300,6 +297,7 @@ export class TaskService extends AsyncServiceBase { rowId: string, taskRow?: TaskRowWithChildTasks ): Promise { + // Fetch task row if not provided explicitly if (!taskRow) { const parentDataList = await this.dynamicDataService.snapshot( "data_list", @@ -307,10 +305,13 @@ export class TaskService extends AsyncServiceBase { ); taskRow = parentDataList.find((row) => row.id === rowId); } - + if (!taskRow) { + console.warn(`[TASK] evaluate - row "${rowId}" in "${dataListName}" not found`); + return false; + } let taskGroupCompleted = taskRow.completed; - const subtasksDataListName = taskRow?.task_child; + const subtasksDataListName = taskRow.task_child; if (!subtasksDataListName) { console.warn( `[TASK] evaluate - row "${rowId}" in "${dataListName}" has no child tasks to evaluate` From ee6b9b7e6735a663ef0f13ce6f9c0f6c130f4f52 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 24 Jun 2024 15:41:35 +0100 Subject: [PATCH 006/204] chore: remove code for WIP alternate action implementation --- src/app/shared/services/task/task.service.ts | 125 +------------------ 1 file changed, 4 insertions(+), 121 deletions(-) diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index 082d3fb8f..04cf4972b 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -12,24 +12,12 @@ import { DynamicDataService } from "../dynamic-data/dynamic-data.service"; export type IProgressStatus = "notStarted" | "inProgress" | "completed"; // This is the definition of a task: a row of a data list that has a "completed" column export type TaskRow = FlowTypes.Data_listRow<{ completed: boolean }>; -// Or "TaskGroupRow?" +/** + * A task row that includes a value for `task_child`. This value is the name of the data list that contains + * a list of subtasks: when all subtasks are completed, the parent task is considered completed + */ export type TaskRowWithChildTasks = TaskRow & { task_child: string }; -// A task group consists of a task row (identified here by a parentDataListName and rowId) and a list of subtasks -interface TaskGroup { - /** The name of the data list that contains the subtasks that constitute the task group */ - subtasksDataListName: string; - /** The row id that identifies the task row corresponding to the task group within its parent data list */ - rowId: string; - /** - * The name of the data list that contains the task row corresponding to the task group - * NB if that data list is itself a task group, this will be identical to the parentTaskGroup's subtasksDataListName - */ - parentDataListName: string; - /** If the parent data list is itself a task group, include its reference */ - parentTaskGroup?: TaskGroup; -} - @Injectable({ providedIn: "root", }) @@ -229,27 +217,6 @@ export class TaskService extends AsyncServiceBase { task: async ({ args, params }) => { const [actionId] = args; const childActions = { - /** - * HACK: evaluate completion status of task groups. Pending an overhaul of the task system, - * this action offers a way to evaluate task group completion status over multiple nested task groups. - * - * @param {string} task_groups A space-separated list of task group references in the format "parentDataListName.rowId", - * starting with the highest level task group and working down to the lowest level. - * The final entry is just a data list name corresponding the the list of subtasks of the last child task group - */ - evaluate_group_completion: async () => { - if (!params.task_groups) - return console.warn( - "[TASK] evaluate_group_completion action - To evaluate task group completion, a list of task group references must be provided via the task_groups param" - ); - const taskGroupRefs = params.task_groups.split(" "); - const nestedTaskGroups = this.parseTaskGroupRefsToTaskGroups(taskGroupRefs); - console.log( - "[TASK] Debug - evaluating task group completion status. Nested task groups object:", - nestedTaskGroups - ); - await this.evaluateTaskGroupCompletion(nestedTaskGroups); - }, evaluate: async () => { const { data_list_name, row_id } = params; if (!data_list_name) { @@ -338,90 +305,6 @@ export class TaskService extends AsyncServiceBase { return taskGroupCompleted; } - /** - * Parses an array of task group references into a nested TaskGroup object structure. - * - * Each task group reference is expected to be in the format "parentDataListName.rowId". - * This method processes the references in order to build the nested TaskGroup object, - * linking each task group to its parent appropriately. - * - * @param {string[]} taskGroupRefs - An array of task group references in the format "parentDataListName.rowId", - * where the final entry is just a data list name corresponding the the list of subtasks of the last child task group. - * @returns {TaskGroup} - The TaskGroup object corresponding to the last child task group, including any parent task groups nested within it. - * - * @example - * For taskGroupRefs = ["debug_task_group_a.a_1","debug_task_group_b.b_2", "debug_task_group_c"]; - * the method will return: - * { - * subtasksDataListName: "debug_task_group_c", - * rowId: "b_2", - * parentDataListName: "debug_task_group_b", - * parentTaskGroup: { - * subtasksDataListName: "debug_task_group_b", - * rowId: "a_1", - * parentDataListName: "debug_task_group_a", - * parentTaskGroup: undefined - * } - * } - */ - public parseTaskGroupRefsToTaskGroups(taskGroupRefs: string[]): TaskGroup { - let parentTaskGroup: TaskGroup | undefined; - - // Iterate over the references in order, stopping before last entry - for (let i = 0; i < taskGroupRefs.length - 1; i++) { - const ref = taskGroupRefs[i]; - const [parentDataListName, rowId] = ref.split("."); - - const taskGroup: TaskGroup = { - subtasksDataListName: taskGroupRefs[i + 1].split(".")[0], - rowId, - parentDataListName, - parentTaskGroup, - }; - - // Update the parentTaskGroup for the next iteration - parentTaskGroup = taskGroup; - } - - // The TaskGroup representing the last child, including its nested parents, is the last one created - return parentTaskGroup; - } - - /** - * Evaluates the completion status of a given task group, including its nested parent task groups - * @param {TaskGroup} taskGroup The task group to evaluate, included any parent task groups nested within it - * @return {Promise} A boolean indicating whether the the last child task group (the top level of the TaskGroup object) is completed. - */ - private async evaluateTaskGroupCompletion(taskGroup: TaskGroup): Promise { - const { subtasksDataListName, rowId, parentDataListName, parentTaskGroup } = taskGroup; - const taskGroupData = await this.dynamicDataService.snapshot( - "data_list", - subtasksDataListName - ); - const taskGroupCompleted = taskGroupData.every((row) => row.completed); - - // Update completion of task group in dynamic data - await this.dynamicDataService.update("data_list", parentDataListName, rowId, { - completed: taskGroupCompleted, - }); - - // Support legacy task group implementation, where task completion is tracked in fields - const taskGroupRow = ( - await this.dynamicDataService.snapshot("data_list", subtasksDataListName) - ).find((row) => row.id === rowId); - const taskGroupCompletedField = taskGroupRow?.["completed_field"]; - if (taskGroupCompletedField) { - await this.setTaskGroupCompletedStatus(taskGroupCompletedField, taskGroupCompleted); - this.evaluateHighlightedTaskGroup(); - } - - // recursively evaluate any parent task groups - if (parentTaskGroup) { - this.evaluateTaskGroupCompletion(parentTaskGroup); - } - return taskGroupCompleted; - } - /** * TODO: this is not currently implemented, and should likely be reworked as part of a broader overhaul of the task system * From c9895f488c763d84e9af7787ec8a93169d3acaf3 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 24 Jun 2024 16:24:00 +0100 Subject: [PATCH 007/204] chore: code tidy --- .../shared/services/task/task.service.spec.ts | 50 ++++++------ src/app/shared/services/task/task.service.ts | 81 +++++++++++-------- 2 files changed, 74 insertions(+), 57 deletions(-) diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index 24f420878..95531c354 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -152,7 +152,7 @@ describe("TaskService", () => { }); it("can set a task group's completed status", async () => { await service.ready(); - await service.setTaskGroupCompletedStatus( + await service.setTaskGroupCompletedField( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, true ); @@ -165,7 +165,7 @@ describe("TaskService", () => { it("completing the highlighted task causes the next highest priority task to be highlighted upon re-evaluation", async () => { await service.ready(); // Complete highlighted task - await service.setTaskGroupCompletedStatus( + await service.setTaskGroupCompletedField( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, true ); @@ -177,15 +177,15 @@ describe("TaskService", () => { it("when all tasks are completed, the highlighted task group is set to ''", async () => { await service.ready(); // Complete all tasks - await service.setTaskGroupCompletedStatus( + await service.setTaskGroupCompletedField( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, true ); - await service.setTaskGroupCompletedStatus( + await service.setTaskGroupCompletedField( MOCK_DATA.data_list[taskGroupsListName].rows[1].completed_field, true ); - await service.setTaskGroupCompletedStatus( + await service.setTaskGroupCompletedField( MOCK_DATA.data_list[taskGroupsListName].rows[2].completed_field, true ); @@ -194,7 +194,7 @@ describe("TaskService", () => { it("schedules campaign notifications on change of highlighted task", async () => { await service.ready(); // Complete highlighted task - await service.setTaskGroupCompletedStatus( + await service.setTaskGroupCompletedField( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, true ); @@ -204,23 +204,23 @@ describe("TaskService", () => { // and again on the evaluation called above expect(scheduleCampaignNotificationsSpy).toHaveBeenCalledTimes(2); }); - it("parses task group refs to TaskGroup array", () => { - const taskGroupRefs = [ - "debug_task_group_a.a_1", - "debug_task_group_b.b_2", - "debug_task_group_c", - ]; - const taskGroups = service.parseTaskGroupRefsToTaskGroups(taskGroupRefs); - expect(taskGroups).toEqual({ - subtasksDataListName: "debug_task_group_c", - rowId: "b_2", - parentDataListName: "debug_task_group_b", - parentTaskGroup: { - subtasksDataListName: "debug_task_group_b", - rowId: "a_1", - parentDataListName: "debug_task_group_a", - parentTaskGroup: undefined, - }, - }); - }); + // it("parses task group refs to TaskGroup array", () => { + // const taskGroupRefs = [ + // "debug_task_group_a.a_1", + // "debug_task_group_b.b_2", + // "debug_task_group_c", + // ]; + // const taskGroups = service.parseTaskGroupRefsToTaskGroups(taskGroupRefs); + // expect(taskGroups).toEqual({ + // subtasksDataListName: "debug_task_group_c", + // rowId: "b_2", + // parentDataListName: "debug_task_group_b", + // parentTaskGroup: { + // subtasksDataListName: "debug_task_group_b", + // rowId: "a_1", + // parentDataListName: "debug_task_group_a", + // parentTaskGroup: undefined, + // }, + // }); + // }); }); diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index 04cf4972b..561803daa 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -171,11 +171,11 @@ export class TaskService extends AsyncServiceBase { // Check whether task group has already been completed if (!this.templateFieldService.getField(completedField)) { // If not, set completed field to "true" - await this.setTaskGroupCompletedStatus(completedField, true); + await this.setTaskGroupCompletedField(completedField, true); newlyCompleted = true; } } else { - await this.setTaskGroupCompletedStatus(completedField, false); + await this.setTaskGroupCompletedField(completedField, false); if (subtasksCompleted) { progressStatus = "inProgress"; } else { @@ -186,7 +186,7 @@ export class TaskService extends AsyncServiceBase { return { subtasksTotal, subtasksCompleted, progressStatus, newlyCompleted }; } - async setTaskGroupCompletedStatus(completedField: string, isCompleted: boolean) { + async setTaskGroupCompletedField(completedField: string, isCompleted: boolean) { console.log(`Setting ${completedField} to ${isCompleted}`); await this.templateFieldService.setField(completedField, `${isCompleted}`); } @@ -251,12 +251,14 @@ export class TaskService extends AsyncServiceBase { } /** - * Evaluate the completion status of a task based on the provided dataListName and rowId. - * Expects the task to have a "task_child" column that contains the name of the data list of subtasks + * For a given parent task (a row specified by the provided dataListName and rowId), + * evaluate its completion status based upon the completion status of its child tasks: + * if all child tasks are completed, the "completed" value of parent task is set to `true`, else it is set to `false`. + * Expects the task row to have a "task_child" column that contains the name of the data list containing the child tasks. * * @param {string} dataListName - The name of the data list that contains the task row * @param {string} rowId - The ID of the task row to evaluate - * @param {TaskRowWithChildTasks} [taskRow] - Optionally provide task row explicitly to avoid duplicate dynamic data query + * @param {TaskRowWithChildTasks} [taskRow] - Optionally provide a task row explicitly to avoid duplicate query to dynamic data * @return {boolean} The completion status of the task group */ private async evaluateTaskCompletion( @@ -264,19 +266,10 @@ export class TaskService extends AsyncServiceBase { rowId: string, taskRow?: TaskRowWithChildTasks ): Promise { - // Fetch task row if not provided explicitly - if (!taskRow) { - const parentDataList = await this.dynamicDataService.snapshot( - "data_list", - dataListName - ); - taskRow = parentDataList.find((row) => row.id === rowId); - } - if (!taskRow) { - console.warn(`[TASK] evaluate - row "${rowId}" in "${dataListName}" not found`); - return false; - } - let taskGroupCompleted = taskRow.completed; + taskRow = taskRow || (await this.fetchTaskRow(dataListName, rowId)); + if (!taskRow) return false; + + let taskCompleted = taskRow.completed; const subtasksDataListName = taskRow.task_child; if (!subtasksDataListName) { @@ -284,25 +277,49 @@ export class TaskService extends AsyncServiceBase { `[TASK] evaluate - row "${rowId}" in "${dataListName}" has no child tasks to evaluate` ); } else { - const taskGroupData = await this.dynamicDataService.snapshot( + const subtasks = await this.dynamicDataService.snapshot( "data_list", subtasksDataListName ); - taskGroupCompleted = taskGroupData.every((row) => row.completed); + taskCompleted = subtasks.every((row) => row.completed); - // Update completion of task group in dynamic data - await this.dynamicDataService.update("data_list", dataListName, rowId, { - completed: taskGroupCompleted, - }); + const taskCompletedField = taskRow["completed_field"]; + await this.setTaskCompletion(dataListName, rowId, taskCompleted, taskCompletedField); + } + return taskCompleted; + } - // Support legacy task group implementation, where task completion is tracked in fields - const taskGroupCompletedField = taskRow?.["completed_field"]; - if (taskGroupCompletedField) { - await this.setTaskGroupCompletedStatus(taskGroupCompletedField, taskGroupCompleted); - this.evaluateHighlightedTaskGroup(); - } + /** Fetch task row from dynamic data */ + private async fetchTaskRow(dataListName: string, rowId: string) { + const parentDataList = await this.dynamicDataService.snapshot( + "data_list", + dataListName + ); + const taskRow = parentDataList.find((row) => row.id === rowId); + if (!taskRow) { + console.warn(`[TASK] - row "${rowId}" in "${dataListName}" not found`); + } + return taskRow || null; + } + + /** + * Update the "completed" value for a given task group. + * @param {string} completed_field - If provided, this field will also be updated to support legacy field-based functionality + * */ + private async setTaskCompletion( + dataListName: string, + rowId: string, + completed: boolean, + completed_field?: string + ) { + // Update task's "completed" value in dynamic data + await this.dynamicDataService.update("data_list", dataListName, rowId, { completed }); + + // Support legacy task group implementation, where task completion is tracked in fields + if (completed_field) { + await this.setTaskGroupCompletedField(completed_field, completed); + this.evaluateHighlightedTaskGroup(); } - return taskGroupCompleted; } /** From 7a00b251892a9c7d9e886d4169e2fb2002bea78a Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 25 Jun 2024 12:17:33 +0100 Subject: [PATCH 008/204] test: add spec tests for evaluateTaskCompletion; code tidy --- .../shared/services/task/task.service.spec.ts | 84 ++++++++++++++----- src/app/shared/services/task/task.service.ts | 31 +++---- 2 files changed, 82 insertions(+), 33 deletions(-) diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index 95531c354..7d5796c0c 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -73,6 +73,8 @@ let mockTemplateFieldService: MockTemplateFieldService; describe("TaskService", () => { let service: TaskService; let scheduleCampaignNotificationsSpy: jasmine.Spy; + let fetchTaskRowSpy: jasmine.Spy; + let fetchTaskRowsSpy: jasmine.Spy; beforeEach(async () => { scheduleCampaignNotificationsSpy = jasmine.createSpy(); @@ -107,6 +109,30 @@ describe("TaskService", () => { ], }); service = TestBed.inject(TaskService); + + fetchTaskRowSpy = spyOn(service, "fetchTaskRow").and.callFake( + (dataListName, rowId) => { + if (rowId === "validRowId") { + return Promise.resolve({ + completed: false, + task_child: "childDataList", + completed_field: "completed_field", + }); + } + return Promise.resolve(null); + } + ); + + fetchTaskRowsSpy = spyOn(service, "fetchTaskRows").and.callFake( + (dataListName) => { + if (dataListName === "childDataList") { + return Promise.resolve([{ completed: true }, { completed: true }]); + } + return Promise.resolve([]); + } + ); + + spyOn(service, "setTaskCompletion").and.resolveTo(true); }); it("should be created", () => { @@ -204,23 +230,43 @@ describe("TaskService", () => { // and again on the evaluation called above expect(scheduleCampaignNotificationsSpy).toHaveBeenCalledTimes(2); }); - // it("parses task group refs to TaskGroup array", () => { - // const taskGroupRefs = [ - // "debug_task_group_a.a_1", - // "debug_task_group_b.b_2", - // "debug_task_group_c", - // ]; - // const taskGroups = service.parseTaskGroupRefsToTaskGroups(taskGroupRefs); - // expect(taskGroups).toEqual({ - // subtasksDataListName: "debug_task_group_c", - // rowId: "b_2", - // parentDataListName: "debug_task_group_b", - // parentTaskGroup: { - // subtasksDataListName: "debug_task_group_b", - // rowId: "a_1", - // parentDataListName: "debug_task_group_a", - // parentTaskGroup: undefined, - // }, - // }); - // }); + + it("evaluate task completion: should return null if taskRow is not found", async () => { + const result = await service["evaluateTaskCompletion"]("dataList", "invalidRowId"); + expect(result).toBeNull(); + }); + it("evaluate task completion: should set parent task completion to true if all child tasks are completed", async () => { + const result = await service["evaluateTaskCompletion"]("dataList", "validRowId"); + expect(service["setTaskCompletion"]).toHaveBeenCalledWith( + "dataList", + "validRowId", + true, + "completed_field" + ); + expect(result).toBeTrue(); + }); + it("evaluate task completion: should set parent task completion to false if not all child tasks are completed", async () => { + fetchTaskRowsSpy.and.resolveTo([ + { id: "a", completed: true }, + { id: "b", completed: false }, + ]); + const result = await service["evaluateTaskCompletion"]("dataList", "validRowId"); + expect(service["setTaskCompletion"]).toHaveBeenCalledWith( + "dataList", + "validRowId", + false, + "completed_field" + ); + expect(result).toBeFalse(); + }); + it("evaluate task completion: should log a warning if task row does not have a 'task_child' property", async () => { + spyOn(console, "warn"); + fetchTaskRowSpy.and.resolveTo({ + completed: false, + }); + await service["evaluateTaskCompletion"]("dataList", "validRowId"); + expect(console.warn).toHaveBeenCalledWith( + '[TASK] evaluate - row "validRowId" in "dataList" has no child tasks to evaluate' + ); + }); }); diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index 561803daa..a8731b4ec 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -241,11 +241,8 @@ export class TaskService extends AsyncServiceBase { } private async bulkEvaluateTaskCompletion(dataListName: string) { - const parentDataList = await this.dynamicDataService.snapshot( - "data_list", - dataListName - ); - for (const taskRow of parentDataList) { + const taskRows = await this.fetchTaskRows(dataListName); + for (const taskRow of taskRows) { await this.evaluateTaskCompletion(dataListName, taskRow.id, taskRow); } } @@ -267,7 +264,7 @@ export class TaskService extends AsyncServiceBase { taskRow?: TaskRowWithChildTasks ): Promise { taskRow = taskRow || (await this.fetchTaskRow(dataListName, rowId)); - if (!taskRow) return false; + if (!taskRow) return null; let taskCompleted = taskRow.completed; @@ -277,10 +274,7 @@ export class TaskService extends AsyncServiceBase { `[TASK] evaluate - row "${rowId}" in "${dataListName}" has no child tasks to evaluate` ); } else { - const subtasks = await this.dynamicDataService.snapshot( - "data_list", - subtasksDataListName - ); + const subtasks = await this.fetchTaskRows(subtasksDataListName); taskCompleted = subtasks.every((row) => row.completed); const taskCompletedField = taskRow["completed_field"]; @@ -289,13 +283,22 @@ export class TaskService extends AsyncServiceBase { return taskCompleted; } - /** Fetch task row from dynamic data */ - private async fetchTaskRow(dataListName: string, rowId: string) { - const parentDataList = await this.dynamicDataService.snapshot( + /** Fetch task rows for a whole data list from dynamic data */ + private async fetchTaskRows(dataListName: string) { + const taskRows = await this.dynamicDataService.snapshot( "data_list", dataListName ); - const taskRow = parentDataList.find((row) => row.id === rowId); + if (!taskRows) { + console.warn(`[TASK] - data list "${dataListName}" not found`); + } + return taskRows || null; + } + + /** Fetch task row from dynamic data */ + private async fetchTaskRow(dataListName: string, rowId: string) { + const taskRows = await this.fetchTaskRows(dataListName); + const taskRow = taskRows?.find((row) => row.id === rowId); if (!taskRow) { console.warn(`[TASK] - row "${rowId}" in "${dataListName}" not found`); } From 100cda4482a6e44d684787a2026362e83031eb26 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Fri, 5 Jul 2024 12:56:43 +0100 Subject: [PATCH 009/204] fix: changing position of the close button for the pop up component --- packages/scripts/bin/app-scripts | 0 packages/scripts/bin/app-workflow | 0 .../template/components/layout/popup/popup.component.scss | 8 ++++++-- 3 files changed, 6 insertions(+), 2 deletions(-) mode change 100644 => 100755 packages/scripts/bin/app-scripts mode change 100644 => 100755 packages/scripts/bin/app-workflow diff --git a/packages/scripts/bin/app-scripts b/packages/scripts/bin/app-scripts old mode 100644 new mode 100755 diff --git a/packages/scripts/bin/app-workflow b/packages/scripts/bin/app-workflow old mode 100644 new mode 100755 diff --git a/src/app/shared/components/template/components/layout/popup/popup.component.scss b/src/app/shared/components/template/components/layout/popup/popup.component.scss index b8704ea61..be35048d9 100644 --- a/src/app/shared/components/template/components/layout/popup/popup.component.scss +++ b/src/app/shared/components/template/components/layout/popup/popup.component.scss @@ -18,6 +18,10 @@ .popup-container { height: var(--safe-area-height); } + .close-button { + top: 8px; + right: 10px; + } } .popup-content { margin: 30px auto; @@ -39,8 +43,8 @@ } .close-button { position: absolute; - top: 16px; - right: 22px; + top: 14px; + right: 14px; background: white; width: 40px; height: 40px; From 9f62b4ebada3a0d3cb2afcb2f3cda2db79a8580e Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Fri, 5 Jul 2024 13:11:09 +0100 Subject: [PATCH 010/204] Changed the dimensions of th close button to better suit the requirements --- .../template/components/layout/popup/popup.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/layout/popup/popup.component.scss b/src/app/shared/components/template/components/layout/popup/popup.component.scss index be35048d9..00b1b45af 100644 --- a/src/app/shared/components/template/components/layout/popup/popup.component.scss +++ b/src/app/shared/components/template/components/layout/popup/popup.component.scss @@ -19,7 +19,7 @@ height: var(--safe-area-height); } .close-button { - top: 8px; + top: 10px; right: 10px; } } From cfecb0672d699e7af490be721cfa119dcce376f1 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Mon, 8 Jul 2024 09:33:09 +0100 Subject: [PATCH 011/204] Found the source that needs to be changed --- .../template/components/toggle-bar/toggle-bar.html | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index 68a782627..ccb64f657 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -9,14 +9,12 @@
- + +
From 9fd9ffe5cf249049d0519c142672112abd492678 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Mon, 8 Jul 2024 10:47:49 +0100 Subject: [PATCH 012/204] Made changes, however, I am not too sure where to proceed with this. --- .../components/template/components/toggle-bar/toggle-bar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index ccb64f657..efbb06560 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -9,10 +9,10 @@
From cc8f2abecd71bb6d67762f82764e1e818aa660e8 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 10 Jul 2024 09:54:21 +0100 Subject: [PATCH 013/204] feat: Written regex to be in the parser. It should remove whitespace. --- .../processors/flowParser/parsers/default.parser.spec.ts | 2 +- .../convert/processors/flowParser/parsers/default.parser.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index d3b268069..186cac5fe 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -1,3 +1,3 @@ describe("default Parser", () => { - it("TODO - add tests", () => expect(true).toEqual(true)); + it("should be able to remove whitespace", () => expect(true).toEqual(true)); }); diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts index 30a8786a1..ed3d03caf 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts @@ -285,6 +285,9 @@ class RowProcessor { if (typeof this.row[field] === "string") { // remove whitespace this.row[field] = this.row[field].trim(); + + // remove all line breaks in the field using replaceAll() and regex + this.row[field] = this.row[field].replaceAll(/^((\n)*|(\r)*|(\n\r)*)+$/gm, ""); } }); } From cd1b4c5ce7d6eceb4cb2729b26cdb94544287d99 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 10 Jul 2024 11:01:35 +0100 Subject: [PATCH 014/204] feat: Created test data --- .../flowParser/parsers/default.parser.spec.ts | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index 186cac5fe..c95e884cc 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -1,3 +1,41 @@ +import chalk from "chalk"; +import { FlowTypes } from "data-models"; +import { ActiveDeployment } from "../../../../../deployment/get"; +import { FlowParserProcessor } from "../flowParser"; +import { DefaultParser } from "./default.parser"; + +// class MockFlowParserProcessor extends FlowParserProcessor { +// constructor() { +// super({} as ActiveDeployment); +// } +// } + describe("default Parser", () => { - it("should be able to remove whitespace", () => expect(true).toEqual(true)); + +}); + +describe("RowProcessor", () => { + let flow: FlowTypes.FlowTypeWithData; + let parser: DefaultParser; + + beforeEach(() => { + parser = new DefaultParser(new MockFlowParserProcessor()); + const mockRow = { + type: 'test', + field1: ' value with spaces ', // leading and trailing whitespace + field2: '\n\nvalue with line breaks\n\n', // whitespace and newline + field3: ' \n\n ', // whitespace and newline + field4: '\n\r', // newline and carriage return + field5: '\r', // carriage return + field6: '\t', // tab + field7: '\r\rvalue with carriage returns' // carriage returns + }; + + + }); + + // it should define first check if the row is just whitespace + it("should remove any whitespace within a row", () => { + + }); }); From e90d94f43db1132d1ab7cf76b21886b02768a5bb Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 10 Jul 2024 11:50:01 +0100 Subject: [PATCH 015/204] feat: Started adding the test code --- .../processors/flowParser/parsers/default.parser.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index c95e884cc..462a2d83e 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -19,7 +19,7 @@ describe("RowProcessor", () => { let parser: DefaultParser; beforeEach(() => { - parser = new DefaultParser(new MockFlowParserProcessor()); + parser = new DefaultParser(new FlowParserProcessor()); const mockRow = { type: 'test', field1: ' value with spaces ', // leading and trailing whitespace @@ -31,6 +31,8 @@ describe("RowProcessor", () => { field7: '\r\rvalue with carriage returns' // carriage returns }; + rowProcessor = new RowProcessor(mockRow, parser, ); + }); From 598cb97b0da3c56e323fad973bb03eadc753fd17 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 10 Jul 2024 11:57:51 +0100 Subject: [PATCH 016/204] feat: Added rowProcessor, it must be working, will now continue test script. --- .../processors/flowParser/parsers/default.parser.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index 462a2d83e..cab11f08d 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -3,6 +3,7 @@ import { FlowTypes } from "data-models"; import { ActiveDeployment } from "../../../../../deployment/get"; import { FlowParserProcessor } from "../flowParser"; import { DefaultParser } from "./default.parser"; +import { RowProcessor } from "./row.processor"; // class MockFlowParserProcessor extends FlowParserProcessor { // constructor() { @@ -31,9 +32,9 @@ describe("RowProcessor", () => { field7: '\r\rvalue with carriage returns' // carriage returns }; - rowProcessor = new RowProcessor(mockRow, parser, ); - + const rowProcessor = new RowProcessor(mockRow, this, undefined); + rowProcesso }); // it should define first check if the row is just whitespace From ddbe79771717ed0aae39af06c155434b006eb25c Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 10 Jul 2024 12:07:49 +0100 Subject: [PATCH 017/204] feat: Noticed that I need to find a way to individually test row processor, or find a new way to test everything for the testing script --- .../processors/flowParser/parsers/default.parser.spec.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index cab11f08d..1746aa0ed 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -3,7 +3,6 @@ import { FlowTypes } from "data-models"; import { ActiveDeployment } from "../../../../../deployment/get"; import { FlowParserProcessor } from "../flowParser"; import { DefaultParser } from "./default.parser"; -import { RowProcessor } from "./row.processor"; // class MockFlowParserProcessor extends FlowParserProcessor { // constructor() { @@ -32,10 +31,9 @@ describe("RowProcessor", () => { field7: '\r\rvalue with carriage returns' // carriage returns }; - const rowProcessor = new RowProcessor(mockRow, this, undefined); - - rowProcesso - }); + const rowProcessor = new RowProcessor(mockRow, this, undefined); + +}); // it should define first check if the row is just whitespace it("should remove any whitespace within a row", () => { From 1ff3cc759195ee1483c90147d7971f16eda91942 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 10 Jul 2024 12:43:08 +0100 Subject: [PATCH 018/204] feat: restart of writing the test script --- .../flowParser/parsers/default.parser.spec.ts | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index 1746aa0ed..ca89d574f 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -4,39 +4,7 @@ import { ActiveDeployment } from "../../../../../deployment/get"; import { FlowParserProcessor } from "../flowParser"; import { DefaultParser } from "./default.parser"; -// class MockFlowParserProcessor extends FlowParserProcessor { -// constructor() { -// super({} as ActiveDeployment); -// } -// } - describe("default Parser", () => { }); -describe("RowProcessor", () => { - let flow: FlowTypes.FlowTypeWithData; - let parser: DefaultParser; - - beforeEach(() => { - parser = new DefaultParser(new FlowParserProcessor()); - const mockRow = { - type: 'test', - field1: ' value with spaces ', // leading and trailing whitespace - field2: '\n\nvalue with line breaks\n\n', // whitespace and newline - field3: ' \n\n ', // whitespace and newline - field4: '\n\r', // newline and carriage return - field5: '\r', // carriage return - field6: '\t', // tab - field7: '\r\rvalue with carriage returns' // carriage returns - }; - - const rowProcessor = new RowProcessor(mockRow, this, undefined); - -}); - - // it should define first check if the row is just whitespace - it("should remove any whitespace within a row", () => { - - }); -}); From f99665f2d47a04d9f4022f2d6e54fe70af05dd8f Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Fri, 12 Jul 2024 17:21:54 +0100 Subject: [PATCH 019/204] Created test data --- .../flowParser/parsers/default.parser.spec.ts | 137 +++++++++++++++++- .../flowParser/parsers/default.parser.ts | 6 +- 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index ca89d574f..7370307ee 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -4,7 +4,142 @@ import { ActiveDeployment } from "../../../../../deployment/get"; import { FlowParserProcessor } from "../flowParser"; import { DefaultParser } from "./default.parser"; +// NEXT STEPS to consider: make a mock of the rows of data that will be passed to the parser +// and then somehow find a way to perform, not only a run, but a test on the output for each result +// basically, would be quite long but knowing how to do so, it should be way more reassuring + +// Flow { +// flow_name: 'example_pipe_sort_desc', +// flow_subtype: 'generated', +// flow_type: 'data_list', +// rows: [ +// { +// id: 'id_2', +// value: 2, +// sort_order: 11, +// sort_alpha: 'basis', +// completed: false +// }, +// { +// id: 'id_1', +// value: 1, +// sort_order: 7, +// sort_alpha: 'bonus', +// completed: true +// }, +// { id: 'id_3', value: 3, sort_order: 6, sort_alpha: 'topic' }, +// { id: 'id_4', value: 4, sort_order: 5, sort_alpha: 'world' }, +// { +// id: 'id_5', +// value: 5, +// sort_order: 4, +// sort_alpha: 'event', +// completed: true +// }, +// { +// id: 'id_8', +// value: 8, +// sort_order: 3, +// sort_alpha: 'skill', +// completed: false +// }, +// { +// id: 'id_7', +// value: 7, +// sort_order: 2, +// sort_alpha: 'guest', +// completed: false +// }, +// { +// id: 'id_6', +// value: 6, +// sort_order: 1, +// sort_alpha: 'photo', +// completed: true +// } +// ] +// } +const testFlowInput: FlowTypes.FlowTypeWithData = { + flow_name: "test_flow_input_empty", + flow_subtype: "generator", + flow_type: "generated", + rows: [ + { + type: "text", + name: "empty", + value: "", + }, + { + type: "text", + name: "tab_empty", + value: "\t\t", + }, + { + type: "text", + name: "newline_empty", + value: "\n\n", + }, + { + type: "text", + name: "return_empty", + value: "\r\r", + }, + { + type: "text", + name: "newline_return_empty", + value: "\r\n\n\r", + }, + ], +}; + +const testFlowInputNonEmpty: FlowTypes.FlowTypeWithData = { + flow_name: "test_flow_input_nonempty", + flow_subtype: "generator", + flow_type: "generated", + rows: [ + { + type: "text", + name: "spaces", + value: "this is a test string with spaces", + }, + { + type: "text", + name: "tab_and_spaces", + value: "\tthis is a test string with spaces and tabs\t", + }, + { + type: "text", + name: "newline_not_empty" + value: "Cats and dogs are the best pets\nTrue?", + }, + { + type: "text", + name: "newline_return_not_empty", + value: "Cats are cool\rDogs are great\nPersonally...\nI prefer hamsters\r", + }, + { + type: "text", + name: "return_not_empty", + value: "\this is a test string with spaces and tabs\t", + }, + ] +}; + + describe("default Parser", () => { - + it('should remove any spaces and tabs from the input string', () => { + const expectedOutput = 'thisisateststringwithspacesandtabs'; // this is the expected output + + // not possible to use the actual objects as the function used is not exported + expect(input.trim()).toBe(expectedOutput); // this is the actual operation that will be tested + }); + + it('should remove any line breaks from the input string, if it is empty', () => { + const input1 = 'this is a test string with line breaks\n\n'; + const expectedOutput1 = 'this is a test string with line breaks'; // this is the expected output + + const input2 = '\n\n\n'; + const expectedOutput2 = ''; // this is the expected output + }); }); diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts index ed3d03caf..8c03404b9 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts @@ -36,6 +36,8 @@ export class DefaultParser< /** Default function to call a start the process of parsing rows */ public run(flow: FlowTypes.FlowTypeWithData): FlowTypes.FlowTypeWithData { this.flow = JSON.parse(JSON.stringify(flow)); + + console.log("Flow", this.flow) this.queue = flow.rows; const processedRows = []; // If first row specifies default values extract them and remove row from queue @@ -287,7 +289,9 @@ class RowProcessor { this.row[field] = this.row[field].trim(); // remove all line breaks in the field using replaceAll() and regex - this.row[field] = this.row[field].replaceAll(/^((\n)*|(\r)*|(\n\r)*)+$/gm, ""); + if (!this.row[field].match(/\S/)) { + this.row[field] = ""; + } } }); } From 182b754befb5984fc2f6502c7f9a4ceb9227bed1 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Fri, 12 Jul 2024 17:39:24 +0100 Subject: [PATCH 020/204] feat: Starting to write how the parser should be formed. Testing will resume soon --- .../processors/flowParser/parsers/default.parser.spec.ts | 2 +- .../convert/processors/flowParser/parsers/default.parser.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index 7370307ee..aa0f3f0d5 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -128,7 +128,7 @@ const testFlowInputNonEmpty: FlowTypes.FlowTypeWithData = { describe("default Parser", () => { it('should remove any spaces and tabs from the input string', () => { - const expectedOutput = 'thisisateststringwithspacesandtabs'; // this is the expected output + const parser: DefaultParser = new DefaultParser; // not possible to use the actual objects as the function used is not exported expect(input.trim()).toBe(expectedOutput); // this is the actual operation that will be tested diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts index 8c03404b9..1ea59a698 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts @@ -37,7 +37,7 @@ export class DefaultParser< public run(flow: FlowTypes.FlowTypeWithData): FlowTypes.FlowTypeWithData { this.flow = JSON.parse(JSON.stringify(flow)); - console.log("Flow", this.flow) + console.log("Flow before:", this.flow) this.queue = flow.rows; const processedRows = []; // If first row specifies default values extract them and remove row from queue @@ -71,6 +71,9 @@ export class DefaultParser< } this.flow.rows = processedRows; this.flow = this.postProcessFlow(this.flow); + + console.log("Flow after", this.flow) + return this.flow; } From 874d078dee5aa9a102f8ab64dacbd92fd66656b5 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Mon, 15 Jul 2024 15:41:59 +0100 Subject: [PATCH 021/204] fix: adding the expected output, figuring the kinks of it --- .../flowParser/parsers/default.parser.spec.ts | 137 ++++++------------ 1 file changed, 43 insertions(+), 94 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index aa0f3f0d5..1dfb3cbdf 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -1,98 +1,9 @@ -import chalk from "chalk"; import { FlowTypes } from "data-models"; -import { ActiveDeployment } from "../../../../../deployment/get"; -import { FlowParserProcessor } from "../flowParser"; import { DefaultParser } from "./default.parser"; -// NEXT STEPS to consider: make a mock of the rows of data that will be passed to the parser -// and then somehow find a way to perform, not only a run, but a test on the output for each result -// basically, would be quite long but knowing how to do so, it should be way more reassuring +const testFlowInputEmpty = ; -// Flow { -// flow_name: 'example_pipe_sort_desc', -// flow_subtype: 'generated', -// flow_type: 'data_list', -// rows: [ -// { -// id: 'id_2', -// value: 2, -// sort_order: 11, -// sort_alpha: 'basis', -// completed: false -// }, -// { -// id: 'id_1', -// value: 1, -// sort_order: 7, -// sort_alpha: 'bonus', -// completed: true -// }, -// { id: 'id_3', value: 3, sort_order: 6, sort_alpha: 'topic' }, -// { id: 'id_4', value: 4, sort_order: 5, sort_alpha: 'world' }, -// { -// id: 'id_5', -// value: 5, -// sort_order: 4, -// sort_alpha: 'event', -// completed: true -// }, -// { -// id: 'id_8', -// value: 8, -// sort_order: 3, -// sort_alpha: 'skill', -// completed: false -// }, -// { -// id: 'id_7', -// value: 7, -// sort_order: 2, -// sort_alpha: 'guest', -// completed: false -// }, -// { -// id: 'id_6', -// value: 6, -// sort_order: 1, -// sort_alpha: 'photo', -// completed: true -// } -// ] -// } -const testFlowInput: FlowTypes.FlowTypeWithData = { - flow_name: "test_flow_input_empty", - flow_subtype: "generator", - flow_type: "generated", - rows: [ - { - type: "text", - name: "empty", - value: "", - }, - { - type: "text", - name: "tab_empty", - value: "\t\t", - }, - { - type: "text", - name: "newline_empty", - value: "\n\n", - }, - { - type: "text", - name: "return_empty", - value: "\r\r", - }, - { - type: "text", - name: "newline_return_empty", - value: "\r\n\n\r", - }, - ], -}; - -const testFlowInputNonEmpty: FlowTypes.FlowTypeWithData = { +const testFlowInputNonEmpty = { flow_name: "test_flow_input_nonempty", flow_subtype: "generator", flow_type: "generated", @@ -125,13 +36,51 @@ const testFlowInputNonEmpty: FlowTypes.FlowTypeWithData = { ] }; +const testInputSources = { + data_list: { test_data_list: [{ id: 1 }, { id: 2 }, { id: 3 }] }, +}; + describe("default Parser", () => { it('should remove any spaces and tabs from the input string', () => { - const parser: DefaultParser = new DefaultParser; - + const parser = new DefaultParser({ processedFlowHashmap: testInputSources } as any); + const output = parser.run({ + flow_name: "test_flow_input_empty", + flow_subtype: "generator", + flow_type: "generated", + rows: [ + { + type: "text", + name: "empty", + value: "", + }, + { + type: "text", + name: "tab_empty", + value: "\t\t", + }, + { + type: "text", + name: "newline_empty", + value: "\n\n", + }, + { + type: "text", + name: "return_empty", + value: "\r\r", + }, + { + type: "text", + name: "newline_return_empty", + value: "\r\n\n\r", + }, + ], + }) as FlowTypes.FlowTypeWithData; // not possible to use the actual objects as the function used is not exported - expect(input.trim()).toBe(expectedOutput); // this is the actual operation that will be tested + expect(output.rows).toEqual({ + + } + ); // this is the actual operation that will be tested }); it('should remove any line breaks from the input string, if it is empty', () => { From 3b3a046b1843fd3c16ee5ee5401670828337aa8f Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Mon, 15 Jul 2024 17:38:24 +0100 Subject: [PATCH 022/204] Completed test script: will resume tsting if it works the next day --- .../flowParser/parsers/default.parser.spec.ts | 105 ++++++++++-------- .../flowParser/parsers/default.parser.ts | 4 +- 2 files changed, 63 insertions(+), 46 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index 1dfb3cbdf..7ca16928f 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -1,41 +1,6 @@ import { FlowTypes } from "data-models"; import { DefaultParser } from "./default.parser"; -const testFlowInputEmpty = ; - -const testFlowInputNonEmpty = { - flow_name: "test_flow_input_nonempty", - flow_subtype: "generator", - flow_type: "generated", - rows: [ - { - type: "text", - name: "spaces", - value: "this is a test string with spaces", - }, - { - type: "text", - name: "tab_and_spaces", - value: "\tthis is a test string with spaces and tabs\t", - }, - { - type: "text", - name: "newline_not_empty" - value: "Cats and dogs are the best pets\nTrue?", - }, - { - type: "text", - name: "newline_return_not_empty", - value: "Cats are cool\rDogs are great\nPersonally...\nI prefer hamsters\r", - }, - { - type: "text", - name: "return_not_empty", - value: "\this is a test string with spaces and tabs\t", - }, - ] -}; - const testInputSources = { data_list: { test_data_list: [{ id: 1 }, { id: 2 }, { id: 3 }] }, }; @@ -77,18 +42,70 @@ describe("default Parser", () => { ], }) as FlowTypes.FlowTypeWithData; // not possible to use the actual objects as the function used is not exported - expect(output.rows).toEqual({ - - } - ); // this is the actual operation that will be tested + expect(output.rows).toEqual([]); }); it('should remove any line breaks from the input string, if it is empty', () => { - const input1 = 'this is a test string with line breaks\n\n'; - const expectedOutput1 = 'this is a test string with line breaks'; // this is the expected output - - const input2 = '\n\n\n'; - const expectedOutput2 = ''; // this is the expected output + const parser = new DefaultParser({ processedFlowHashmap: testInputSources } as any); + const output = parser.run({ + flow_name: "test_flow_input_nonempty", + flow_subtype: "generator", + flow_type: "generated", + rows: [ + { + type: "text", + name: "spaces", + value: "this is a test string with spaces", + }, + { + type: "text", + name: "tab_and_spaces", + value: "\tthis is a test string with spaces and tabs\t", + }, + { + type: "text", + name: "newline_not_empty", + value: "Cats and dogs are the best pets\nTrue?", + }, + { + type: "text", + name: "newline_return_not_empty", + value: "Cats are cool\rDogs are great\nPersonally...\nI prefer hamsters\r", + }, + { + type: "text", + name: "return_not_empty", + value: "\this is a test string with spaces and tabs\t", + }, + ] + }) as FlowTypes.FlowTypeWithData; + expect(output.rows).toBe([ + { + type: "text", + name: "spaces", + value: "this is a test string with spaces", + }, + { + type: "text", + name: "tab_and_spaces", + value: "\tthis is a test string with spaces and tabs\t", + }, + { + type: "text", + name: "newline_not_empty", + value: "Cats and dogs are the best pets\nTrue?", + }, + { + type: "text", + name: "newline_return_not_empty", + value: "Cats are cool\rDogs are great\nPersonally...\nI prefer hamsters\r", + }, + { + type: "text", + name: "return_not_empty", + value: "\this is a test string with spaces and tabs\t", + }, + ]); }); }); diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts index 1ea59a698..2b407fac8 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts @@ -37,7 +37,7 @@ export class DefaultParser< public run(flow: FlowTypes.FlowTypeWithData): FlowTypes.FlowTypeWithData { this.flow = JSON.parse(JSON.stringify(flow)); - console.log("Flow before:", this.flow) + console.log("Flow before:", this.flow.rows) this.queue = flow.rows; const processedRows = []; // If first row specifies default values extract them and remove row from queue @@ -72,7 +72,7 @@ export class DefaultParser< this.flow.rows = processedRows; this.flow = this.postProcessFlow(this.flow); - console.log("Flow after", this.flow) + console.log("Flow after", this.flow.rows) return this.flow; } From aa211430a66eec7e3f4ff2fac00ceb4007d7c241 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Tue, 16 Jul 2024 15:18:34 +0100 Subject: [PATCH 023/204] Completed test script: verifying that everything works --- .../flowParser/parsers/default.parser.spec.ts | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index 7ca16928f..5819f01d5 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -6,8 +6,8 @@ const testInputSources = { }; -describe("default Parser", () => { - it('should remove any spaces and tabs from the input string', () => { +describe("Default Parser", () => { + it('Cleans field values - removes whitespace within empty strings', () => { const parser = new DefaultParser({ processedFlowHashmap: testInputSources } as any); const output = parser.run({ flow_name: "test_flow_input_empty", @@ -42,10 +42,36 @@ describe("default Parser", () => { ], }) as FlowTypes.FlowTypeWithData; // not possible to use the actual objects as the function used is not exported - expect(output.rows).toEqual([]); + expect(output.rows).toEqual([ + { + type: "text", + name: "empty", + value: "", + }, + { + type: "text", + name: "tab_empty", + value: "", + }, + { + type: "text", + name: "newline_empty", + value: "", + }, + { + type: "text", + name: "return_empty", + value: "", + }, + { + type: "text", + name: "newline_return_empty", + value: "", + }, + ]); }); - it('should remove any line breaks from the input string, if it is empty', () => { + it('Cleans field values - Replaces string consisting of only line breaks and new lines', () => { const parser = new DefaultParser({ processedFlowHashmap: testInputSources } as any); const output = parser.run({ flow_name: "test_flow_input_nonempty", @@ -79,7 +105,7 @@ describe("default Parser", () => { }, ] }) as FlowTypes.FlowTypeWithData; - expect(output.rows).toBe([ + expect(output.rows).toEqual([ { type: "text", name: "spaces", From 8b3d54e5353889004d176b15503e052ec869fa40 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Tue, 16 Jul 2024 20:40:58 +0100 Subject: [PATCH 024/204] feat: Migrating tick and cross icons to modern syntax --- .../components/toggle-bar/toggle-bar.html | 4 +-- .../components/toggle-bar/toggle-bar.scss | 28 ++++--------------- .../components/toggle-bar/toggle-bar.ts | 3 +- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index efbb06560..1fde2e237 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -9,10 +9,10 @@
diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss index 4dcaa58ea..0d2f18311 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss @@ -9,34 +9,13 @@ ion-toggle { ion-toggle { --track-background: #b4b7b7; - --handle-spacing: 2px; + --handle-spacing: 0px; --track-background-checked: var(--ion-color-primary, "darkblue"); --handle-background: var(--ion-item-background, #fefefe); --handle-background-checked: #fff; --padding-inline: $togglePadding; } -.show-tick-cross { - $iconPadding: 3px; - ion-toggle[aria-checked="true"]::before, - ion-toggle[aria-checked="false"]::after { - position: absolute; - top: $togglePadding + $iconPadding; - left: $togglePadding + $iconPadding; - color: white; - z-index: 1; - content: " "; - width: $toggleWidth - 2 * $iconPadding; - height: $toggleHeight - 2 * $iconPadding; - } - ion-toggle[aria-checked="true"]::before { - background: url(/assets/icon/shared/tick.svg) no-repeat left / contain; - } - ion-toggle[aria-checked="false"]::after { - background: url(/assets/icon/shared/cross.svg) no-repeat right / contain; - } -} - .container { position: relative; display: flex; @@ -90,3 +69,8 @@ ion-toggle { align-self: baseline; margin-top: 5px; } + +.native-wrapper .toggle-icon { + width: 36px; + height: $toggleHeight; +} diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts index 92e2ae2b9..2147e0703 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts @@ -11,8 +11,9 @@ interface IToggleParams { variant: "" | "icon" | "in_button"; /** TEMPLATE PARAMETER: "style". Legacy, use "variant" instead. */ style: string; - /** TEMPLATE PARAMETER: "show_tick_and_cross" */ + /** TEMPLATE PARAMETER: "show_tick_and_cross". Legacy, use "show-icons" instead */ showTickAndCross: boolean; + showIcons: boolean; /** TEMPLATE PARAMETER: "position". Default "left" */ position: "left" | "center" | "right"; /** TEMPLATE PARAMETER: "false_text". Label text to display when value is false */ From b98da308db5e75ab23fb937729e132e667b6d0a3 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 17 Jul 2024 10:55:32 +0100 Subject: [PATCH 025/204] feat: Switched the mode of the toggle to ios rather than its default: md --- .../components/template/components/toggle-bar/toggle-bar.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index 1fde2e237..04a646aab 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -12,7 +12,8 @@ [class.show-tick-cross]="params.showTickAndCross" > From de8ce3634cd6b41bc9aabafabc396b3972b90931 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 17 Jul 2024 15:14:58 +0100 Subject: [PATCH 026/204] chore: added mode as a parameter with the option of being 'ios' or 'md' --- .../template/components/toggle-bar/toggle-bar.html | 2 +- .../template/components/toggle-bar/toggle-bar.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index 04a646aab..5ccfb4035 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -13,7 +13,7 @@ > diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts index 2147e0703..2df7292f4 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts @@ -14,6 +14,8 @@ interface IToggleParams { /** TEMPLATE PARAMETER: "show_tick_and_cross". Legacy, use "show-icons" instead */ showTickAndCross: boolean; showIcons: boolean; + /** TEMPLATE PARAMETER: "mode". Enables toggle to have a certain appearance .*/ + toggleMode: "md" | "ios"; /** TEMPLATE PARAMETER: "position". Default "left" */ position: "left" | "center" | "right"; /** TEMPLATE PARAMETER: "false_text". Label text to display when value is false */ @@ -67,6 +69,16 @@ export class TmplToggleBarComponent "show_tick_and_cross", true ); + this.params.showTickAndCross = getBooleanParamFromTemplateRow( + this._row, + "show_icons", + true + ); + this.params.toggleMode = getStringParamFromTemplateRow( + this._row, + "mode", + "md" + ) as IToggleParams["toggleMode"]; this.params.style = getStringParamFromTemplateRow(this._row, "style", ""); this.params.variant = getStringParamFromTemplateRow(this._row, "variant", "") .split(",") From 88c51b7e82f9826421393e9a89f611a4d2f972b9 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 17 Jul 2024 15:42:45 +0100 Subject: [PATCH 027/204] feat: new display-group variant, 'speech_bubble' --- .../display-group.component.html | 8 ++ .../display-group.component.scss | 77 ++++++++++++++++--- .../display-group/display-group.component.ts | 32 +++++--- 3 files changed, 97 insertions(+), 20 deletions(-) diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.html b/src/app/shared/components/template/components/layout/display-group/display-group.component.html index 8859c983a..38551e977 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.html +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.html @@ -4,6 +4,7 @@ [attr.data-param-style]="params.style" [attr.data-rowname]="_row.name" [attr.data-variant]="params.variant" + [attr.data-speaker-position]="params.speakerPosition" [style.marginBottom.px]="params.offset" [ngSwitch]="type" [style]="_row.style_list | styleList" @@ -28,3 +29,10 @@ +@if (params.variant.includes("speech_bubble") && params.speakerImageAsset) { + +} diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss index 986f3f959..f3026ad4f 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss @@ -1,3 +1,6 @@ +// Used for speech_bubble variant +$speaker-image-width: 84px; + :host { width: 100%; } @@ -42,24 +45,76 @@ margin: 1em 0 0 0; } -.display-group-wrapper[data-variant*="box"] { - margin-top: var(--regular-margin); - padding: var(--regular-padding); - border-radius: var(--ion-border-radius-secondary); - flex: 1; +.display-group-wrapper { + &[data-variant~="box_gray"], + &[data-variant~="box_primary"], + &[data-variant~="box_secondary"] { + margin-top: var(--regular-margin); + padding: var(--regular-padding); + border-radius: var(--ion-border-radius-secondary); + flex: 1; + background-color: var(--background-color, transparent); + border: 2px solid var(--border-color, transparent); + } &[data-variant~="box_gray"] { - background-color: var(--ion-color-gray-100); - border: 2px solid var(--ion-color-gray-300); + --background-color: var(--ion-color-gray-100); + --border-color: var(--ion-color-gray-300); } &[data-variant~="box_primary"] { - background-color: var(--ion-color-primary-200); - border: 2px solid var(--ion-color-primary-500); + --background-color: var(--ion-color-primary-200); + --border-color: var(--ion-color-primary-500); } &[data-variant~="box_secondary"] { - background-color: var(--ion-color-secondary-200); - border: 2px solid var(--ion-color-secondary-500); + --background-color: var(--ion-color-secondary-200); + --border-color: var(--ion-color-secondary-500); + } +} + +.display-group-wrapper[data-variant~="speech_bubble"] { + position: relative; + --speech-bubble-tail-offset: calc(#{$speaker-image-width} + (2 * var(--small-margin))); + + &:before, + &:after { + content: ""; + position: absolute; + width: 0; + height: 0; + } + &:before { + left: calc(var(--speech-bubble-tail-offset) - 2px); + bottom: -33px; + border: 16px solid; + border-color: var(--border-color) transparent transparent var(--border-color); + } + &:after { + left: var(--speech-bubble-tail-offset); + bottom: -28px; + border: 14px solid; + border-color: var(--background-color) transparent transparent var(--background-color); + } + + &[data-speaker-position="right"] { + &:before { + border-color: var(--border-color) var(--border-color) transparent transparent; + left: unset; + right: calc(var(--speech-bubble-tail-offset) - 2px); + } + &:after { + border-color: var(--background-color) var(--background-color) transparent transparent; + left: unset; + right: var(--speech-bubble-tail-offset); + } + } +} + +.speaker-image { + width: $speaker-image-width; + margin: var(--small-margin); + &[data-speaker-position="right"] { + float: right; } } diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.ts b/src/app/shared/components/template/components/layout/display-group/display-group.component.ts index a55449e69..902bcd408 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.ts +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.ts @@ -4,11 +4,15 @@ import { getNumberParamFromTemplateRow, getStringParamFromTemplateRow } from ".. interface IDisplayGroupParams { /** TEMPLATE PARAMETER: "variant" */ - variant: "box_gray" | "box_primary" | "box_secondary"; + variant: "box_gray" | "box_primary" | "box_secondary" | "dashed_box" | "speech_bubble"; /** TEMPLATE PARAMETER: "style". TODO: Various additional legacy styles, review and convert some to variants */ - style: "form" | "dashed_box" | "default" | string | null; + style: "form" | "default" | string | null; /** TEMPLATE PARAMETER: "offset". Add a custom bottom margin */ offset: number; + /** TEMPLATE PARAMETER: "speaker_image_asset". The path to an image to be used as the speaker for the 'speech_bubble' variant */ + speakerImageAsset: string; + /** TEMPLATE PARAMETER: "speaker_position". The position of the speaker image and speech bubble tail for the 'speech_bubble' variant. Default 'left' */ + speakerPosition: "left" | "right"; } @Component({ @@ -34,15 +38,25 @@ export class TmplDisplayGroupComponent extends TemplateBaseComponent implements this.params.offset = getNumberParamFromTemplateRow(this._row, "offset", 0); this.params.variant = getStringParamFromTemplateRow(this._row, "variant", "") .split(",") - .join(" ") as IDisplayGroupParams["variant"]; - this.type = this.getTypeFromStyles(this.params.style || ""); + .join(" ") + .concat(" " + this.params.style) as IDisplayGroupParams["variant"]; + this.params.speakerImageAsset = getStringParamFromTemplateRow( + this._row, + "speaker_image_asset", + "" + ); + this.params.speakerPosition = getStringParamFromTemplateRow( + this._row, + "speaker_position", + "left" + ) as "left" | "right"; + this.type = this.getTypeFromStyles(); } - private getTypeFromStyles(styles: string) { - if (styles) { - if (styles.includes("form")) return "form"; - if (styles.includes("dashed_box")) return "dashed_box"; - } + private getTypeFromStyles() { + if (this.params.style?.includes("form") || this.params.variant?.includes("form")) return "form"; + if (this.params.style?.includes("dashed_box") || this.params.variant?.includes("dashed_box")) + return "dashed_box"; return "default"; } } From a6a7aaf51d5149d520846c0369a3397fb269d8d1 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 17 Jul 2024 16:00:16 +0100 Subject: [PATCH 028/204] chore: handling the colours of the ion toggles --- .../template/components/toggle-bar/toggle-bar.html | 4 ++-- .../template/components/toggle-bar/toggle-bar.scss | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index 5ccfb4035..229cbc2e6 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -12,8 +12,8 @@ [class.show-tick-cross]="params.showTickAndCross" > diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss index 0d2f18311..a41685436 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss @@ -8,8 +8,8 @@ ion-toggle { } ion-toggle { - --track-background: #b4b7b7; - --handle-spacing: 0px; + --track-background: var(--ion-color-gray-200); + --handle-spacing: 1.5px; --track-background-checked: var(--ion-color-primary, "darkblue"); --handle-background: var(--ion-item-background, #fefefe); --handle-background-checked: #fff; @@ -68,9 +68,4 @@ ion-toggle { .toggle_wrapper { align-self: baseline; margin-top: 5px; -} - -.native-wrapper .toggle-icon { - width: 36px; - height: $toggleHeight; -} +} \ No newline at end of file From 1eb8bb1c32c683447215452837e7f341cec86ad3 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 17 Jul 2024 17:09:20 +0100 Subject: [PATCH 029/204] chore: fixed toggle modes, now trying to changed the icon colour --- .../components/template/components/toggle-bar/toggle-bar.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss index a41685436..c8682e374 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss @@ -14,6 +14,7 @@ ion-toggle { --handle-background: var(--ion-item-background, #fefefe); --handle-background-checked: #fff; --padding-inline: $togglePadding; + --checked-icon-color: #000000; } .container { From b75f95d5f431700a6fc041e40f332e12b848cd8a Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 17 Jul 2024 22:27:47 -0700 Subject: [PATCH 030/204] refactor: text-bubble component --- packages/data-models/flowTypes.ts | 5 +- .../components/template/components/index.ts | 3 + .../display-group.component.html | 8 --- .../display-group.component.scss | 49 ----------------- .../display-group/display-group.component.ts | 16 +----- .../text_bubble/text-bubble.component.html | 11 ++++ .../text_bubble/text-bubble.component.scss | 55 +++++++++++++++++++ .../text_bubble/text-bubble.component.ts | 32 +++++++++++ 8 files changed, 105 insertions(+), 74 deletions(-) create mode 100644 src/app/shared/components/template/components/text_bubble/text-bubble.component.html create mode 100644 src/app/shared/components/template/components/text_bubble/text-bubble.component.scss create mode 100644 src/app/shared/components/template/components/text_bubble/text-bubble.component.ts diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 9b568bfe7..9f4aaa7c7 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -293,7 +293,7 @@ export namespace FlowTypes { | "round_button" | "select_text" | "set_default" - | "set_field" // TODO - requires global implementation (and possibly rename to set_field_default as value does not override) + | "set_field" | "set_local" | "set_variable" | "simple_checkbox" @@ -305,12 +305,13 @@ export namespace FlowTypes { | "template" | "text_area" | "text_box" + | "text_bubble" | "text" | "tile_component" | "timer" | "title" | "toggle_bar" - | "update_action_list" // update own action list + | "update_action_list" | "video" | "workshops_accordion"; diff --git a/src/app/shared/components/template/components/index.ts b/src/app/shared/components/template/components/index.ts index 66ca6a9f2..973da6c8c 100644 --- a/src/app/shared/components/template/components/index.ts +++ b/src/app/shared/components/template/components/index.ts @@ -62,6 +62,7 @@ import { TmplToggleBarComponent } from "./toggle-bar/toggle-bar"; import { TmplVideoComponent } from "./video"; import { WorkshopsComponent } from "./layout/workshops_accordion"; +import { TmplTextBubbleComponent } from "./text_bubble/text-bubble.component"; /** All components should be exported as a single array for easy module import */ export const TEMPLATE_COMPONENTS = [ @@ -113,6 +114,7 @@ export const TEMPLATE_COMPONENTS = [ TmplTaskProgressBarComponent, TmplTextAreaComponent, TmplTextBoxComponent, + TmplTextBubbleComponent, TmplTextComponent, TmplTileComponent, TmplTimerComponent, @@ -183,6 +185,7 @@ export const TEMPLATE_COMPONENT_MAPPING: Record< text: TmplTextComponent, text_area: TmplTextAreaComponent, text_box: TmplTextBoxComponent, + text_bubble: TmplTextBubbleComponent, tile_component: TmplTileComponent, timer: TmplTimerComponent, title: TmplTitleComponent, diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.html b/src/app/shared/components/template/components/layout/display-group/display-group.component.html index 38551e977..8859c983a 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.html +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.html @@ -4,7 +4,6 @@ [attr.data-param-style]="params.style" [attr.data-rowname]="_row.name" [attr.data-variant]="params.variant" - [attr.data-speaker-position]="params.speakerPosition" [style.marginBottom.px]="params.offset" [ngSwitch]="type" [style]="_row.style_list | styleList" @@ -29,10 +28,3 @@ -@if (params.variant.includes("speech_bubble") && params.speakerImageAsset) { - -} diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss index f3026ad4f..210125a5b 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.scss +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.scss @@ -1,6 +1,3 @@ -// Used for speech_bubble variant -$speaker-image-width: 84px; - :host { width: 100%; } @@ -72,49 +69,3 @@ $speaker-image-width: 84px; --border-color: var(--ion-color-secondary-500); } } - -.display-group-wrapper[data-variant~="speech_bubble"] { - position: relative; - --speech-bubble-tail-offset: calc(#{$speaker-image-width} + (2 * var(--small-margin))); - - &:before, - &:after { - content: ""; - position: absolute; - width: 0; - height: 0; - } - &:before { - left: calc(var(--speech-bubble-tail-offset) - 2px); - bottom: -33px; - border: 16px solid; - border-color: var(--border-color) transparent transparent var(--border-color); - } - &:after { - left: var(--speech-bubble-tail-offset); - bottom: -28px; - border: 14px solid; - border-color: var(--background-color) transparent transparent var(--background-color); - } - - &[data-speaker-position="right"] { - &:before { - border-color: var(--border-color) var(--border-color) transparent transparent; - left: unset; - right: calc(var(--speech-bubble-tail-offset) - 2px); - } - &:after { - border-color: var(--background-color) var(--background-color) transparent transparent; - left: unset; - right: var(--speech-bubble-tail-offset); - } - } -} - -.speaker-image { - width: $speaker-image-width; - margin: var(--small-margin); - &[data-speaker-position="right"] { - float: right; - } -} diff --git a/src/app/shared/components/template/components/layout/display-group/display-group.component.ts b/src/app/shared/components/template/components/layout/display-group/display-group.component.ts index 902bcd408..2eadf8583 100644 --- a/src/app/shared/components/template/components/layout/display-group/display-group.component.ts +++ b/src/app/shared/components/template/components/layout/display-group/display-group.component.ts @@ -4,15 +4,11 @@ import { getNumberParamFromTemplateRow, getStringParamFromTemplateRow } from ".. interface IDisplayGroupParams { /** TEMPLATE PARAMETER: "variant" */ - variant: "box_gray" | "box_primary" | "box_secondary" | "dashed_box" | "speech_bubble"; + variant: "box_gray" | "box_primary" | "box_secondary" | "dashed_box"; /** TEMPLATE PARAMETER: "style". TODO: Various additional legacy styles, review and convert some to variants */ style: "form" | "default" | string | null; /** TEMPLATE PARAMETER: "offset". Add a custom bottom margin */ offset: number; - /** TEMPLATE PARAMETER: "speaker_image_asset". The path to an image to be used as the speaker for the 'speech_bubble' variant */ - speakerImageAsset: string; - /** TEMPLATE PARAMETER: "speaker_position". The position of the speaker image and speech bubble tail for the 'speech_bubble' variant. Default 'left' */ - speakerPosition: "left" | "right"; } @Component({ @@ -40,16 +36,6 @@ export class TmplDisplayGroupComponent extends TemplateBaseComponent implements .split(",") .join(" ") .concat(" " + this.params.style) as IDisplayGroupParams["variant"]; - this.params.speakerImageAsset = getStringParamFromTemplateRow( - this._row, - "speaker_image_asset", - "" - ); - this.params.speakerPosition = getStringParamFromTemplateRow( - this._row, - "speaker_position", - "left" - ) as "left" | "right"; this.type = this.getTypeFromStyles(); } diff --git a/src/app/shared/components/template/components/text_bubble/text-bubble.component.html b/src/app/shared/components/template/components/text_bubble/text-bubble.component.html new file mode 100644 index 000000000..45b0268ec --- /dev/null +++ b/src/app/shared/components/template/components/text_bubble/text-bubble.component.html @@ -0,0 +1,11 @@ + +
+
+ {{ _row.value }} +
+ +
diff --git a/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss b/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss new file mode 100644 index 000000000..b46684e5e --- /dev/null +++ b/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss @@ -0,0 +1,55 @@ +// Adapted from https://www.smashingmagazine.com/2024/03/modern-css-tooltips-speech-bubbles-part1/ +// NOTE - when copying css border-image double-slash // misinterpreted so replace with / 0 / + +$imageSize: 64px; + +.tooltip { + /* triangle dimension */ + --b: 2em; /* base */ + --h: 1em; /* height*/ + + --p: 50%; /* main position (0%:left 100%:right) */ + + min-width: 200px; + max-width: 400px; + text-align: center; + + border-image: fill 0 / 0 / var(--h) conic-gradient(var(--c, #cc333f) 0 0); + color: #fff; + padding: 1em; + + clip-path: polygon( + 0 100%, + 0 0, + 100% 0, + 100% 100%, + calc(var(--p) + var(--b) / 2) 100%, + var(--p) calc(100% + var(--h)), + calc(var(--p) - var(--b) / 2) 100% + ); + + &[data-position="right"] { + --p: calc(100% - 76px - 8px); + } + &[data-position="left"] { + --p: calc(0% + 76px + 8px); + } +} +.container { + position: relative; + width: fit-content; + margin-bottom: calc($imageSize + 8px); +} + +.speaker-image { + position: absolute; + bottom: -$imageSize - 8px; + width: $imageSize; + height: $imageSize; + &[data-position="left"] { + left: 8px; + } + &[data-position="right"] { + right: 8px; + } +} diff --git a/src/app/shared/components/template/components/text_bubble/text-bubble.component.ts b/src/app/shared/components/template/components/text_bubble/text-bubble.component.ts new file mode 100644 index 000000000..94ee6d1bb --- /dev/null +++ b/src/app/shared/components/template/components/text_bubble/text-bubble.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit, ViewEncapsulation } from "@angular/core"; +import { TemplateBaseComponent } from "../base"; +import { getStringParamFromTemplateRow } from "src/app/shared/utils"; + +interface ITextBubbleParams { + /** TEMPLATE PARAMETER: "speaker_image_asset". The path to an image to be used as the speaker for the 'speech_bubble' variant */ + speakerImageAsset: string; + /** TEMPLATE PARAMETER: "speaker_position". The position of the speaker image and speech bubble tail for the 'speech_bubble' variant. Default 'left' */ + speakerPosition: "left" | "right"; +} + +@Component({ + selector: "tmpl-text-bubble", + templateUrl: "text-bubble.component.html", + styleUrl: "text-bubble.component.scss", + encapsulation: ViewEncapsulation.None, +}) +export class TmplTextBubbleComponent extends TemplateBaseComponent implements OnInit { + params: Partial = {}; + ngOnInit() { + this.params.speakerImageAsset = getStringParamFromTemplateRow( + this._row, + "speaker_image_asset", + "" + ); + this.params.speakerPosition = getStringParamFromTemplateRow( + this._row, + "speaker_position", + "left" + ) as "left" | "right"; + } +} From 9e42668525a625ec221b95e742a9c9528f8f5515 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 17 Jul 2024 22:37:55 -0700 Subject: [PATCH 031/204] chore: code tidying --- .../components/text_bubble/text-bubble.component.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss b/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss index b46684e5e..20fa9bb4c 100644 --- a/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss +++ b/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss @@ -7,7 +7,6 @@ $imageSize: 64px; /* triangle dimension */ --b: 2em; /* base */ --h: 1em; /* height*/ - --p: 50%; /* main position (0%:left 100%:right) */ min-width: 200px; @@ -29,10 +28,10 @@ $imageSize: 64px; ); &[data-position="right"] { - --p: calc(100% - 76px - 8px); + --p: calc(100% - #{$imageSize} - 8px); } &[data-position="left"] { - --p: calc(0% + 76px + 8px); + --p: calc(0% + #{$imageSize} + 8px); } } .container { From 6cfcbabe6850cab2233305a86ac9f76752ca67ef Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Thu, 18 Jul 2024 11:18:56 +0100 Subject: [PATCH 032/204] chore: Added a section to the stylesheet to allow for the colour of the check mark to be set (Still in the process of searching for it). --- .../template/components/toggle-bar/toggle-bar.scss | 5 ++++- .../components/template/components/toggle-bar/toggle-bar.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss index c8682e374..6997c7881 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss @@ -14,7 +14,6 @@ ion-toggle { --handle-background: var(--ion-item-background, #fefefe); --handle-background-checked: #fff; --padding-inline: $togglePadding; - --checked-icon-color: #000000; } .container { @@ -69,4 +68,8 @@ ion-toggle { .toggle_wrapper { align-self: baseline; margin-top: 5px; +} + +.icon_inner { + color: var(--track-background-checked); } \ No newline at end of file diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts index 2df7292f4..07ff097a4 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts @@ -69,7 +69,7 @@ export class TmplToggleBarComponent "show_tick_and_cross", true ); - this.params.showTickAndCross = getBooleanParamFromTemplateRow( + this.params.showIcons = getBooleanParamFromTemplateRow( this._row, "show_icons", true @@ -86,6 +86,8 @@ export class TmplToggleBarComponent this.populateVariantMap(); this.params.iconTrue = getStringParamFromTemplateRow(this._row, "icon_true_asset", ""); this.params.iconFalse = getStringParamFromTemplateRow(this._row, "icon_false_asset", ""); + + console.log("params", this.params); } private populateVariantMap() { From afee2412065c6717a9d0e11ed2952276cb7a6069 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Thu, 18 Jul 2024 14:40:04 +0100 Subject: [PATCH 033/204] chore: Making the check mark visible when using OnOffLabels. However, its not working --- android/app/src/main/res/xml/config.xml | 2 -- .../components/template/components/toggle-bar/toggle-bar.scss | 4 ---- 2 files changed, 6 deletions(-) diff --git a/android/app/src/main/res/xml/config.xml b/android/app/src/main/res/xml/config.xml index 1b1b0e0dc..77bc445a5 100644 --- a/android/app/src/main/res/xml/config.xml +++ b/android/app/src/main/res/xml/config.xml @@ -1,6 +1,4 @@ - - \ No newline at end of file diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss index 6997c7881..a41685436 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss @@ -68,8 +68,4 @@ ion-toggle { .toggle_wrapper { align-self: baseline; margin-top: 5px; -} - -.icon_inner { - color: var(--track-background-checked); } \ No newline at end of file From 0c7646a5fe9a9ad41e62567d7cf905174195d9b5 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 18 Jul 2024 16:40:39 +0100 Subject: [PATCH 034/204] feat: text_bubble component --- .../components/template/components/index.ts | 2 +- .../text-bubble/text-bubble.component.html | 20 ++++ .../text-bubble/text-bubble.component.scss | 110 ++++++++++++++++++ .../text-bubble.component.ts | 14 ++- .../text_bubble/text-bubble.component.html | 11 -- .../text_bubble/text-bubble.component.scss | 54 --------- 6 files changed, 142 insertions(+), 69 deletions(-) create mode 100644 src/app/shared/components/template/components/text-bubble/text-bubble.component.html create mode 100644 src/app/shared/components/template/components/text-bubble/text-bubble.component.scss rename src/app/shared/components/template/components/{text_bubble => text-bubble}/text-bubble.component.ts (72%) delete mode 100644 src/app/shared/components/template/components/text_bubble/text-bubble.component.html delete mode 100644 src/app/shared/components/template/components/text_bubble/text-bubble.component.scss diff --git a/src/app/shared/components/template/components/index.ts b/src/app/shared/components/template/components/index.ts index 973da6c8c..0c0c277db 100644 --- a/src/app/shared/components/template/components/index.ts +++ b/src/app/shared/components/template/components/index.ts @@ -62,7 +62,7 @@ import { TmplToggleBarComponent } from "./toggle-bar/toggle-bar"; import { TmplVideoComponent } from "./video"; import { WorkshopsComponent } from "./layout/workshops_accordion"; -import { TmplTextBubbleComponent } from "./text_bubble/text-bubble.component"; +import { TmplTextBubbleComponent } from "./text-bubble/text-bubble.component"; /** All components should be exported as a single array for easy module import */ export const TEMPLATE_COMPONENTS = [ diff --git a/src/app/shared/components/template/components/text-bubble/text-bubble.component.html b/src/app/shared/components/template/components/text-bubble/text-bubble.component.html new file mode 100644 index 000000000..2ba1b26a5 --- /dev/null +++ b/src/app/shared/components/template/components/text-bubble/text-bubble.component.html @@ -0,0 +1,20 @@ +
+
+ + +
+ +
diff --git a/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss b/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss new file mode 100644 index 000000000..5030cf38f --- /dev/null +++ b/src/app/shared/components/template/components/text-bubble/text-bubble.component.scss @@ -0,0 +1,110 @@ +// Adapted from https://www.smashingmagazine.com/2024/03/modern-css-tooltips-speech-bubbles-part1/ +// WIth code available here https://css-generators.com/tooltip-speech-bubble/ +// NOTE - when copying css border-image double-slash // misinterpreted so replace with / 0 / + +$imageSize: 64px; + +.container { + position: relative; + width: fit-content; + margin-bottom: calc($imageSize + 8px); + + &[data-position="right"] { + float: right; + } + + &[data-variant~="gray"] { + --background-color: var(--ion-color-gray-100); + --border-color: var(--ion-color-gray-300); + } + + &[data-variant~="primary"] { + --background-color: var(--ion-color-primary-200); + --border-color: var(--ion-color-primary-500); + } + + &[data-variant~="secondary"] { + --background-color: var(--ion-color-secondary-200); + --border-color: var(--ion-color-secondary-500); + } + + &[data-variant~="no_border"] { + --border-color: transparent; + } +} + +.speaker-image { + position: absolute; + bottom: -$imageSize - 8px; + width: $imageSize; + height: $imageSize; + &[data-position="left"] { + left: 8px; + } + &[data-position="right"] { + right: 8px; + } +} + +.text-bubble { + /* triangle dimension */ + --a: 75deg; /* angle */ + --h: 1em; /* height */ + + --p: 50%; /* triangle position (0%:left 100%:right) */ + &[data-position="left"] { + --p: calc(0% + #{$imageSize} + 12px); + } + &[data-position="right"] { + --p: calc(100% - #{$imageSize} - 12px); + } + --r: var(--ion-border-radius-secondary); /* border radius */ + --b: 2px; /* border width */ + --c1: var(--border-color, var(--ion-color-gray-300)); /* border color */ + --c2: var(--background-color, var(--ion-color-gray-100)); /* background color */ + + padding: 1em; + border-radius: var(--r) var(--r) min(var(--r), 100% - var(--p) - var(--h) * tan(var(--a) / 2)) + min(var(--r), var(--p) - var(--h) * tan(var(--a) / 2)) / var(--r); + clip-path: polygon( + 0 100%, + 0 0, + 100% 0, + 100% 100%, + min(100%, var(--p) + var(--h) * tan(var(--a) / 2)) 100%, + var(--p) calc(100% + var(--h)), + max(0%, var(--p) - var(--h) * tan(var(--a) / 2)) 100% + ); + background: var(--c1); + border-image: conic-gradient(var(--c1) 0 0) fill 0 / var(--r) + max(0%, 100% - var(--p) - var(--h) * tan(var(--a) / 2)) 0 + max(0%, var(--p) - var(--h) * tan(var(--a) / 2)) / 0 0 var(--h) 0; + position: relative; +} +// This is used to create the border +.text-bubble:before { + content: ""; + position: absolute; + z-index: -1; + inset: 0; + padding: var(--b); + border-radius: inherit; + clip-path: polygon( + 0 100%, + 0 0, + 100% 0, + 100% 100%, + min( + 100% - var(--b), + var(--p) + var(--h) * tan(var(--a) / 2) - var(--b) * tan(45deg - var(--a) / 4) + ) + calc(100% - var(--b)), + var(--p) calc(100% + var(--h) - var(--b) / sin(var(--a) / 2)), + max(var(--b), var(--p) - var(--h) * tan(var(--a) / 2) + var(--b) * tan(45deg - var(--a) / 4)) + calc(100% - var(--b)) + ); + background: var(--c2) content-box; + border-image: conic-gradient(var(--c2) 0 0) fill 0 / var(--r) + max(var(--b), 100% - var(--p) - var(--h) * tan(var(--a) / 2)) 0 + max(var(--b), var(--p) - var(--h) * tan(var(--a) / 2)) / 0 0 var(--h) 0; +} diff --git a/src/app/shared/components/template/components/text_bubble/text-bubble.component.ts b/src/app/shared/components/template/components/text-bubble/text-bubble.component.ts similarity index 72% rename from src/app/shared/components/template/components/text_bubble/text-bubble.component.ts rename to src/app/shared/components/template/components/text-bubble/text-bubble.component.ts index 94ee6d1bb..7c05b1a18 100644 --- a/src/app/shared/components/template/components/text_bubble/text-bubble.component.ts +++ b/src/app/shared/components/template/components/text-bubble/text-bubble.component.ts @@ -3,21 +3,26 @@ import { TemplateBaseComponent } from "../base"; import { getStringParamFromTemplateRow } from "src/app/shared/utils"; interface ITextBubbleParams { - /** TEMPLATE PARAMETER: "speaker_image_asset". The path to an image to be used as the speaker for the 'speech_bubble' variant */ + /** TEMPLATE PARAMETER: "speaker_image_asset". The path to an image to be used as the speaker */ speakerImageAsset: string; - /** TEMPLATE PARAMETER: "speaker_position". The position of the speaker image and speech bubble tail for the 'speech_bubble' variant. Default 'left' */ + /** TEMPLATE PARAMETER: "speaker_position". The position of the speaker image and speech bubble tail */ speakerPosition: "left" | "right"; + /** TEMPLATE PARAMETER: "variant" */ + variant: "gray" | "primary" | "secondary" | "no-border"; } @Component({ selector: "tmpl-text-bubble", templateUrl: "text-bubble.component.html", styleUrl: "text-bubble.component.scss", - encapsulation: ViewEncapsulation.None, }) export class TmplTextBubbleComponent extends TemplateBaseComponent implements OnInit { params: Partial = {}; ngOnInit() { + this.getParams(); + } + + getParams() { this.params.speakerImageAsset = getStringParamFromTemplateRow( this._row, "speaker_image_asset", @@ -28,5 +33,8 @@ export class TmplTextBubbleComponent extends TemplateBaseComponent implements On "speaker_position", "left" ) as "left" | "right"; + this.params.variant = getStringParamFromTemplateRow(this._row, "variant", "") + .split(",") + .join(" ") as ITextBubbleParams["variant"]; } } diff --git a/src/app/shared/components/template/components/text_bubble/text-bubble.component.html b/src/app/shared/components/template/components/text_bubble/text-bubble.component.html deleted file mode 100644 index 45b0268ec..000000000 --- a/src/app/shared/components/template/components/text_bubble/text-bubble.component.html +++ /dev/null @@ -1,11 +0,0 @@ - -
-
- {{ _row.value }} -
- -
diff --git a/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss b/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss deleted file mode 100644 index 20fa9bb4c..000000000 --- a/src/app/shared/components/template/components/text_bubble/text-bubble.component.scss +++ /dev/null @@ -1,54 +0,0 @@ -// Adapted from https://www.smashingmagazine.com/2024/03/modern-css-tooltips-speech-bubbles-part1/ -// NOTE - when copying css border-image double-slash // misinterpreted so replace with / 0 / - -$imageSize: 64px; - -.tooltip { - /* triangle dimension */ - --b: 2em; /* base */ - --h: 1em; /* height*/ - --p: 50%; /* main position (0%:left 100%:right) */ - - min-width: 200px; - max-width: 400px; - text-align: center; - - border-image: fill 0 / 0 / var(--h) conic-gradient(var(--c, #cc333f) 0 0); - color: #fff; - padding: 1em; - - clip-path: polygon( - 0 100%, - 0 0, - 100% 0, - 100% 100%, - calc(var(--p) + var(--b) / 2) 100%, - var(--p) calc(100% + var(--h)), - calc(var(--p) - var(--b) / 2) 100% - ); - - &[data-position="right"] { - --p: calc(100% - #{$imageSize} - 8px); - } - &[data-position="left"] { - --p: calc(0% + #{$imageSize} + 8px); - } -} -.container { - position: relative; - width: fit-content; - margin-bottom: calc($imageSize + 8px); -} - -.speaker-image { - position: absolute; - bottom: -$imageSize - 8px; - width: $imageSize; - height: $imageSize; - &[data-position="left"] { - left: 8px; - } - &[data-position="right"] { - right: 8px; - } -} From 1c999541f6aac7371adc3e06bca1382774083f08 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Tue, 23 Jul 2024 10:17:13 +0100 Subject: [PATCH 035/204] chore: made changes to the test script --- .../processors/flowParser/parsers/default.parser.spec.ts | 9 ++++----- .../processors/flowParser/parsers/default.parser.ts | 3 --- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index 5819f01d5..22973832e 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -101,7 +101,7 @@ describe("Default Parser", () => { { type: "text", name: "return_not_empty", - value: "\this is a test string with spaces and tabs\t", + value: "\tThis is a test string with spaces and tabs\t", }, ] }) as FlowTypes.FlowTypeWithData; @@ -114,7 +114,7 @@ describe("Default Parser", () => { { type: "text", name: "tab_and_spaces", - value: "\tthis is a test string with spaces and tabs\t", + value: "this is a test string with spaces and tabs", }, { type: "text", @@ -129,9 +129,8 @@ describe("Default Parser", () => { { type: "text", name: "return_not_empty", - value: "\this is a test string with spaces and tabs\t", + value: "This is a test string with spaces and tabs", }, ]); }); -}); - +}); \ No newline at end of file diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts index 2b407fac8..ef84fad3c 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts @@ -37,7 +37,6 @@ export class DefaultParser< public run(flow: FlowTypes.FlowTypeWithData): FlowTypes.FlowTypeWithData { this.flow = JSON.parse(JSON.stringify(flow)); - console.log("Flow before:", this.flow.rows) this.queue = flow.rows; const processedRows = []; // If first row specifies default values extract them and remove row from queue @@ -72,8 +71,6 @@ export class DefaultParser< this.flow.rows = processedRows; this.flow = this.postProcessFlow(this.flow); - console.log("Flow after", this.flow.rows) - return this.flow; } From 7ddab6af56cc4a3b503cdb31884eda3d673f15f9 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Tue, 23 Jul 2024 11:07:42 +0100 Subject: [PATCH 036/204] chore: Changed the spacing of the close button --- .../template/components/layout/popup/popup.component.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/components/layout/popup/popup.component.scss b/src/app/shared/components/template/components/layout/popup/popup.component.scss index 00b1b45af..92e1bcf26 100644 --- a/src/app/shared/components/template/components/layout/popup/popup.component.scss +++ b/src/app/shared/components/template/components/layout/popup/popup.component.scss @@ -43,8 +43,8 @@ } .close-button { position: absolute; - top: 14px; - right: 14px; + top: 21px; + right: 22px; background: white; width: 40px; height: 40px; From 9ed03214fc988beb7fdf72edffc65190393a0c6c Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Tue, 23 Jul 2024 12:32:24 +0100 Subject: [PATCH 037/204] chore: Changed the dimensions of the close button to still suit previous requirements whilst hopefully not taking up as much space. --- .../template/components/layout/popup/popup.component.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/components/layout/popup/popup.component.scss b/src/app/shared/components/template/components/layout/popup/popup.component.scss index 92e1bcf26..642684884 100644 --- a/src/app/shared/components/template/components/layout/popup/popup.component.scss +++ b/src/app/shared/components/template/components/layout/popup/popup.component.scss @@ -43,8 +43,8 @@ } .close-button { position: absolute; - top: 21px; - right: 22px; + top: 18px; + right: 19px; background: white; width: 40px; height: 40px; From 1be711d05ac9155cf31310554aa92c0e41fb1b23 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 23 Jul 2024 15:12:41 +0100 Subject: [PATCH 038/204] feat: allow text_bubble to take a text value in value column --- .../text-bubble/text-bubble.component.html | 5 +++++ .../text-bubble/text-bubble.component.scss | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/components/text-bubble/text-bubble.component.html b/src/app/shared/components/template/components/text-bubble/text-bubble.component.html index 2ba1b26a5..436c7672d 100644 --- a/src/app/shared/components/template/components/text-bubble/text-bubble.component.html +++ b/src/app/shared/components/template/components/text-bubble/text-bubble.component.html @@ -4,6 +4,11 @@ [attr.data-variant]="params.variant" >
+ @if (_row.value) { +

+ {{ _row.value }} +

+ } Date: Tue, 23 Jul 2024 15:47:58 +0100 Subject: [PATCH 039/204] feat: markdown interprets ASCII punctuation characters into typographic HTML punctuation characters --- package.json | 1 + src/app/shared/utils/utils.ts | 2 ++ yarn.lock | 10 ++++++++++ 3 files changed, 13 insertions(+) diff --git a/package.json b/package.json index 2aa77493f..79b87cf79 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "katex": "^0.16.10", "lottie-web": "^5.12.2", "marked": "^2.1.3", + "marked-smartypants-lite": "^1.0.2", "mergexml": "^1.2.3", "ng2-nouislider": "^2.0.0", "ngx-extended-pdf-viewer": "18.1.9", diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 432a58676..52bb83cce 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -6,6 +6,7 @@ import * as Sentry from "@sentry/angular-ivy"; import { FlowTypes } from "../model"; import { objectToArray } from "../components/template/utils"; import marked from "marked"; +import { markedSmartypantsLite } from "marked-smartypants-lite"; /** * Generate a random string of characters in base-36 (a-z and 0-9 characters) @@ -495,6 +496,7 @@ export function parseMarkdown(src: string, options?: marked.MarkedOptions) { const link = marked.Renderer.prototype.link.apply(this, arguments); return link.replace("=4 <12" + checksum: 0ae237f603e43e628d56c11d4b2910a586757bcf6818ad9503203cc819f99f5ee5deecaeec749533bdf980f3b75441a039d7bbed932b649936c5b07916edf13c + languageName: node + linkType: hard + "marked-terminal@npm:^5.1.1": version: 5.2.0 resolution: "marked-terminal@npm:5.2.0" From 2bc4e9f9b638440f19916583752d6b8203c4f190 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 23 Jul 2024 15:51:52 +0100 Subject: [PATCH 040/204] chore: added explanatory comment --- src/app/shared/utils/utils.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 52bb83cce..301b754d8 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -496,10 +496,24 @@ export function parseMarkdown(src: string, options?: marked.MarkedOptions) { const link = marked.Renderer.prototype.link.apply(this, arguments); return link.replace(" Date: Wed, 24 Jul 2024 11:42:02 +0100 Subject: [PATCH 041/204] style: add custom styling for ios/md versions of toggle bar --- .../components/toggle-bar/toggle-bar.html | 3 ++- .../components/toggle-bar/toggle-bar.scss | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index 229cbc2e6..71658d665 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -11,7 +11,8 @@ class="toggle_wrapper" [class.show-tick-cross]="params.showTickAndCross" > - diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss index a41685436..5f39844ae 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss @@ -9,11 +9,17 @@ ion-toggle { ion-toggle { --track-background: var(--ion-color-gray-200); - --handle-spacing: 1.5px; - --track-background-checked: var(--ion-color-primary, "darkblue"); - --handle-background: var(--ion-item-background, #fefefe); - --handle-background-checked: #fff; + --track-background-checked: var(--ion-color-primary-500); + --handle-background: var(--ion-item-background, white); --padding-inline: $togglePadding; + + &[data-mode~="ios"] { + --handle-background-checked: white; + } + &[data-mode~="md"] { + --handle-background-checked: var(--ion-color-primary); + --padding-inline: $togglePadding; + } } .container { @@ -68,4 +74,4 @@ ion-toggle { .toggle_wrapper { align-self: baseline; margin-top: 5px; -} \ No newline at end of file +} From eaee80817774dfcb9ef3ec8ce73336bf7fd02a49 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 24 Jul 2024 12:00:08 +0100 Subject: [PATCH 042/204] chore: move toggleMode param to variant --- .../components/toggle-bar/toggle-bar.html | 4 ++-- .../components/toggle-bar/toggle-bar.ts | 20 ++++++------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index 71658d665..f475a529c 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -12,8 +12,8 @@ [class.show-tick-cross]="params.showTickAndCross" > diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts index 07ff097a4..6f4a04c42 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts @@ -8,14 +8,13 @@ import { interface IToggleParams { /** TEMPLATE PARAMETER: "variant" */ - variant: "" | "icon" | "in_button"; + variant: "" | "icon" | "in_button" | "ios" | "android"; /** TEMPLATE PARAMETER: "style". Legacy, use "variant" instead. */ style: string; + /** TEMPLATE PARAMETER: "show_icons". Display icons within toggle to represent enabled/disabled state. Default true. */ + showIcons: boolean; /** TEMPLATE PARAMETER: "show_tick_and_cross". Legacy, use "show-icons" instead */ showTickAndCross: boolean; - showIcons: boolean; - /** TEMPLATE PARAMETER: "mode". Enables toggle to have a certain appearance .*/ - toggleMode: "md" | "ios"; /** TEMPLATE PARAMETER: "position". Default "left" */ position: "left" | "center" | "right"; /** TEMPLATE PARAMETER: "false_text". Label text to display when value is false */ @@ -38,6 +37,7 @@ export class TmplToggleBarComponent implements ITemplateRowProps, OnInit { params: Partial = {}; + platformVariant: "ios" | "md" = "ios"; /** @ignore */ variantMap: { icon: boolean }; @@ -69,20 +69,12 @@ export class TmplToggleBarComponent "show_tick_and_cross", true ); - this.params.showIcons = getBooleanParamFromTemplateRow( - this._row, - "show_icons", - true - ); - this.params.toggleMode = getStringParamFromTemplateRow( - this._row, - "mode", - "md" - ) as IToggleParams["toggleMode"]; + this.params.showIcons = getBooleanParamFromTemplateRow(this._row, "show_icons", true); this.params.style = getStringParamFromTemplateRow(this._row, "style", ""); this.params.variant = getStringParamFromTemplateRow(this._row, "variant", "") .split(",") .join(" ") as IToggleParams["variant"]; + this.platformVariant = this.params.variant.split(" ").includes("android") ? "md" : "ios"; this.populateVariantMap(); this.params.iconTrue = getStringParamFromTemplateRow(this._row, "icon_true_asset", ""); this.params.iconFalse = getStringParamFromTemplateRow(this._row, "icon_false_asset", ""); From fe87910fe0d221946db0a73238c345230104d326 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 26 Jul 2024 11:07:18 +0100 Subject: [PATCH 043/204] refactor: use Angular control-flow syntax --- .../text-bubble/text-bubble.component.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/shared/components/template/components/text-bubble/text-bubble.component.html b/src/app/shared/components/template/components/text-bubble/text-bubble.component.html index 436c7672d..41b01cb9c 100644 --- a/src/app/shared/components/template/components/text-bubble/text-bubble.component.html +++ b/src/app/shared/components/template/components/text-bubble/text-bubble.component.html @@ -9,13 +9,13 @@ {{ _row.value }}

} - - + @for (childRow of _row.rows | filterDisplayComponent; track trackByRow($index, childRow)) { + + }
Date: Fri, 26 Jul 2024 14:55:38 +0100 Subject: [PATCH 044/204] fix: Toggles were missing; missing code was added to allow them to return. --- .../components/template/components/toggle-bar/toggle-bar.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index f475a529c..2b888bf75 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -13,8 +13,10 @@ > From 0dd1529c090b8d9aee0acdd53901847dee197733 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Wed, 31 Jul 2024 16:55:59 +0100 Subject: [PATCH 045/204] chore: Completed test script for default parser. --- .../flowParser/parsers/default.parser.spec.ts | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index 22973832e..e2c831bb1 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -5,9 +5,8 @@ const testInputSources = { data_list: { test_data_list: [{ id: 1 }, { id: 2 }, { id: 3 }] }, }; - describe("Default Parser", () => { - it('Cleans field values - removes whitespace within empty strings', () => { + it("Cleans field values - removes whitespace within empty strings", () => { const parser = new DefaultParser({ processedFlowHashmap: testInputSources } as any); const output = parser.run({ flow_name: "test_flow_input_empty", @@ -40,38 +39,38 @@ describe("Default Parser", () => { value: "\r\n\n\r", }, ], - }) as FlowTypes.FlowTypeWithData; + }) as FlowTypes.FlowTypeWithData; // not possible to use the actual objects as the function used is not exported expect(output.rows).toEqual([ - { - type: "text", - name: "empty", - value: "", - }, - { - type: "text", - name: "tab_empty", - value: "", - }, - { - type: "text", - name: "newline_empty", - value: "", - }, - { - type: "text", - name: "return_empty", - value: "", - }, - { - type: "text", - name: "newline_return_empty", - value: "", - }, - ]); + { + type: "text", + name: "empty", + value: "", + }, + { + type: "text", + name: "tab_empty", + value: "", + }, + { + type: "text", + name: "newline_empty", + value: "", + }, + { + type: "text", + name: "return_empty", + value: "", + }, + { + type: "text", + name: "newline_return_empty", + value: "", + }, + ]); }); - it('Cleans field values - Replaces string consisting of only line breaks and new lines', () => { + it("Cleans field values - Replaces string consisting of only line breaks and new lines", () => { const parser = new DefaultParser({ processedFlowHashmap: testInputSources } as any); const output = parser.run({ flow_name: "test_flow_input_nonempty", @@ -96,14 +95,14 @@ describe("Default Parser", () => { { type: "text", name: "newline_return_not_empty", - value: "Cats are cool\rDogs are great\nPersonally...\nI prefer hamsters\r", + value: "Hello,\rI like dogs...\but not cats at all!\r", }, { type: "text", name: "return_not_empty", value: "\tThis is a test string with spaces and tabs\t", }, - ] + ], }) as FlowTypes.FlowTypeWithData; expect(output.rows).toEqual([ { @@ -124,7 +123,7 @@ describe("Default Parser", () => { { type: "text", name: "newline_return_not_empty", - value: "Cats are cool\rDogs are great\nPersonally...\nI prefer hamsters\r", + value: "Hello,\rI like dogs...\but not cats at all!", }, { type: "text", @@ -133,4 +132,4 @@ describe("Default Parser", () => { }, ]); }); -}); \ No newline at end of file +}); From 6cb040adabde7ec8a0f9d7984e4833b8b476d977 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Fri, 2 Aug 2024 15:48:27 +0100 Subject: [PATCH 046/204] fix: Incorporating feedback into code. Such as removing 'flow_subtype' and set flow_type to 'data_list'. I also initialised the parser to be used for all tests. --- .../flowParser/parsers/default.parser.spec.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index e2c831bb1..a15ec3b51 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -1,17 +1,13 @@ -import { FlowTypes } from "data-models"; import { DefaultParser } from "./default.parser"; - -const testInputSources = { - data_list: { test_data_list: [{ id: 1 }, { id: 2 }, { id: 3 }] }, -}; +import { FlowTypes } from "data-models"; +import { getTestFlowParserProcessor } from "../flowParser.spec"; describe("Default Parser", () => { - it("Cleans field values - removes whitespace within empty strings", () => { - const parser = new DefaultParser({ processedFlowHashmap: testInputSources } as any); + const parser = new DefaultParser({ processedFlowHashmap: {} } as any); + it("Cleans field values - handles strings consisting only of whitespace", () => { const output = parser.run({ flow_name: "test_flow_input_empty", - flow_subtype: "generator", - flow_type: "generated", + flow_type: "data_list", rows: [ { type: "text", @@ -70,12 +66,10 @@ describe("Default Parser", () => { ]); }); - it("Cleans field values - Replaces string consisting of only line breaks and new lines", () => { - const parser = new DefaultParser({ processedFlowHashmap: testInputSources } as any); + it("Cleans field values - trims whitespace from beginning and end of strings", () => { const output = parser.run({ flow_name: "test_flow_input_nonempty", - flow_subtype: "generator", - flow_type: "generated", + flow_type: "data_list", rows: [ { type: "text", From be6d539db209923df4429b4e9c314110a2b89f15 Mon Sep 17 00:00:00 2001 From: Jody Gyekye Date: Fri, 2 Aug 2024 16:33:56 +0100 Subject: [PATCH 047/204] chore: Added feedback suggested by comments for default.parser.ts --- .../convert/processors/flowParser/parsers/default.parser.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts index ef84fad3c..4394097e6 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.ts @@ -70,7 +70,6 @@ export class DefaultParser< } this.flow.rows = processedRows; this.flow = this.postProcessFlow(this.flow); - return this.flow; } @@ -288,9 +287,9 @@ class RowProcessor { // remove whitespace this.row[field] = this.row[field].trim(); - // remove all line breaks in the field using replaceAll() and regex + // replace any strings that consist only of whitespace with the empty string if (!this.row[field].match(/\S/)) { - this.row[field] = ""; + this.row[field] = ""; } } }); From fa9788602f91185b0e2659a0884acd5867d3be20 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 6 Aug 2024 14:39:44 +0100 Subject: [PATCH 048/204] chore: toggle styling --- .../components/template/components/toggle-bar/toggle-bar.html | 2 +- .../components/template/components/toggle-bar/toggle-bar.scss | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html index 2b888bf75..825e3dd57 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.html +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.html @@ -13,10 +13,10 @@ > diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss index 5f39844ae..e43bf6126 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss @@ -30,9 +30,8 @@ ion-toggle { &[data-param-style~="in_button"], &[data-variant~="in_button"] { - flex-direction: row-reverse; ion-toggle { - transform: translate($togglePadding, $togglePadding); + transform: translate(4px, 4px); } } From 37759da2a5f7d90284d4ea102e8cd6a246d4e58a Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 6 Aug 2024 14:45:43 +0100 Subject: [PATCH 049/204] chore: code tidy --- .../components/template/components/toggle-bar/toggle-bar.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts index 6f4a04c42..381a17262 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts @@ -78,8 +78,6 @@ export class TmplToggleBarComponent this.populateVariantMap(); this.params.iconTrue = getStringParamFromTemplateRow(this._row, "icon_true_asset", ""); this.params.iconFalse = getStringParamFromTemplateRow(this._row, "icon_false_asset", ""); - - console.log("params", this.params); } private populateVariantMap() { From b88d400b5e8438b47dbb2a59ff0cba6930b2a9c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 02:38:33 +0000 Subject: [PATCH 050/204] chore(deps): bump axios from 0.25.0 to 1.7.3 Bumps [axios](https://github.com/axios/axios) from 0.25.0 to 1.7.3. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.25.0...v1.7.3) --- updated-dependencies: - dependency-name: axios dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7eea1f176..25173846b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9475,13 +9475,13 @@ __metadata: linkType: hard "axios@npm:^1.5.1, axios@npm:^1.6.0": - version: 1.6.7 - resolution: "axios@npm:1.6.7" + version: 1.7.3 + resolution: "axios@npm:1.7.3" dependencies: - follow-redirects: ^1.15.4 + follow-redirects: ^1.15.6 form-data: ^4.0.0 proxy-from-env: ^1.1.0 - checksum: 87d4d429927d09942771f3b3a6c13580c183e31d7be0ee12f09be6d5655304996bb033d85e54be81606f4e89684df43be7bf52d14becb73a12727bf33298a082 + checksum: bc304d6da974922342aed7c33155934354429cdc7e1ba9d399ab9ff3ac76103f3697eeedf042a634d43cdae682182bcffd942291db42d2be45b750597cdd5eef languageName: node linkType: hard @@ -14597,7 +14597,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.7, follow-redirects@npm:^1.15.4": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.14.7, follow-redirects@npm:^1.15.6": version: 1.15.6 resolution: "follow-redirects@npm:1.15.6" peerDependenciesMeta: From da23cfe3a052682fca855795b00bb6af0348dda0 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 15 Aug 2024 17:20:52 +0100 Subject: [PATCH 051/204] empty commit From c09c3a271961945bb651a1f56992504a0c3d8dd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:16:11 +0000 Subject: [PATCH 052/204] chore(deps): bump axios from 1.7.3 to 1.7.4 Bumps [axios](https://github.com/axios/axios) from 1.7.3 to 1.7.4. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.7.3...v1.7.4) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- packages/test-visual/package.json | 84 +++++++++++++++---------------- yarn.lock | 15 +++++- 2 files changed, 55 insertions(+), 44 deletions(-) diff --git a/packages/test-visual/package.json b/packages/test-visual/package.json index 07ac83080..44d8518d8 100644 --- a/packages/test-visual/package.json +++ b/packages/test-visual/package.json @@ -1,42 +1,42 @@ -{ - "name": "test-visual", - "version": "1.0.0", - "description": "", - "main": "lib/index.js", - "bin": { - "idems-test-visual": "./lib/index.js" - }, - "scripts": { - "build": "tsc ", - "dev": "ts-node-dev --respawn --watch 'src/**/*.ts' src/index.ts", - "start": "ts-node src/index.ts" - }, - "dependencies": { - "archiver": "^5.3.0", - "axios": "^1.6.0", - "boxen": "^5.1.2", - "chalk": "^4.1.2", - "commander": "^8.2.0", - "data-models": "1.0.0", - "dotenv": "^10.0.0", - "extract-zip": "^2.0.1", - "fs-extra": "^10.0.0", - "jpeg-js": "^0.4.4", - "log-update": "^4.0.0", - "octokit": "^3.1.2", - "p-queue": "^6.6.2", - "pixelmatch": "^5.2.1", - "pngjs": "^6.0.0", - "puppeteer": "^10.2.0", - "serve": "^13.0.2", - "ts-node": "^10.8.0", - "typescript": "~4.2.4" - }, - "devDependencies": { - "@types/fs-extra": "^9.0.12", - "@types/node": "^15.12.4", - "@types/pixelmatch": "^5.2.4", - "@types/pngjs": "^6.0.1", - "ts-node-dev": "^1.1.8" - } -} +{ + "name": "test-visual", + "version": "1.0.0", + "description": "", + "main": "lib/index.js", + "bin": { + "idems-test-visual": "./lib/index.js" + }, + "scripts": { + "build": "tsc ", + "dev": "ts-node-dev --respawn --watch 'src/**/*.ts' src/index.ts", + "start": "ts-node src/index.ts" + }, + "dependencies": { + "archiver": "^5.3.0", + "axios": "^1.7.4", + "boxen": "^5.1.2", + "chalk": "^4.1.2", + "commander": "^8.2.0", + "data-models": "1.0.0", + "dotenv": "^10.0.0", + "extract-zip": "^2.0.1", + "fs-extra": "^10.0.0", + "jpeg-js": "^0.4.4", + "log-update": "^4.0.0", + "octokit": "^3.1.2", + "p-queue": "^6.6.2", + "pixelmatch": "^5.2.1", + "pngjs": "^6.0.0", + "puppeteer": "^10.2.0", + "serve": "^13.0.2", + "ts-node": "^10.8.0", + "typescript": "~4.2.4" + }, + "devDependencies": { + "@types/fs-extra": "^9.0.12", + "@types/node": "^15.12.4", + "@types/pixelmatch": "^5.2.4", + "@types/pngjs": "^6.0.1", + "ts-node-dev": "^1.1.8" + } +} diff --git a/yarn.lock b/yarn.lock index 25173846b..eb2962f24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9474,7 +9474,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.5.1, axios@npm:^1.6.0": +"axios@npm:^1.5.1": version: 1.7.3 resolution: "axios@npm:1.7.3" dependencies: @@ -9485,6 +9485,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.7.4": + version: 1.7.4 + resolution: "axios@npm:1.7.4" + dependencies: + follow-redirects: ^1.15.6 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: 0c17039a9acfe6a566fca8431ba5c1b455c83d30ea6157fec68a6722878fcd30f3bd32d172f6bee0c51fe75ca98e6414ddcd968a87b5606b573731629440bfaf + languageName: node + linkType: hard + "axobject-query@npm:2.0.2": version: 2.0.2 resolution: "axobject-query@npm:2.0.2" @@ -25208,7 +25219,7 @@ __metadata: "@types/pixelmatch": ^5.2.4 "@types/pngjs": ^6.0.1 archiver: ^5.3.0 - axios: ^1.6.0 + axios: ^1.7.4 boxen: ^5.1.2 chalk: ^4.1.2 commander: ^8.2.0 From af787b3e451715c49930974ee2dd6850e0c80228 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 16 Aug 2024 10:56:33 +0100 Subject: [PATCH 053/204] chore: package.json whitespace --- packages/test-visual/package.json | 84 +++++++++++++++---------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/packages/test-visual/package.json b/packages/test-visual/package.json index 44d8518d8..f19807ff7 100644 --- a/packages/test-visual/package.json +++ b/packages/test-visual/package.json @@ -1,42 +1,42 @@ -{ - "name": "test-visual", - "version": "1.0.0", - "description": "", - "main": "lib/index.js", - "bin": { - "idems-test-visual": "./lib/index.js" - }, - "scripts": { - "build": "tsc ", - "dev": "ts-node-dev --respawn --watch 'src/**/*.ts' src/index.ts", - "start": "ts-node src/index.ts" - }, - "dependencies": { - "archiver": "^5.3.0", - "axios": "^1.7.4", - "boxen": "^5.1.2", - "chalk": "^4.1.2", - "commander": "^8.2.0", - "data-models": "1.0.0", - "dotenv": "^10.0.0", - "extract-zip": "^2.0.1", - "fs-extra": "^10.0.0", - "jpeg-js": "^0.4.4", - "log-update": "^4.0.0", - "octokit": "^3.1.2", - "p-queue": "^6.6.2", - "pixelmatch": "^5.2.1", - "pngjs": "^6.0.0", - "puppeteer": "^10.2.0", - "serve": "^13.0.2", - "ts-node": "^10.8.0", - "typescript": "~4.2.4" - }, - "devDependencies": { - "@types/fs-extra": "^9.0.12", - "@types/node": "^15.12.4", - "@types/pixelmatch": "^5.2.4", - "@types/pngjs": "^6.0.1", - "ts-node-dev": "^1.1.8" - } -} +{ + "name": "test-visual", + "version": "1.0.0", + "description": "", + "main": "lib/index.js", + "bin": { + "idems-test-visual": "./lib/index.js" + }, + "scripts": { + "build": "tsc ", + "dev": "ts-node-dev --respawn --watch 'src/**/*.ts' src/index.ts", + "start": "ts-node src/index.ts" + }, + "dependencies": { + "archiver": "^5.3.0", + "axios": "^1.7.4", + "boxen": "^5.1.2", + "chalk": "^4.1.2", + "commander": "^8.2.0", + "data-models": "1.0.0", + "dotenv": "^10.0.0", + "extract-zip": "^2.0.1", + "fs-extra": "^10.0.0", + "jpeg-js": "^0.4.4", + "log-update": "^4.0.0", + "octokit": "^3.1.2", + "p-queue": "^6.6.2", + "pixelmatch": "^5.2.1", + "pngjs": "^6.0.0", + "puppeteer": "^10.2.0", + "serve": "^13.0.2", + "ts-node": "^10.8.0", + "typescript": "~4.2.4" + }, + "devDependencies": { + "@types/fs-extra": "^9.0.12", + "@types/node": "^15.12.4", + "@types/pixelmatch": "^5.2.4", + "@types/pngjs": "^6.0.1", + "ts-node-dev": "^1.1.8" + } +} From 3101f1bef928ceba42c0d80a02c0ce32fad3d192 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 19 Aug 2024 17:58:55 +0100 Subject: [PATCH 054/204] wip: journey component and circle task-card variant --- packages/data-models/flowTypes.ts | 1 + .../components/template/components/index.ts | 3 + .../components/journey/journey.component.html | 11 ++ .../components/journey/journey.component.scss | 47 ++++++ .../journey/journey.component.spec.ts | 24 +++ .../components/journey/journey.component.ts | 28 ++++ .../task-card/task-card.component.html | 152 +++++++++++------- .../task-card/task-card.component.scss | 24 +++ 8 files changed, 228 insertions(+), 62 deletions(-) create mode 100644 src/app/shared/components/template/components/journey/journey.component.html create mode 100644 src/app/shared/components/template/components/journey/journey.component.scss create mode 100644 src/app/shared/components/template/components/journey/journey.component.spec.ts create mode 100644 src/app/shared/components/template/components/journey/journey.component.ts diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 9f4aaa7c7..c466ffc4d 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -276,6 +276,7 @@ export namespace FlowTypes { | "icon_banner" | "image" | "items" + | "journey" | "latex" | "lottie_animation" | "nav_group" diff --git a/src/app/shared/components/template/components/index.ts b/src/app/shared/components/template/components/index.ts index 0c0c277db..b54cd1dd1 100644 --- a/src/app/shared/components/template/components/index.ts +++ b/src/app/shared/components/template/components/index.ts @@ -36,6 +36,7 @@ import { TmplDrawerComponent } from "./drawer/drawer.component"; import { TmplHelpIconComponent } from "./help-icon"; import { TmplIconBannerComponent } from "./icon-banner/icon-banner.component"; import { TmplImageComponent } from "./image"; +import { TmplJourneyComponent } from "./journey/journey.component"; import { TmplLatexComponent } from "./latex/latex.component"; import { TmplLottieAnimation } from "./lottie-animation"; import { TmplNavigationBarComponent } from "./navigation-bar/navigation-bar.component"; @@ -96,6 +97,7 @@ export const TEMPLATE_COMPONENTS = [ TmplHelpIconComponent, TmplIconBannerComponent, TmplImageComponent, + TmplJourneyComponent, TmplLatexComponent, TmplLottieAnimation, TmplNavigationBarComponent, @@ -155,6 +157,7 @@ export const TEMPLATE_COMPONENT_MAPPING: Record< icon_banner: TmplIconBannerComponent, image: TmplImageComponent, items: null, + journey: TmplJourneyComponent, latex: TmplLatexComponent, lottie_animation: TmplLottieAnimation, nav_group: NavGroupComponent, diff --git a/src/app/shared/components/template/components/journey/journey.component.html b/src/app/shared/components/template/components/journey/journey.component.html new file mode 100644 index 000000000..8c42b0b10 --- /dev/null +++ b/src/app/shared/components/template/components/journey/journey.component.html @@ -0,0 +1,11 @@ +
+ @for (childRow of _row.rows | filterDisplayComponent; track trackByRow($index, childRow)) { +
+
+ + +
+ +
+ } +
diff --git a/src/app/shared/components/template/components/journey/journey.component.scss b/src/app/shared/components/template/components/journey/journey.component.scss new file mode 100644 index 000000000..b3f899e6d --- /dev/null +++ b/src/app/shared/components/template/components/journey/journey.component.scss @@ -0,0 +1,47 @@ +.journey-group-wrapper { + position: relative; + display: flex; + flex-direction: column; +} + +.journey-child-wrapper { + position: relative; + margin-bottom: -34px; + display: flex; + flex-direction: column; + align-items: center; + + .path-segment { + position: absolute; + top: 25%; + z-index: -1; + width: 90%; + height: 90%; + } + + .journey-child-content { + width: 150px; + text-align: center; + } + + &.odd { + .journey-child-content { + align-self: flex-start; + } + } + + &.even { + .journey-child-content { + align-self: flex-end; + } + .path-segment { + transform: scaleX(-1); + } + } + + &:last-child { + .path-segment { + display: none; + } + } +} diff --git a/src/app/shared/components/template/components/journey/journey.component.spec.ts b/src/app/shared/components/template/components/journey/journey.component.spec.ts new file mode 100644 index 000000000..98e1a615e --- /dev/null +++ b/src/app/shared/components/template/components/journey/journey.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { IonicModule } from "@ionic/angular"; + +import { TmplJourneyComponent } from "./journey.component"; + +describe("JourneyComponent", () => { + let component: TmplJourneyComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [TmplJourneyComponent], + imports: [IonicModule.forRoot()], + }).compileComponents(); + + fixture = TestBed.createComponent(TmplJourneyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/template/components/journey/journey.component.ts b/src/app/shared/components/template/components/journey/journey.component.ts new file mode 100644 index 000000000..a04147d64 --- /dev/null +++ b/src/app/shared/components/template/components/journey/journey.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from "@angular/core"; +import { TemplateBaseComponent } from "../base"; +import { getStringParamFromTemplateRow } from "src/app/shared/utils"; + +interface IJourneyParams { + /** TEMPLATE PARAMETER: path_segment_asset */ + pathSegmentAsset: string; +} +@Component({ + selector: "plh-journey", + templateUrl: "./journey.component.html", + styleUrls: ["./journey.component.scss"], +}) +export class TmplJourneyComponent extends TemplateBaseComponent implements OnInit { + params: Partial = {}; + + ngOnInit() { + this.getParams(); + } + + getParams() { + this.params.pathSegmentAsset = getStringParamFromTemplateRow( + this._row, + "path_segment_asset", + "" + ); + } +} diff --git a/src/app/shared/components/template/components/task-card/task-card.component.html b/src/app/shared/components/template/components/task-card/task-card.component.html index 1baa40374..62e66acd4 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.html +++ b/src/app/shared/components/template/components/task-card/task-card.component.html @@ -1,14 +1,83 @@ -
- - - {{ highlightedText }} - - - +@if (!variant.includes("circle")) { +
+ + + {{ highlightedText }} + + + + + + + + + +
+ + + + + + +
+ + +
+
+
+
+

+ {{ title }} +

+
+
+

+ {{ subtitle }} +

+
+ +
+ +
+
+
+ +
+
+
+
+} @else { +
- -
- - - - - - -
- - -
-
-
-
-

- {{ title }} -

-
-
-

- {{ subtitle }} -

-
- -
- -
-
-
- +
+ +
+ @if (title) { +
+

+ {{ title }} +

- + }
-
+} diff --git a/src/app/shared/components/template/components/task-card/task-card.component.scss b/src/app/shared/components/template/components/task-card/task-card.component.scss index f7ad8e718..3e55ec097 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.scss +++ b/src/app/shared/components/template/components/task-card/task-card.component.scss @@ -145,6 +145,30 @@ } } +.circle-card-wrapper { + width: 100%; + // height: 150px; + border-radius: 50%; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + .circle-wrapper { + width: 100px; + height: 100px; + border-radius: 50%; + overflow: hidden; + + .img { + width: 100px; + } + } + + .title-wrapper { + width: 150px; + } +} + // TODO: These could be moved to the global scope, and possibly reworked to be "styles" rather than "variants" // as part of https://github.com/IDEMSInternational/parenting-app-ui/issues/1971 *[data-variant~="background-secondary"] { From 1212e4ab9fc7fef5c5d5eadc98b5edc8419a1ea1 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 20 Aug 2024 14:26:13 +0100 Subject: [PATCH 055/204] feat: journey component; task-card circle variant --- .../components/journey/journey.component.html | 4 +- .../components/journey/journey.component.scss | 13 ++- .../task-card/task-card.component.html | 15 ++- .../task-card/task-card.component.scss | 106 ++++++++++++------ .../task-progress-bar.component.ts | 4 +- 5 files changed, 101 insertions(+), 41 deletions(-) diff --git a/src/app/shared/components/template/components/journey/journey.component.html b/src/app/shared/components/template/components/journey/journey.component.html index 8c42b0b10..7ec8403d0 100644 --- a/src/app/shared/components/template/components/journey/journey.component.html +++ b/src/app/shared/components/template/components/journey/journey.component.html @@ -5,7 +5,9 @@
- +
+ +
} diff --git a/src/app/shared/components/template/components/journey/journey.component.scss b/src/app/shared/components/template/components/journey/journey.component.scss index b3f899e6d..3c7e7777d 100644 --- a/src/app/shared/components/template/components/journey/journey.component.scss +++ b/src/app/shared/components/template/components/journey/journey.component.scss @@ -1,7 +1,12 @@ +$path-segment-width: 264px; + .journey-group-wrapper { position: relative; display: flex; flex-direction: column; + min-width: $path-segment-width; + max-width: 380px; + margin: auto; } .journey-child-wrapper { @@ -15,8 +20,7 @@ position: absolute; top: 25%; z-index: -1; - width: 90%; - height: 90%; + width: $path-segment-width; } .journey-child-content { @@ -28,6 +32,9 @@ .journey-child-content { align-self: flex-start; } + .path-segment { + margin-left: 32px; + } } &.even { @@ -35,7 +42,9 @@ align-self: flex-end; } .path-segment { + // Flip horizontally for alternate instances transform: scaleX(-1); + margin-right: 32px; } } diff --git a/src/app/shared/components/template/components/task-card/task-card.component.html b/src/app/shared/components/template/components/task-card/task-card.component.html index 62e66acd4..2baeba45a 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.html +++ b/src/app/shared/components/template/components/task-card/task-card.component.html @@ -78,6 +78,17 @@

} @else {
+
+ +
@if (title) {
-

+

{{ title }} -

+

} diff --git a/src/app/shared/components/template/components/task-card/task-card.component.scss b/src/app/shared/components/template/components/task-card/task-card.component.scss index 3e55ec097..146eea3a3 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.scss +++ b/src/app/shared/components/template/components/task-card/task-card.component.scss @@ -103,11 +103,62 @@ padding-left: var(--small-padding); } } +} + +.badge { + position: absolute; + top: -10px; + filter: drop-shadow(var(--ion-default-box-shadow)); + &.highlighted-badge { + right: -10px; + padding: 5px 10px; + border-radius: var(--ion-border-radius-small); + background: var(--ion-color-secondary); + color: white; + font-weight: var(--font-weight-bold); + } + &.progress-badge { + right: -12px; + width: 36px; + } + .circle { + height: 36px; + width: 36px; + border-radius: 50%; + position: absolute; + z-index: 1; + &.completed { + background-color: var(--ion-color-green); + } + &.inProgress { + background-color: var(--ion-color-gray-light); + } + } + .icon { + position: absolute; + z-index: 2; + width: 100%; + padding: 4px; + &.completed { + top: 2px; + padding: 8px; + } + } +} + +.circle-card-wrapper { + position: relative; + width: 100%; + height: 160px; + border-radius: 50%; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; .badge { position: absolute; - top: -10px; - filter: drop-shadow(var(--ion-default-box-shadow)); + top: -6px; &.highlighted-badge { right: -10px; padding: 5px 10px; @@ -117,47 +168,19 @@ font-weight: var(--font-weight-bold); } &.progress-badge { - right: -12px; - width: 36px; - } - .circle { - height: 36px; - width: 36px; - border-radius: 50%; - position: absolute; - z-index: 1; - &.completed { - background-color: var(--ion-color-green); - } - &.inProgress { - background-color: var(--ion-color-gray-light); - } - } - .icon { - position: absolute; z-index: 2; - padding: 4px; - &.completed { - top: 2px; - padding: 8px; - } + right: 32px; } } -} -.circle-card-wrapper { - width: 100%; - // height: 150px; - border-radius: 50%; - padding: 0; - display: flex; - flex-direction: column; - align-items: center; .circle-wrapper { width: 100px; height: 100px; border-radius: 50%; overflow: hidden; + background-color: white; + border: 1px solid rgba(black, 0.07); + filter: drop-shadow(var(--ion-default-box-shadow)); .img { width: 100px; @@ -165,7 +188,20 @@ } .title-wrapper { - width: 150px; + position: absolute; + top: 100px; + width: 140px; + + p { + line-height: inherit; + font-size: var(--font-size-text-large); + color: var(--ion-color-primary); + font-weight: var(--font-weight-bold); + } + } + + plh-task-progress-bar { + position: absolute; } } diff --git a/src/app/shared/components/template/components/task-progress-bar/task-progress-bar.component.ts b/src/app/shared/components/template/components/task-progress-bar/task-progress-bar.component.ts index ff5ae3380..b054cd79e 100644 --- a/src/app/shared/components/template/components/task-progress-bar/task-progress-bar.component.ts +++ b/src/app/shared/components/template/components/task-progress-bar/task-progress-bar.component.ts @@ -138,7 +138,7 @@ export class TmplTaskProgressBarComponent this.params.completedField = this.completedField; this.params.progressUnitsName = this.progressUnitsName; this.params.showText = this.showText; - this.params.completedColumnName = null; + this.params.completedColumnName = "completed"; this.params.completedFieldColumnName = "completed_field"; } } @@ -153,11 +153,13 @@ export class TmplTaskProgressBarComponent } private checkAndSetUseDynamicData() { + console.log("this.completedColumnName", this.params.completedColumnName); this.useDynamicData = this.dataRows?.[0]?.hasOwnProperty(this.params.completedColumnName); } private async evaluateTaskGroupData() { const previousProgressStatus = this.progressStatus; + console.log("this.dataRows", this.dataRows); const { subtasksTotal, subtasksCompleted, progressStatus, newlyCompleted } = await this.taskService.evaluateTaskGroupData(this.dataRows, { completedColumnName: this.params.completedColumnName, From bf23b8abacc2f81696536075219df229b02ccc2f Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 20 Aug 2024 14:26:54 +0100 Subject: [PATCH 056/204] chore: code tidy --- .../components/task-progress-bar/task-progress-bar.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/shared/components/template/components/task-progress-bar/task-progress-bar.component.ts b/src/app/shared/components/template/components/task-progress-bar/task-progress-bar.component.ts index b054cd79e..9b6162eeb 100644 --- a/src/app/shared/components/template/components/task-progress-bar/task-progress-bar.component.ts +++ b/src/app/shared/components/template/components/task-progress-bar/task-progress-bar.component.ts @@ -153,13 +153,11 @@ export class TmplTaskProgressBarComponent } private checkAndSetUseDynamicData() { - console.log("this.completedColumnName", this.params.completedColumnName); this.useDynamicData = this.dataRows?.[0]?.hasOwnProperty(this.params.completedColumnName); } private async evaluateTaskGroupData() { const previousProgressStatus = this.progressStatus; - console.log("this.dataRows", this.dataRows); const { subtasksTotal, subtasksCompleted, progressStatus, newlyCompleted } = await this.taskService.evaluateTaskGroupData(this.dataRows, { completedColumnName: this.params.completedColumnName, From 15cd97574832193a355eabf050cec823f47f7aa1 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 21 Aug 2024 11:56:21 +0100 Subject: [PATCH 057/204] chore: rename component 'journey' -> 'progress-path' --- packages/data-models/flowTypes.ts | 2 +- .../shared/components/template/components/index.ts | 6 +++--- .../progress-path.component.html} | 9 ++++++--- .../progress-path.component.scss} | 10 +++++----- .../progress-path.component.spec.ts} | 12 ++++++------ .../progress-path.component.ts} | 12 ++++++------ 6 files changed, 27 insertions(+), 24 deletions(-) rename src/app/shared/components/template/components/{journey/journey.component.html => progress-path/progress-path.component.html} (64%) rename src/app/shared/components/template/components/{journey/journey.component.scss => progress-path/progress-path.component.scss} (83%) rename src/app/shared/components/template/components/{journey/journey.component.spec.ts => progress-path/progress-path.component.spec.ts} (55%) rename src/app/shared/components/template/components/{journey/journey.component.ts => progress-path/progress-path.component.ts} (60%) diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index c466ffc4d..b6751a86f 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -276,7 +276,6 @@ export namespace FlowTypes { | "icon_banner" | "image" | "items" - | "journey" | "latex" | "lottie_animation" | "nav_group" @@ -288,6 +287,7 @@ export namespace FlowTypes { | "parent_point_box" | "parent_point_counter" | "pdf" + | "progress_path" | "qr_code" | "radio_button_grid" | "radio_group" diff --git a/src/app/shared/components/template/components/index.ts b/src/app/shared/components/template/components/index.ts index b54cd1dd1..bc90d760d 100644 --- a/src/app/shared/components/template/components/index.ts +++ b/src/app/shared/components/template/components/index.ts @@ -36,7 +36,6 @@ import { TmplDrawerComponent } from "./drawer/drawer.component"; import { TmplHelpIconComponent } from "./help-icon"; import { TmplIconBannerComponent } from "./icon-banner/icon-banner.component"; import { TmplImageComponent } from "./image"; -import { TmplJourneyComponent } from "./journey/journey.component"; import { TmplLatexComponent } from "./latex/latex.component"; import { TmplLottieAnimation } from "./lottie-animation"; import { TmplNavigationBarComponent } from "./navigation-bar/navigation-bar.component"; @@ -45,6 +44,7 @@ import { TmplOdkFormComponent } from "./odk-form/odk-form.component"; import { TmplParentPointBoxComponent } from "./points-item/points-item.component"; import { TmplParentPointCounterComponent } from "./parent-point-counter/parent-point-counter.component"; import { TmplPdfComponent } from "./pdf/pdf.component"; +import { TmplProgressPathComponent } from "./progress-path/progress-path.component"; import { TmplQRCodeComponent } from "./qr-code/qr-code.component"; import { TmplRadioButtonGridComponent } from "./radio-button-grid/radio-button-grid.component"; import { TmplRadioGroupComponent } from "./radio-group/radio-group.component"; @@ -97,7 +97,6 @@ export const TEMPLATE_COMPONENTS = [ TmplHelpIconComponent, TmplIconBannerComponent, TmplImageComponent, - TmplJourneyComponent, TmplLatexComponent, TmplLottieAnimation, TmplNavigationBarComponent, @@ -106,6 +105,7 @@ export const TEMPLATE_COMPONENTS = [ TmplParentPointBoxComponent, TmplParentPointCounterComponent, TmplPdfComponent, + TmplProgressPathComponent, TmplQRCodeComponent, TmplRadioButtonGridComponent, TmplRadioGroupComponent, @@ -157,7 +157,6 @@ export const TEMPLATE_COMPONENT_MAPPING: Record< icon_banner: TmplIconBannerComponent, image: TmplImageComponent, items: null, - journey: TmplJourneyComponent, latex: TmplLatexComponent, lottie_animation: TmplLottieAnimation, nav_group: NavGroupComponent, @@ -169,6 +168,7 @@ export const TEMPLATE_COMPONENT_MAPPING: Record< parent_point_box: TmplParentPointBoxComponent, parent_point_counter: TmplParentPointCounterComponent, pdf: TmplPdfComponent, + progress_path: TmplProgressPathComponent, qr_code: TmplQRCodeComponent, radio_button_grid: TmplRadioButtonGridComponent, radio_group: TmplRadioGroupComponent, diff --git a/src/app/shared/components/template/components/journey/journey.component.html b/src/app/shared/components/template/components/progress-path/progress-path.component.html similarity index 64% rename from src/app/shared/components/template/components/journey/journey.component.html rename to src/app/shared/components/template/components/progress-path/progress-path.component.html index 7ec8403d0..789ff8b24 100644 --- a/src/app/shared/components/template/components/journey/journey.component.html +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.html @@ -1,7 +1,10 @@ -
+
@for (childRow of _row.rows | filterDisplayComponent; track trackByRow($index, childRow)) { -
-
+
+
diff --git a/src/app/shared/components/template/components/journey/journey.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss similarity index 83% rename from src/app/shared/components/template/components/journey/journey.component.scss rename to src/app/shared/components/template/components/progress-path/progress-path.component.scss index 3c7e7777d..a4388e192 100644 --- a/src/app/shared/components/template/components/journey/journey.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -1,6 +1,6 @@ $path-segment-width: 264px; -.journey-group-wrapper { +.progress-path-wrapper { position: relative; display: flex; flex-direction: column; @@ -9,7 +9,7 @@ $path-segment-width: 264px; margin: auto; } -.journey-child-wrapper { +.progress-path-child-wrapper { position: relative; margin-bottom: -34px; display: flex; @@ -23,13 +23,13 @@ $path-segment-width: 264px; width: $path-segment-width; } - .journey-child-content { + .progress-path-child-content { width: 150px; text-align: center; } &.odd { - .journey-child-content { + .progress-path-child-content { align-self: flex-start; } .path-segment { @@ -38,7 +38,7 @@ $path-segment-width: 264px; } &.even { - .journey-child-content { + .progress-path-child-content { align-self: flex-end; } .path-segment { diff --git a/src/app/shared/components/template/components/journey/journey.component.spec.ts b/src/app/shared/components/template/components/progress-path/progress-path.component.spec.ts similarity index 55% rename from src/app/shared/components/template/components/journey/journey.component.spec.ts rename to src/app/shared/components/template/components/progress-path/progress-path.component.spec.ts index 98e1a615e..2f965f4ee 100644 --- a/src/app/shared/components/template/components/journey/journey.component.spec.ts +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { IonicModule } from "@ionic/angular"; -import { TmplJourneyComponent } from "./journey.component"; +import { TmplProgressPathComponent } from "./progress-path.component"; -describe("JourneyComponent", () => { - let component: TmplJourneyComponent; - let fixture: ComponentFixture; +describe("TmplProgressPathComponent", () => { + let component: TmplProgressPathComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [TmplJourneyComponent], + declarations: [TmplProgressPathComponent], imports: [IonicModule.forRoot()], }).compileComponents(); - fixture = TestBed.createComponent(TmplJourneyComponent); + fixture = TestBed.createComponent(TmplProgressPathComponent); component = fixture.componentInstance; fixture.detectChanges(); })); diff --git a/src/app/shared/components/template/components/journey/journey.component.ts b/src/app/shared/components/template/components/progress-path/progress-path.component.ts similarity index 60% rename from src/app/shared/components/template/components/journey/journey.component.ts rename to src/app/shared/components/template/components/progress-path/progress-path.component.ts index a04147d64..bc37c66f1 100644 --- a/src/app/shared/components/template/components/journey/journey.component.ts +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.ts @@ -2,17 +2,17 @@ import { Component, OnInit } from "@angular/core"; import { TemplateBaseComponent } from "../base"; import { getStringParamFromTemplateRow } from "src/app/shared/utils"; -interface IJourneyParams { +interface IProgressPathParams { /** TEMPLATE PARAMETER: path_segment_asset */ pathSegmentAsset: string; } @Component({ - selector: "plh-journey", - templateUrl: "./journey.component.html", - styleUrls: ["./journey.component.scss"], + selector: "plh-progress-path", + templateUrl: "./progress-path.component.html", + styleUrls: ["./progress-path.component.scss"], }) -export class TmplJourneyComponent extends TemplateBaseComponent implements OnInit { - params: Partial = {}; +export class TmplProgressPathComponent extends TemplateBaseComponent implements OnInit { + params: Partial = {}; ngOnInit() { this.getParams(); From b8d3c1373628dce3d5e2e76470202c7d8d48d63d Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 21 Aug 2024 12:32:33 +0100 Subject: [PATCH 058/204] chore: handle highlighted badge; code tidy --- .../progress-path.component.scss | 1 - .../task-card/task-card.component.html | 32 +++++++++++-------- .../task-card/task-card.component.scss | 16 +++------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss index a4388e192..3ec8c3b4b 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -25,7 +25,6 @@ $path-segment-width: 264px; .progress-path-child-content { width: 150px; - text-align: center; } &.odd { diff --git a/src/app/shared/components/template/components/task-card/task-card.component.html b/src/app/shared/components/template/components/task-card/task-card.component.html index 2baeba45a..fe857068b 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.html +++ b/src/app/shared/components/template/components/task-card/task-card.component.html @@ -89,19 +89,25 @@

(newlyCompleted)="handleNewlyCompleted($event)" >

- - - - - + @if (highlighted) { + + {{ highlightedText }} + + } @else { + + + + + + }
diff --git a/src/app/shared/components/template/components/task-card/task-card.component.scss b/src/app/shared/components/template/components/task-card/task-card.component.scss index 146eea3a3..ef7ce2ba4 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.scss +++ b/src/app/shared/components/template/components/task-card/task-card.component.scss @@ -159,17 +159,10 @@ .badge { position: absolute; top: -6px; + right: 14px; + z-index: 2; &.highlighted-badge { - right: -10px; - padding: 5px 10px; - border-radius: var(--ion-border-radius-small); - background: var(--ion-color-secondary); - color: white; - font-weight: var(--font-weight-bold); - } - &.progress-badge { - z-index: 2; - right: 32px; + right: -4px; } } @@ -193,7 +186,8 @@ width: 140px; p { - line-height: inherit; + text-align: center; + line-height: var(--line-height-text); font-size: var(--font-size-text-large); color: var(--ion-color-primary); font-weight: var(--font-weight-bold); From 2b9f3016f1e64fa22f58a2a9dabc24b73711c17b Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 21 Aug 2024 14:24:32 +0100 Subject: [PATCH 059/204] fix: make calc context vars available immediately on init --- .../services/template-calc.service.ts | 29 +++++++++++++++---- .../services/userMeta/userMeta.service.ts | 1 + 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/app/shared/components/template/services/template-calc.service.ts b/src/app/shared/components/template/services/template-calc.service.ts index b9c577da4..42e4ce5b8 100644 --- a/src/app/shared/components/template/services/template-calc.service.ts +++ b/src/app/shared/components/template/services/template-calc.service.ts @@ -1,15 +1,19 @@ import { IFunctionHashmap, IConstantHashmap } from "src/app/shared/utils"; - import { Injectable } from "@angular/core"; +import { Device, DeviceInfo } from "@capacitor/device"; import * as date_fns from "date-fns"; import { ServerService } from "src/app/shared/services/server/server.service"; import { DataEvaluationService } from "src/app/shared/services/data/data-evaluation.service"; import { AsyncServiceBase } from "src/app/shared/services/asyncService.base"; import { PLH_CALC_FUNCTIONS } from "./template-calc-functions/plh-calc-functions"; import { CORE_CALC_FUNCTIONS } from "./template-calc-functions/core-calc-functions"; +import { UserMetaService } from "src/app/shared/services/userMeta/userMeta.service"; +import { LocalStorageService } from "src/app/shared/services/local-storage/local-storage.service"; @Injectable({ providedIn: "root" }) export class TemplateCalcService extends AsyncServiceBase { + private app_user_id: string; + private device_info: DeviceInfo; /** list of all variables accessible directly within calculations */ private calcContext: ICalcContext; @@ -20,14 +24,18 @@ export class TemplateCalcService extends AsyncServiceBase { constructor( private serverService: ServerService, - private dataEvaluationService: DataEvaluationService + private dataEvaluationService: DataEvaluationService, + private localStorageService: LocalStorageService, + private userMetaService: UserMetaService ) { super("TemplateCalc"); this.registerInitFunction(this.initialise); } private async initialise() { - this.ensureSyncServicesReady([this.serverService]); - await this.ensureAsyncServicesReady([this.dataEvaluationService]); + this.ensureSyncServicesReady([this.serverService, this.localStorageService]); + await this.ensureAsyncServicesReady([this.dataEvaluationService, this.userMetaService]); + await this.setUserMetaData(); + this.getCalcContext(); } /** Provide calc context, initialising only once */ @@ -57,11 +65,20 @@ export class TemplateCalcService extends AsyncServiceBase { calc: (v: any) => v, // include simple function so @calc(...) returns the value already parsed inside app_day: this.dataEvaluationService.data.app_day, app_first_launch: this.dataEvaluationService.data.first_app_launch, - app_user_id: this.serverService.app_user_id, - device_info: this.serverService.device_info, + app_user_id: this.app_user_id, + device_info: this.device_info, }; } + private async setUserMetaData() { + if (!this.device_info) { + this.device_info = await Device.getInfo(); + } + if (!this.app_user_id) { + this.app_user_id = this.localStorageService.getProtected("APP_USER_ID"); + } + } + /** * Provide a list of variables that can be accessed directly within calculations * diff --git a/src/app/shared/services/userMeta/userMeta.service.ts b/src/app/shared/services/userMeta/userMeta.service.ts index 2f227e63a..102c6a9c2 100644 --- a/src/app/shared/services/userMeta/userMeta.service.ts +++ b/src/app/shared/services/userMeta/userMeta.service.ts @@ -30,6 +30,7 @@ export class UserMetaService extends AsyncServiceBase { /** When first initialising ensure a default profile created and any newer defaults are merged with older user profiles */ private async initialise() { + this.ensureSyncServicesReady([this.localStorageService]); await this.ensureAsyncServicesReady([ this.dbService, this.fieldService, From 6c619ac73856d9c88acb255b0f9693dee8f5f811 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 21 Aug 2024 15:13:37 +0100 Subject: [PATCH 060/204] docs: updates to README and Deployments section --- README.md | 33 ++++++++++------- documentation/docs/developers/deployments.md | 24 ++++++------- documentation/docs/index.md | 37 ++++++++++++-------- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 34ee84d21..d71c085b8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # Quickstart -## Prequisites +## Prerequisites 1. Download and install [Git](https://git-scm.com/downloads) This will be used to download the repository @@ -14,8 +14,8 @@ 2. Download and install [Git LFS](https://git-lfs.github.com/) This will be used to download any required binary assets, such as images or pdfs -3. Download and install [Node](https://nodejs.org/en/download/) - This is the programming lanugage required to run the project +3. Download and install [Node](https://nodejs.org/en/download/) (choose v18.20.4) + This is the programming language required to run the project 4. Download and Install [Yarn](https://classic.yarnpkg.com/en/docs/install) This manages all 3rd-party code dependencies @@ -24,16 +24,21 @@ ### Download the repo with binary assets ``` -$ git lfs clone https://github.com/IDEMSInternational/open-app-builder.git +git lfs clone https://github.com/IDEMSInternational/open-app-builder.git ``` Note - if you do a regular git clone, you can always run `git lfs fetch --all` later to sync assets ### Install required dependencies +Navigate to the newly cloned directory if you have not done so already: ``` -$ cd open-app-builder -$ yarn install +cd open-app-builder ``` -Note - you may have to do this from time to time when content is updated) + +From the route of the project, run the following command to download and install the required dependencies: +``` +yarn install +``` +Note - you may have to do this from time to time when the code is updated ## Configuration ### Set Deployment @@ -43,9 +48,9 @@ To use an existing deployment, run the following script: ``` yarn workflow deployment set ``` -This will present an interactive list of deployments to select from. +If you have already imported or created a deployment, this will present an interactive list of deployments to select from. -See [Deployment Documentation](https://idemsinternational.github.io/open-app-builder/developers/deployments/) for information about creating and configuring deployments. +If you have no available deployments, see [Deployment Documentation](https://idemsinternational.github.io/open-app-builder/developers/deployments/) for information about creating and configuring deployments. ## Running locally @@ -53,12 +58,16 @@ See [Deployment Documentation](https://idemsinternational.github.io/open-app-bui ``` yarn start ``` -This will start a local server and serve the app in your browser on http://localhost:4200 +This will start a local server and serve the app in your browser on http://localhost:4200. + +## Finishing setup + +In order to complete the setup process, navigate to the relevant section of the documentation from the options below, and continue with the steps outlined. -# For Content Coders +### For Content Authors Please see [Quickstart Authors](https://idemsinternational.github.io/open-app-builder/authors/quickstart/) -# For Developers +### For Developers Please see [Quickstart Developers](https://idemsinternational.github.io/open-app-builder/developers/quickstart/) diff --git a/documentation/docs/developers/deployments.md b/documentation/docs/developers/deployments.md index 9923fabad..9452b3135 100644 --- a/documentation/docs/developers/deployments.md +++ b/documentation/docs/developers/deployments.md @@ -2,8 +2,16 @@ All user-generated content are stored within deployments, alongside app-specific settings such as remote data sources and app strings. +## Import Existing +If an external content repo already exists it is possible to directly import it into the local workspace, instead of first creating a new deployment and then configuring for import. +This can be done via the script +``` +yarn workflow deployment import [url] +``` +Where [url] can be replaced with the url of a github repository where content is stored, e.g. https://github.com/IDEMSInternational/app-debug-content. +You will see the new deployment appear in the `.idems_app` folder and be available for selection ## Create Deployment All deployments are stored in the `.idems_app/deployments` folder, and new deployments can be added by calling the script: @@ -43,9 +51,8 @@ const config = generateDeploymentConfig("example"); // Main Deployment config config.google_drive = { - sheets_folder_ids: [], - assets_folder_ids: [], - } + sheets_folder_ids: [], + assets_folder_ids: [], }; // Deployment app config overrides @@ -90,17 +97,6 @@ const config: IDeploymentConfig = { }, ``` -### Import Existing -If an external content repo already exists it is possible to directly import into the local workspace, instead of first creating a new deployment and then configuring for import. - -This can be done via the script -``` -yarn workflow deployment import [url] -``` -Where [url] can be replaced with the url of a github repository where content is stored - -You will see the new deployment appear in the `.idems_app` folder and be available for selection - ### Sync Content Content from external repos can be synced in the usual way diff --git a/documentation/docs/index.md b/documentation/docs/index.md index 4522d4590..50f7fa3b9 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -8,7 +8,7 @@ 2. Download and install [Git LFS](https://git-lfs.github.com/) This will be used to download any required binary assets, such as images or pdfs -3. Download and install [Node](https://nodejs.org/en/download/) +3. Download and install [Node](https://nodejs.org/en/download/) (choose v18.20.4) This is the programming language required to run the project 4. Download and Install [Yarn](https://classic.yarnpkg.com/en/docs/install) @@ -18,28 +18,33 @@ ### Download the repo with binary assets ``` -$ git lfs clone https://github.com/IDEMSInternational/open-app-builder.git +git lfs clone https://github.com/IDEMSInternational/open-app-builder.git ``` Note - if you do a regular git clone, you can always run `git lfs fetch --all` later to sync assets ### Install required dependencies +Navigate to the newly cloned directory if you have not done so already: ``` -$ cd open-app-builder -$ yarn install +cd open-app-builder ``` -Note - you may have to do this from time to time when content is updated) + +From the route of the project, run the following command to download and install the required dependencies: +``` +yarn install +``` +Note - you may have to do this from time to time when the code is updated ## Configuration ### Set Deployment -Deployments are used to configure data sources (such as google drive) and store generated content. +The app supports using different workspace or deployment configurations. These are stored in [.idems_app/deployments](./.idems_app/deployments) -An initial deployment can be created via the command +To use an existing deployment, run the following script: ``` -yarn workflow deployment create +yarn workflow deployment set ``` -You will be prompted to specify the deployment type, this should be a `New Local Deployment`. You will also be prompted to provide a name. +If you have already imported or created a deployment, this will present an interactive list of deployments to select from. -See [Deployment Documentation](./developers/deployments.md) for more information about configuring deployments +If you have no available deployments, see [Deployment Documentation](https://idemsinternational.github.io/open-app-builder/developers/deployments/) for information about creating and configuring deployments. ## Running locally @@ -47,12 +52,16 @@ See [Deployment Documentation](./developers/deployments.md) for more information ``` yarn start ``` -This will start a local server and serve the app in your browser on http://localhost:4200 +This will start a local server and serve the app in your browser on http://localhost:4200. + +## Finishing setup + +In order to complete the setup process, navigate to the relevant section of the documentation from the options below, and continue with the steps outlined. -# For Content Coders +### For Content Authors Please see [Quickstart Authors](./authors/quickstart.md) -# For Developers +### For Developers -Please see [Quickstart Developers](./developers/quickstart.md) +Please see [Quickstart Developers](/developers/quickstart.md) From 662a0484cde9352fdc5d7411abef7ff2645051ed Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 22 Aug 2024 10:59:51 +0100 Subject: [PATCH 061/204] refactor: use Angular's native context variables for tracking odd index children --- .../progress-path/progress-path.component.html | 5 +---- .../progress-path/progress-path.component.scss | 18 ++++++------------ 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.html b/src/app/shared/components/template/components/progress-path/progress-path.component.html index 789ff8b24..8e064b93e 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.html +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.html @@ -1,9 +1,6 @@
@for (childRow of _row.rows | filterDisplayComponent; track trackByRow($index, childRow)) { -
+
diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss index 3ec8c3b4b..d5e5bc24b 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -11,7 +11,7 @@ $path-segment-width: 264px; .progress-path-child-wrapper { position: relative; - margin-bottom: -34px; + margin-bottom: 34px; display: flex; flex-direction: column; align-items: center; @@ -21,27 +21,21 @@ $path-segment-width: 264px; top: 25%; z-index: -1; width: $path-segment-width; + margin-left: 32px; } .progress-path-child-content { width: 150px; + align-self: flex-start; } - &.odd { - .progress-path-child-content { - align-self: flex-start; - } - .path-segment { - margin-left: 32px; - } - } - - &.even { + // For alternate instances, mirror the way the content displays for a staggered effect + &.odd-index { .progress-path-child-content { align-self: flex-end; } .path-segment { - // Flip horizontally for alternate instances + // Flip horizontally transform: scaleX(-1); margin-right: 32px; } From 349f22c5dd6c7df4b1a7714426d1d99fdcdc577c Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 22 Aug 2024 14:54:34 +0100 Subject: [PATCH 062/204] chore: accommodate longer multi-line task card titles in progress-path --- .../progress-path.component.html | 7 +- .../progress-path.component.scss | 15 +++- .../task-card/task-card.component.html | 66 +++++++++-------- .../task-card/task-card.component.scss | 73 +++++++++++-------- 4 files changed, 94 insertions(+), 67 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.html b/src/app/shared/components/template/components/progress-path/progress-path.component.html index 8e064b93e..77740bf00 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.html +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.html @@ -2,7 +2,12 @@ @for (childRow of _row.rows | filterDisplayComponent; track trackByRow($index, childRow)) {
- +
diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss index d5e5bc24b..0c04ae423 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -10,18 +10,24 @@ $path-segment-width: 264px; } .progress-path-child-wrapper { + $path-segment-spacing: 24px; + position: relative; - margin-bottom: 34px; + margin-bottom: 96px; display: flex; flex-direction: column; align-items: center; .path-segment { position: absolute; - top: 25%; + top: 40px; + margin-left: $path-segment-spacing; z-index: -1; width: $path-segment-width; - margin-left: 32px; + svg { + inline-size: 100%; + block-size: 100%; + } } .progress-path-child-content { @@ -37,7 +43,8 @@ $path-segment-width: 264px; .path-segment { // Flip horizontally transform: scaleX(-1); - margin-right: 32px; + margin-left: 0px; + margin-right: $path-segment-spacing; } } diff --git a/src/app/shared/components/template/components/task-card/task-card.component.html b/src/app/shared/components/template/components/task-card/task-card.component.html index fe857068b..889c13fb0 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.html +++ b/src/app/shared/components/template/components/task-card/task-card.component.html @@ -78,38 +78,40 @@

} @else {
-
- -
- @if (highlighted) { - - {{ highlightedText }} - - } @else { - - - - - - } -
- +
+
+ +
+ @if (highlighted) { + + {{ highlightedText }} + + } @else { + + + + + + } +
+ +
@if (title) {
diff --git a/src/app/shared/components/template/components/task-card/task-card.component.scss b/src/app/shared/components/template/components/task-card/task-card.component.scss index ef7ce2ba4..0e29a4c16 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.scss +++ b/src/app/shared/components/template/components/task-card/task-card.component.scss @@ -147,43 +147,60 @@ } .circle-card-wrapper { - position: relative; + $circle-width: 100px; + width: 100%; - height: 160px; - border-radius: 50%; - padding: 0; display: flex; flex-direction: column; - align-items: center; - .badge { - position: absolute; - top: -6px; - right: 14px; - z-index: 2; - &.highlighted-badge { - right: -4px; - } - } + // HACK: use custom CSS variable, set in progress-path component, to set flex alignment + // based on the index parity of the card component within progress-path. + // Default to center when not a child of progress-path + align-items: var(--progress-path-flex-align, center); - .circle-wrapper { - width: 100px; - height: 100px; + .image-and-badge-wrapper { + position: relative; + width: 100%; border-radius: 50%; - overflow: hidden; - background-color: white; - border: 1px solid rgba(black, 0.07); - filter: drop-shadow(var(--ion-default-box-shadow)); + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + + .badge { + position: absolute; + top: -6px; + right: 14px; + z-index: 2; + &.highlighted-badge { + right: -4px; + } + } + + .circle-wrapper { + width: $circle-width; + height: $circle-width; + border-radius: 50%; + overflow: hidden; + background-color: white; + border: 1px solid rgba(black, 0.07); + filter: drop-shadow(var(--ion-default-box-shadow)); - .img { - width: 100px; + .img { + width: $circle-width; + } } - } + plh-task-progress-bar { + position: absolute; + visibility: none; + } + } .title-wrapper { position: absolute; - top: 100px; - width: 140px; + top: $circle-width; + min-width: 150px; + max-width: 240px; p { text-align: center; @@ -193,10 +210,6 @@ font-weight: var(--font-weight-bold); } } - - plh-task-progress-bar { - position: absolute; - } } // TODO: These could be moved to the global scope, and possibly reworked to be "styles" rather than "variants" From fa704a4ab643f1c8fc455f9e58761bf9c3fdb1ca Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 22 Aug 2024 15:23:06 +0100 Subject: [PATCH 063/204] refactor: path is now drawn using inline svg, with colour and stroke wifth parameters exposed to themes --- .../progress-path.component.html | 24 ++++++++++++++++++- .../progress-path.component.scss | 4 ++++ .../progress-path/progress-path.component.ts | 23 ++---------------- src/theme/themes/default.scss | 2 ++ src/theme/themes/early_family_math.scss | 4 +++- src/theme/themes/pfr.scss | 2 ++ src/theme/themes/plh_facilitator_mx.scss | 4 +++- src/theme/themes/professional.scss | 4 +++- 8 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.html b/src/app/shared/components/template/components/progress-path/progress-path.component.html index 77740bf00..ad208e6ad 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.html +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.html @@ -11,7 +11,29 @@
- + + + + + + +
} diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss index 0c04ae423..045b7a628 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -1,3 +1,5 @@ +$path-background: var(--progress-path-line-background, var(--ion-color-primary-200)); +$path-weight: var(--progress-path-line-weight, 72px); $path-segment-width: 264px; .progress-path-wrapper { @@ -27,6 +29,8 @@ $path-segment-width: 264px; svg { inline-size: 100%; block-size: 100%; + stroke: $path-background; + stroke-width: $path-weight; } } diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.ts b/src/app/shared/components/template/components/progress-path/progress-path.component.ts index bc37c66f1..52bc3d7d5 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.ts +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.ts @@ -1,28 +1,9 @@ -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { TemplateBaseComponent } from "../base"; -import { getStringParamFromTemplateRow } from "src/app/shared/utils"; -interface IProgressPathParams { - /** TEMPLATE PARAMETER: path_segment_asset */ - pathSegmentAsset: string; -} @Component({ selector: "plh-progress-path", templateUrl: "./progress-path.component.html", styleUrls: ["./progress-path.component.scss"], }) -export class TmplProgressPathComponent extends TemplateBaseComponent implements OnInit { - params: Partial = {}; - - ngOnInit() { - this.getParams(); - } - - getParams() { - this.params.pathSegmentAsset = getStringParamFromTemplateRow( - this._row, - "path_segment_asset", - "" - ); - } -} +export class TmplProgressPathComponent extends TemplateBaseComponent {} diff --git a/src/theme/themes/default.scss b/src/theme/themes/default.scss index ef063cd5b..8cf412e64 100644 --- a/src/theme/themes/default.scss +++ b/src/theme/themes/default.scss @@ -52,6 +52,8 @@ ion-item-background: var(--ion-color-gray-light), // task-progress-bar-color: var(--ion-color-primary), // checkbox-background-color: white, + // progress-path-line-background, var(--ion-color-gray-100), + // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background); @each $name, $value in $variable-overrides { diff --git a/src/theme/themes/early_family_math.scss b/src/theme/themes/early_family_math.scss index e306f5203..4ea98766f 100644 --- a/src/theme/themes/early_family_math.scss +++ b/src/theme/themes/early_family_math.scss @@ -47,7 +47,9 @@ // radio-button-font-color: var(--ion-color-primary), ion-item-background: var(--ion-color-gray-light), task-progress-bar-color: var(--ion-color-green), - // checkbox-background-color: white + // checkbox-background-color: white, + // progress-path-line-background, var(--ion-color-gray-100), + // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background); @each $name, $value in $variable-overrides { diff --git a/src/theme/themes/pfr.scss b/src/theme/themes/pfr.scss index 436481110..47e39d1d3 100644 --- a/src/theme/themes/pfr.scss +++ b/src/theme/themes/pfr.scss @@ -49,6 +49,8 @@ ion-item-background: var(--ion-color-gray-light), // task-progress-bar-color: var(--ion-color-primary), // checkbox-background-color: white, + // progress-path-line-background, var(--ion-color-gray-100), + // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background, $g: $green); @each $name, $value in $variable-overrides { diff --git a/src/theme/themes/plh_facilitator_mx.scss b/src/theme/themes/plh_facilitator_mx.scss index 64dff6dd1..c68c99ee4 100644 --- a/src/theme/themes/plh_facilitator_mx.scss +++ b/src/theme/themes/plh_facilitator_mx.scss @@ -47,7 +47,9 @@ // radio-button-font-color: var(--ion-color-primary), ion-item-background: var(--ion-color-gray-light), task-progress-bar-color: var(--ion-color-green), - // checkbox-background-color: white + // checkbox-background-color: white, + // progress-path-line-background, var(--ion-color-gray-100), + // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background); @each $name, $value in $variable-overrides { diff --git a/src/theme/themes/professional.scss b/src/theme/themes/professional.scss index 6c95697ee..717a8820b 100644 --- a/src/theme/themes/professional.scss +++ b/src/theme/themes/professional.scss @@ -47,7 +47,9 @@ // radio-button-font-color: var(--ion-color-primary), ion-item-background: var(--ion-color-gray-light), task-progress-bar-color: var(--ion-color-green), - // checkbox-background-color: white + // checkbox-background-color: white, + progress-path-line-background: var(--ion-color-gray-100), + // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background); @each $name, $value in $variable-overrides { From d41a3f762cb48709805a21f9ad38b816b6ad312b Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 22 Aug 2024 17:21:10 +0100 Subject: [PATCH 064/204] chore: default to 'md' variant on web; clarify variant logic; handle both variants in button --- .../components/toggle-bar/toggle-bar.scss | 7 ++++- .../components/toggle-bar/toggle-bar.ts | 29 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss index e43bf6126..62b8af8ef 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.scss @@ -31,7 +31,12 @@ ion-toggle { &[data-param-style~="in_button"], &[data-variant~="in_button"] { ion-toggle { - transform: translate(4px, 4px); + transform: translate(12px, -4px); + } + &[data-variant~="ios"] { + ion-toggle { + transform: translate(4px, 4px); + } } } diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts index 381a17262..c9d037718 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from "@angular/core"; +import { Capacitor } from "@capacitor/core"; import { TemplateBaseComponent } from "../base"; import { ITemplateRowProps } from "../../models"; import { @@ -7,7 +8,10 @@ import { } from "src/app/shared/utils"; interface IToggleParams { - /** TEMPLATE PARAMETER: "variant" */ + /** + * TEMPLATE PARAMETER: "variant". Setting "ios" or "android" will style the toggle to match the respective + * platform, otherwise the default is to match the current device platform, using "ios" on web. + * */ variant: "" | "icon" | "in_button" | "ios" | "android"; /** TEMPLATE PARAMETER: "style". Legacy, use "variant" instead. */ style: string; @@ -37,6 +41,10 @@ export class TmplToggleBarComponent implements ITemplateRowProps, OnInit { params: Partial = {}; + /** + * The ion-toggle component uses "md" ("material design") and "ios" to refer to visual styles of the component + * corresponding to the respective platforms. See docs here: https://ionicframework.com/docs/api/toggle + */ platformVariant: "ios" | "md" = "ios"; /** @ignore */ variantMap: { icon: boolean }; @@ -74,12 +82,29 @@ export class TmplToggleBarComponent this.params.variant = getStringParamFromTemplateRow(this._row, "variant", "") .split(",") .join(" ") as IToggleParams["variant"]; - this.platformVariant = this.params.variant.split(" ").includes("android") ? "md" : "ios"; + this.setPlatformVariant(this.params.variant); this.populateVariantMap(); this.params.iconTrue = getStringParamFromTemplateRow(this._row, "icon_true_asset", ""); this.params.iconFalse = getStringParamFromTemplateRow(this._row, "icon_false_asset", ""); } + /** + * Use the platform variant explicitly set by the author, + * otherwise default to "ios" on iOS, and "md" on Android and web + * @param variantString A space-separated string of variants + */ + private setPlatformVariant(variantString: string) { + const variantArray = variantString.split(" "); + + if (variantArray.includes("ios")) { + this.platformVariant = "ios"; + } else if (variantArray.includes("android")) { + this.platformVariant = "md"; + } else { + this.platformVariant = Capacitor.getPlatform() === "ios" ? "ios" : "md"; + } + } + private populateVariantMap() { const variantArray = this.params.variant.split(" "); this.variantMap = { From 2fcb65fd208b3cf68b00ca2fa57dfe09f757982b Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 22 Aug 2024 11:31:34 -0700 Subject: [PATCH 065/204] refactor: progress path svg Recreate svg element to avoid use of nested groups and transformations. Fix inheritance of stroke color. Remove path-line-weight variable (as image distorts when using different weight) --- .../progress-path.component.html | 24 ++----------------- .../progress-path.component.scss | 7 +++--- src/theme/themes/default.scss | 1 - src/theme/themes/early_family_math.scss | 1 - src/theme/themes/pfr.scss | 1 - src/theme/themes/plh_facilitator_mx.scss | 1 - src/theme/themes/professional.scss | 1 - 7 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.html b/src/app/shared/components/template/components/progress-path/progress-path.component.html index ad208e6ad..211713ba5 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.html +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.html @@ -11,28 +11,8 @@
- - - - - - + +
diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss index 045b7a628..5e1e58d58 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -1,5 +1,4 @@ -$path-background: var(--progress-path-line-background, var(--ion-color-primary-200)); -$path-weight: var(--progress-path-line-weight, 72px); +$path-background: var(--progress-path-line-background, var(--ion-color-gray-100)); $path-segment-width: 264px; .progress-path-wrapper { @@ -15,7 +14,7 @@ $path-segment-width: 264px; $path-segment-spacing: 24px; position: relative; - margin-bottom: 96px; + margin-bottom: 27px; display: flex; flex-direction: column; align-items: center; @@ -30,7 +29,7 @@ $path-segment-width: 264px; inline-size: 100%; block-size: 100%; stroke: $path-background; - stroke-width: $path-weight; + stroke-width: 51px; } } diff --git a/src/theme/themes/default.scss b/src/theme/themes/default.scss index 8cf412e64..fa54f62b9 100644 --- a/src/theme/themes/default.scss +++ b/src/theme/themes/default.scss @@ -53,7 +53,6 @@ // task-progress-bar-color: var(--ion-color-primary), // checkbox-background-color: white, // progress-path-line-background, var(--ion-color-gray-100), - // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background); @each $name, $value in $variable-overrides { diff --git a/src/theme/themes/early_family_math.scss b/src/theme/themes/early_family_math.scss index 4ea98766f..c2e3895a7 100644 --- a/src/theme/themes/early_family_math.scss +++ b/src/theme/themes/early_family_math.scss @@ -49,7 +49,6 @@ task-progress-bar-color: var(--ion-color-green), // checkbox-background-color: white, // progress-path-line-background, var(--ion-color-gray-100), - // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background); @each $name, $value in $variable-overrides { diff --git a/src/theme/themes/pfr.scss b/src/theme/themes/pfr.scss index 47e39d1d3..38649f354 100644 --- a/src/theme/themes/pfr.scss +++ b/src/theme/themes/pfr.scss @@ -50,7 +50,6 @@ // task-progress-bar-color: var(--ion-color-primary), // checkbox-background-color: white, // progress-path-line-background, var(--ion-color-gray-100), - // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background, $g: $green); @each $name, $value in $variable-overrides { diff --git a/src/theme/themes/plh_facilitator_mx.scss b/src/theme/themes/plh_facilitator_mx.scss index c68c99ee4..9e823d425 100644 --- a/src/theme/themes/plh_facilitator_mx.scss +++ b/src/theme/themes/plh_facilitator_mx.scss @@ -49,7 +49,6 @@ task-progress-bar-color: var(--ion-color-green), // checkbox-background-color: white, // progress-path-line-background, var(--ion-color-gray-100), - // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background); @each $name, $value in $variable-overrides { diff --git a/src/theme/themes/professional.scss b/src/theme/themes/professional.scss index 717a8820b..2668bc2cb 100644 --- a/src/theme/themes/professional.scss +++ b/src/theme/themes/professional.scss @@ -49,7 +49,6 @@ task-progress-bar-color: var(--ion-color-green), // checkbox-background-color: white, progress-path-line-background: var(--ion-color-gray-100), - // progress-path-line-weight: 72px, ); @include utils.generateTheme($color-primary, $color-secondary, $page-background); @each $name, $value in $variable-overrides { From 1c75030b51c3b48489e6c6dabfa715aecccabc4f Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 22 Aug 2024 11:37:37 -0700 Subject: [PATCH 066/204] chore: code tidying --- .../components/progress-path/progress-path.component.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss index 5e1e58d58..7e1163a2e 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -1,5 +1,6 @@ -$path-background: var(--progress-path-line-background, var(--ion-color-gray-100)); +$path-background: var(--progress-path-line-background, var(--ion-color-primary-200)); $path-segment-width: 264px; +$path-stroke-width: 51px; .progress-path-wrapper { position: relative; @@ -29,7 +30,7 @@ $path-segment-width: 264px; inline-size: 100%; block-size: 100%; stroke: $path-background; - stroke-width: 51px; + stroke-width: $path-stroke-width; } } From 711fe551aae717af64f03a5c345402e64995ab45 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 22 Aug 2024 22:48:00 -0700 Subject: [PATCH 067/204] refactor: dynamic svg component --- cspell.config.yml | 1 + .../progress-path.component.html | 11 +++- .../progress-path.component.scss | 13 ++-- .../progress-path/progress-path.component.ts | 66 ++++++++++++++++++- 4 files changed, 79 insertions(+), 12 deletions(-) diff --git a/cspell.config.yml b/cspell.config.yml index e02f642f4..eb37eae59 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -34,6 +34,7 @@ "templatename", "tmpl", "venv", + "viewbox", ] # flagWords - list of words to be always considered incorrect # This is useful for offensive words and common spelling errors. diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.html b/src/app/shared/components/template/components/progress-path/progress-path.component.html index 211713ba5..f75bcc1fd 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.html +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.html @@ -1,6 +1,11 @@
@for (childRow of _row.rows | filterDisplayComponent; track trackByRow($index, childRow)) { -
+
- - + +
diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss index 7e1163a2e..cb3c3ed9f 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -1,31 +1,28 @@ $path-background: var(--progress-path-line-background, var(--ion-color-primary-200)); -$path-segment-width: 264px; -$path-stroke-width: 51px; +$path-stroke-width: 32px; .progress-path-wrapper { position: relative; display: flex; flex-direction: column; - min-width: $path-segment-width; - max-width: 380px; margin: auto; + align-items: center; } .progress-path-child-wrapper { $path-segment-spacing: 24px; position: relative; - margin-bottom: 27px; display: flex; flex-direction: column; align-items: center; .path-segment { position: absolute; - top: 40px; - margin-left: $path-segment-spacing; + top: 0; + left: 0; + width: 100%; z-index: -1; - width: $path-segment-width; svg { inline-size: 100%; block-size: 100%; diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.ts b/src/app/shared/components/template/components/progress-path/progress-path.component.ts index 52bc3d7d5..6f82a2b35 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.ts +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.ts @@ -1,9 +1,73 @@ import { Component } from "@angular/core"; import { TemplateBaseComponent } from "../base"; +// HACK - hardcoded sizing values to make content fit reasonably well +const SIZING = { + /** Total width for container */ + widthPx: 384, + /** Target height for text content */ + textContentHeight: 72, + /** Adjust start of line for task card */ + xOffset: 64, + /** Adjust start of line for task card */ + yOffset: 56, +}; + @Component({ selector: "plh-progress-path", templateUrl: "./progress-path.component.html", styleUrls: ["./progress-path.component.scss"], }) -export class TmplProgressPathComponent extends TemplateBaseComponent {} +export class TmplProgressPathComponent extends TemplateBaseComponent { + public svgPath: string; + public svgViewBox: string; + public contentHeight: string; + public width = `${SIZING.widthPx}px`; + + constructor() { + super(); + this.generateSVGPath("wavy"); + } + + /** + * Generate a base SVG segment used to connect 2 progress items together + * Roughly a horizontal line and smooth bend, adjusted for sizing + */ + private generateSVGPath(variant: "basic" | "wavy" = "basic") { + // arbitrary values used to make base width/height fit + const { widthPx, xOffset, yOffset, textContentHeight } = SIZING; + + // adjust viewbox to include both title content and 100px card (+overlap) + const viewboxHeight = textContentHeight + 128; + + // SVG Generation (https://www.aleksandrhovhannisyan.com/blog/svg-tutorial/) + + // M - start point (allow space for stroke width and content offset) + // h - horizontal line, relative length + // c - bezier curve (64 unit rounded) + // v - vertical line, relative length + + // Basic generation, smooth + // https://svg-path-visualizer.netlify.app/#M%20128%2C128%0Ah%20384%20%0Ac%2064%2C0%2064%2C64%2064%2C64%0Av%20352 + const basic = () => + ` + M ${xOffset},${yOffset} + h ${widthPx - 2 * xOffset - 32} + c 32,0 32,32 32,32 + v ${viewboxHeight - yOffset - 32} + `.trim(); + + // Alt generation that is a bit more wavy + // https://svg-path-visualizer.netlify.app/#M%20128%2C128%0Ah%20384%20%0Ac%2064%2C0%20128%2C64%2064%2C200%0A + const wavy = () => + ` + M ${xOffset},${yOffset} + h ${widthPx - 2 * xOffset - 48} + c 48,0 72,64 48,${viewboxHeight - yOffset - 16} + `.trim(); + + this.svgPath = variant === "basic" ? basic() : wavy(); + this.svgViewBox = `0 0 ${widthPx} ${viewboxHeight}`; + this.contentHeight = `${textContentHeight}px`; + } +} From 786f948d8a5ad037c23fddc355c88e290118b913 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 22 Aug 2024 22:49:53 -0700 Subject: [PATCH 068/204] chore: code tidying --- .../components/progress-path/progress-path.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.ts b/src/app/shared/components/template/components/progress-path/progress-path.component.ts index 6f82a2b35..225083c5c 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.ts +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.ts @@ -7,9 +7,9 @@ const SIZING = { widthPx: 384, /** Target height for text content */ textContentHeight: 72, - /** Adjust start of line for task card */ + /** Adjust x for task card overlap */ xOffset: 64, - /** Adjust start of line for task card */ + /** Adjust y for task card overlap */ yOffset: 56, }; @@ -58,7 +58,7 @@ export class TmplProgressPathComponent extends TemplateBaseComponent { `.trim(); // Alt generation that is a bit more wavy - // https://svg-path-visualizer.netlify.app/#M%20128%2C128%0Ah%20384%20%0Ac%2064%2C0%20128%2C64%2064%2C200%0A + // https://svg-path-visualizer.netlify.app/#M%2064%2C56%0Ah%20208%0Ac%2048%2C0%2072%2C64%2048%2C128%0A const wavy = () => ` M ${xOffset},${yOffset} From fda72673cd1aee3cf9669654f3ed21ebfd231944 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 11:03:42 +0100 Subject: [PATCH 069/204] chore: make progress-path responsive and accommodate narrower ciew widths --- .../progress-path/progress-path.component.html | 18 ++++++++++-------- .../progress-path/progress-path.component.scss | 9 ++++++++- .../progress-path/progress-path.component.ts | 10 +++++----- .../task-card/task-card.component.scss | 3 ++- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.html b/src/app/shared/components/template/components/progress-path/progress-path.component.html index f75bcc1fd..9cfbcd983 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.html +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.html @@ -6,14 +6,16 @@ [style.margin-bottom]="contentHeight" [style.width]="width" > -
- - +
+
+ + +
diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss index cb3c3ed9f..45f87ba71 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -1,5 +1,5 @@ $path-background: var(--progress-path-line-background, var(--ion-color-primary-200)); -$path-stroke-width: 32px; +$path-stroke-width: 28px; .progress-path-wrapper { position: relative; @@ -31,6 +31,13 @@ $path-stroke-width: 32px; } } + .progress-path-child-content-wrapper { + width: 100vw; + max-width: calc(var(--container-width) + 20px); + min-width: calc(var(--container-width) - 44px); + display: flex; + flex-direction: column; + } .progress-path-child-content { width: 150px; align-self: flex-start; diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.ts b/src/app/shared/components/template/components/progress-path/progress-path.component.ts index 225083c5c..12e521d4a 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.ts +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.ts @@ -4,13 +4,13 @@ import { TemplateBaseComponent } from "../base"; // HACK - hardcoded sizing values to make content fit reasonably well const SIZING = { /** Total width for container */ - widthPx: 384, + widthPx: 364, /** Target height for text content */ - textContentHeight: 72, + textContentHeight: 82, /** Adjust x for task card overlap */ - xOffset: 64, + xOffset: 68, /** Adjust y for task card overlap */ - yOffset: 56, + yOffset: 48, }; @Component({ @@ -63,7 +63,7 @@ export class TmplProgressPathComponent extends TemplateBaseComponent { ` M ${xOffset},${yOffset} h ${widthPx - 2 * xOffset - 48} - c 48,0 72,64 48,${viewboxHeight - yOffset - 16} + c 48,0 72,64 48,${viewboxHeight - yOffset - 4} `.trim(); this.svgPath = variant === "basic" ? basic() : wavy(); diff --git a/src/app/shared/components/template/components/task-card/task-card.component.scss b/src/app/shared/components/template/components/task-card/task-card.component.scss index 0e29a4c16..e533dffb9 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.scss +++ b/src/app/shared/components/template/components/task-card/task-card.component.scss @@ -201,11 +201,12 @@ top: $circle-width; min-width: 150px; max-width: 240px; + padding: 0 12px; p { text-align: center; line-height: var(--line-height-text); - font-size: var(--font-size-text-large); + font-size: var(--font-size-text-medium); color: var(--ion-color-primary); font-weight: var(--font-weight-bold); } From d7850bd9b3ca50fa9fe97447248e14f4e4da89f2 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 11:24:44 +0100 Subject: [PATCH 070/204] chore: expose 'basic'/'wavy' variant to template params --- .../progress-path/progress-path.component.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.ts b/src/app/shared/components/template/components/progress-path/progress-path.component.ts index 12e521d4a..5e697b091 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.ts +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.ts @@ -1,5 +1,11 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { TemplateBaseComponent } from "../base"; +import { getStringParamFromTemplateRow } from "src/app/shared/utils"; + +interface IProgressPathParams { + /** TEMPLATE_PARAMETER: "variant" */ + variant: "basic" | "wavy"; +} // HACK - hardcoded sizing values to make content fit reasonably well const SIZING = { @@ -18,15 +24,25 @@ const SIZING = { templateUrl: "./progress-path.component.html", styleUrls: ["./progress-path.component.scss"], }) -export class TmplProgressPathComponent extends TemplateBaseComponent { +export class TmplProgressPathComponent extends TemplateBaseComponent implements OnInit { + private params: Partial = {}; + private pathVariant: "basic" | "wavy"; + public svgPath: string; public svgViewBox: string; public contentHeight: string; public width = `${SIZING.widthPx}px`; - constructor() { - super(); - this.generateSVGPath("wavy"); + ngOnInit() { + this.getParams(); + this.generateSVGPath(this.pathVariant); + } + + private getParams() { + this.params.variant = getStringParamFromTemplateRow(this._row, "variant", "basic") + .split(",") + .join(" ") as IProgressPathParams["variant"]; + this.pathVariant = this.params.variant.includes("wavy") ? "wavy" : "basic"; } /** From d1f97b799745212ed60212ea4684eff4a68fcb26 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 14:35:23 +0100 Subject: [PATCH 071/204] chore: move all ios template file references into same top-level .gitignore --- .gitignore | 2 ++ ios/.gitignore | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 8fc0e5ac8..f2f6ca5db 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,5 @@ android/app/src/main/AndroidManifest.xml android/app/src/main/java/international/idems/debug_app/MainActivity.java android/app/src/main/res/values/strings.xml ios/App/App.xcodeproj/project.pbxproj +ios/App/App/capacitor.config.json +ios/App/App/config.xml diff --git a/ios/.gitignore b/ios/.gitignore index f47029973..23d202f2c 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -7,7 +7,3 @@ xcuserdata # Cordova plugins for Capacitor capacitor-cordova-ios-plugins - -# Generated Config files -App/App/capacitor.config.json -App/App/config.xml From f0529b39c7937c0b64ec14f9c82c85ae558964dc Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 14:36:25 +0100 Subject: [PATCH 072/204] chore apply Capcitor 6 migrations --- android/app/build.template.gradle | 2 +- android/app/src/main/res/xml/config.xml | 2 + android/build.gradle | 4 +- android/gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 61608 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 +- android/gradlew | 272 +++++++++++------- android/gradlew.bat | 38 +-- android/variables.gradle | 16 +- package.json | 26 +- yarn.lock | 107 ++++--- 10 files changed, 278 insertions(+), 195 deletions(-) diff --git a/android/app/build.template.gradle b/android/app/build.template.gradle index 6734a6df6..1aa16647d 100644 --- a/android/app/build.template.gradle +++ b/android/app/build.template.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' android { namespace "${APP_ID}" - compileSdkVersion rootProject.ext.compileSdkVersion + compileSdk rootProject.ext.compileSdkVersion defaultConfig { applicationId "${APP_ID}" minSdkVersion rootProject.ext.minSdkVersion diff --git a/android/app/src/main/res/xml/config.xml b/android/app/src/main/res/xml/config.xml index 77bc445a5..1b1b0e0dc 100644 --- a/android/app/src/main/res/xml/config.xml +++ b/android/app/src/main/res/xml/config.xml @@ -1,4 +1,6 @@ + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 1e8c9173a..0bb5d5205 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,8 +7,8 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.1' - classpath 'com.google.gms:google-services:4.3.15' + classpath 'com.android.tools.build:gradle:8.2.1' + classpath 'com.google.gms:google-services:4.4.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..ccebba7710deaf9f98673a68957ea02138b60d0a 100644 GIT binary patch delta 42198 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%0Dx9&DJ+=*NrOLFNd~jpXIl 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 OOi;9)pb#jaQ2zt%fx@f+ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f9db3bc13..9b0a13f0f 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Dec 04 19:15:48 PST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew index 83f2acfdc..79a61d421 100755 --- a/android/gradlew +++ b/android/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,113 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -105,84 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat index 9618d8d96..93e3f59f1 100644 --- a/android/gradlew.bat +++ b/android/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,10 +25,14 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,38 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/android/variables.gradle b/android/variables.gradle index ba23edaed..c0069493c 100644 --- a/android/variables.gradle +++ b/android/variables.gradle @@ -1,18 +1,18 @@ ext { minSdkVersion = 22 - compileSdkVersion = 33 - targetSdkVersion = 33 - androidxActivityVersion = '1.7.0' + compileSdkVersion = 34 + targetSdkVersion = 34 + androidxActivityVersion = '1.8.0' androidxAppCompatVersion = '1.6.1' androidxCoordinatorLayoutVersion = '1.2.0' - firebaseMessagingVersion = '23.1.2' - androidxCoreVersion = '1.10.0' - androidxFragmentVersion = '1.5.6' + firebaseMessagingVersion = '23.3.1' + androidxCoreVersion = '1.12.0' + androidxFragmentVersion = '1.6.2' junitVersion = '4.13.2' androidxJunitVersion = '1.1.5' androidxEspressoCoreVersion = '3.5.1' cordovaAndroidVersion = '10.1.1' rgcfaIncludeGoogle = true - coreSplashScreenVersion = '1.0.0' - androidxWebkitVersion = '1.6.1' + coreSplashScreenVersion = '1.0.1' + androidxWebkitVersion = '1.9.0' } \ No newline at end of file diff --git a/package.json b/package.json index 79b87cf79..2b1458cce 100644 --- a/package.json +++ b/package.json @@ -45,17 +45,17 @@ "@capacitor-firebase/authentication": "^5.3.0", "@capacitor-firebase/crashlytics": "^5.4.1", "@capacitor-firebase/performance": "^5.3.0", - "@capacitor/android": "^5.5.1", - "@capacitor/app": "^5.0.6", - "@capacitor/clipboard": "^5.0.6", - "@capacitor/core": "^5.5.1", - "@capacitor/device": "^5.0.6", - "@capacitor/filesystem": "^5.1.4", - "@capacitor/ios": "^5.7.2", - "@capacitor/local-notifications": "^5.0.6", - "@capacitor/push-notifications": "^5.1.0", - "@capacitor/share": "^5.0.6", - "@capacitor/splash-screen": "^5.0.6", + "@capacitor/android": "^6.0.0", + "@capacitor/app": "^6.0.0", + "@capacitor/clipboard": "^6.0.0", + "@capacitor/core": "^6.0.0", + "@capacitor/device": "^6.0.0", + "@capacitor/filesystem": "^6.0.0", + "@capacitor/ios": "^6.0.0", + "@capacitor/local-notifications": "^6.0.0", + "@capacitor/push-notifications": "^6.0.0", + "@capacitor/share": "^6.0.0", + "@capacitor/splash-screen": "^6.0.0", "@capawesome/capacitor-app-update": "^5.0.1", "@ionic-native/core": "^5.36.0", "@ionic-native/device": "^5.36.0", @@ -123,7 +123,7 @@ "@angular/compiler": "~17.2.2", "@angular/compiler-cli": "~17.2.2", "@angular/language-service": "~17.2.2", - "@capacitor/cli": "^5.5.1", + "@capacitor/cli": "^6.0.0", "@compodoc/compodoc": "^1.1.23", "@ionic/angular-toolkit": "^10.0.0", "@ionic/cli": "^7.1.5", @@ -181,4 +181,4 @@ "node": ">=18.x" }, "packageManager": "yarn@3.3.1" -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index eb2962f24..a1047aafa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2104,13 +2104,14 @@ __metadata: languageName: node linkType: hard -"@capacitor/cli@npm:^5.5.1": - version: 5.7.0 - resolution: "@capacitor/cli@npm:5.7.0" +"@capacitor/cli@npm:^6.1.2": + version: 6.1.2 + resolution: "@capacitor/cli@npm:6.1.2" dependencies: "@ionic/cli-framework-output": ^2.2.5 "@ionic/utils-fs": ^3.1.6 - "@ionic/utils-subprocess": ^2.1.11 + "@ionic/utils-process": ^2.1.11 + "@ionic/utils-subprocess": 2.1.11 "@ionic/utils-terminal": ^2.3.3 commander: ^9.3.0 debug: ^4.3.4 @@ -2128,7 +2129,7 @@ __metadata: bin: cap: bin/capacitor capacitor: bin/capacitor - checksum: ae41ea5176817c8e83edecb45ff6158edf1c49b1cb4b052f7a9ff180fa0663045c6bbf2ffb443531b1eb329c08d92a44d0cb0bb18443d6f1dcfb7b2859570601 + checksum: f3f6b4f134998606cbeb1b2c4e99212d11d88153d2be7be161681d1b362fdebf9aeb8e8bcb67b79e0e74e6e19d42a41c82bfcca5f62e6b154f4f8d22ef0748d9 languageName: node linkType: hard @@ -4756,6 +4757,16 @@ __metadata: languageName: node linkType: hard +"@ionic/utils-array@npm:2.1.5": + version: 2.1.5 + resolution: "@ionic/utils-array@npm:2.1.5" + dependencies: + debug: ^4.0.0 + tslib: ^2.0.1 + checksum: eab54e5ae6c3a7d435e420986cd7a0766c00506a829e001ddfc4124542adace6dd0f0c1fc51fa8f5eaa69bd09354a0b5541ff9b39b61763cb0e415936578fd4b + languageName: node + linkType: hard + "@ionic/utils-array@npm:2.1.6, @ionic/utils-array@npm:^2.1.5": version: 2.1.6 resolution: "@ionic/utils-array@npm:2.1.6" @@ -4766,6 +4777,18 @@ __metadata: languageName: node linkType: hard +"@ionic/utils-fs@npm:3.1.6": + version: 3.1.6 + resolution: "@ionic/utils-fs@npm:3.1.6" + dependencies: + "@types/fs-extra": ^8.0.0 + debug: ^4.0.0 + fs-extra: ^9.0.0 + tslib: ^2.0.1 + checksum: 7cdd69c1aca348192edb588bc24b13491198d4c16428d3b3a176b2d6862a48e3dbb42cf3b677c3c36f3d9ceaf87739c0f633f2ac964092fefe58978863350f04 + languageName: node + linkType: hard + "@ionic/utils-fs@npm:3.1.7, @ionic/utils-fs@npm:^3.1.5, @ionic/utils-fs@npm:^3.1.6, @ionic/utils-fs@npm:^3.1.7": version: 3.1.7 resolution: "@ionic/utils-fs@npm:3.1.7" @@ -4788,6 +4811,16 @@ __metadata: languageName: node linkType: hard +"@ionic/utils-object@npm:2.1.5": + version: 2.1.5 + resolution: "@ionic/utils-object@npm:2.1.5" + dependencies: + debug: ^4.0.0 + tslib: ^2.0.1 + checksum: 123d1fe5aabe984bd5c93f7e70b3166e47007673218fd9347747c9005be0b10b0c639a8eaf36b77e12337d1cc90171624af06620bf9e9cccb2b8414ad80bc4a5 + languageName: node + linkType: hard + "@ionic/utils-object@npm:2.1.6": version: 2.1.6 resolution: "@ionic/utils-object@npm:2.1.6" @@ -4798,21 +4831,21 @@ __metadata: languageName: node linkType: hard -"@ionic/utils-process@npm:2.1.11": - version: 2.1.11 - resolution: "@ionic/utils-process@npm:2.1.11" +"@ionic/utils-process@npm:2.1.10": + version: 2.1.10 + resolution: "@ionic/utils-process@npm:2.1.10" dependencies: - "@ionic/utils-object": 2.1.6 - "@ionic/utils-terminal": 2.3.4 + "@ionic/utils-object": 2.1.5 + "@ionic/utils-terminal": 2.3.3 debug: ^4.0.0 signal-exit: ^3.0.3 tree-kill: ^1.2.2 tslib: ^2.0.1 - checksum: 376994e15774778af7b951c22d20c19510fa2009b5ff1c2e1244be6a0d2b059eba5e7b6db6ddd9e3764c326ab35e4446a17e49f2e1deab66b4eccd008f66cc49 + checksum: 4aa84bcdee08dae2ca0cce37e9109de1de43cba2cb4e4b2aa2324b6fc958ac751251efbf3883959d0d8b02f3ce3beff94f07bd20c4c895ccb270a10aa1360545 languageName: node linkType: hard -"@ionic/utils-process@npm:2.1.12": +"@ionic/utils-process@npm:2.1.12, @ionic/utils-process@npm:^2.1.11": version: 2.1.12 resolution: "@ionic/utils-process@npm:2.1.12" dependencies: @@ -4826,13 +4859,13 @@ __metadata: languageName: node linkType: hard -"@ionic/utils-stream@npm:3.1.6": - version: 3.1.6 - resolution: "@ionic/utils-stream@npm:3.1.6" +"@ionic/utils-stream@npm:3.1.5": + version: 3.1.5 + resolution: "@ionic/utils-stream@npm:3.1.5" dependencies: debug: ^4.0.0 tslib: ^2.0.1 - checksum: cd207a12fdcfa39c3f215620dee17491aca6bf0fa39cd9c7a9a21188013113aa3f3f9e50e2eae590f2dae9f5411e54a6f9cd3916cd87837be9206ea3fedd65f3 + checksum: 6211825c64295df1c368650b445c8cb1220417855aa6f0cdec68f4ccd3c5368b5f825911708a7242386e9546aa9050a5f85f9b1a0356c8dd9280d1dd33bcb33a languageName: node linkType: hard @@ -4846,41 +4879,41 @@ __metadata: languageName: node linkType: hard -"@ionic/utils-subprocess@npm:3.0.1": - version: 3.0.1 - resolution: "@ionic/utils-subprocess@npm:3.0.1" +"@ionic/utils-subprocess@npm:2.1.11": + version: 2.1.11 + resolution: "@ionic/utils-subprocess@npm:2.1.11" dependencies: - "@ionic/utils-array": 2.1.6 - "@ionic/utils-fs": 3.1.7 - "@ionic/utils-process": 2.1.12 - "@ionic/utils-stream": 3.1.7 - "@ionic/utils-terminal": 2.3.5 + "@ionic/utils-array": 2.1.5 + "@ionic/utils-fs": 3.1.6 + "@ionic/utils-process": 2.1.10 + "@ionic/utils-stream": 3.1.5 + "@ionic/utils-terminal": 2.3.3 cross-spawn: ^7.0.3 debug: ^4.0.0 tslib: ^2.0.1 - checksum: 24fee310d3293361a130cacdf2b3dde079f402cd099ce3b121d708e7bce7819a83353172c1c2400afd5cdcd0117c252586e3469d800a828ea8c08754ea5cb3e1 + checksum: f93be70bd164c1386bf4323ebdf6e8672bd0b677cee302d4952162229253639ec3eefd78b955afa752b2571f567e13e4635c23d51969e7d565cfd12b7a0b7df7 languageName: node linkType: hard -"@ionic/utils-subprocess@npm:^2.1.11": - version: 2.1.14 - resolution: "@ionic/utils-subprocess@npm:2.1.14" +"@ionic/utils-subprocess@npm:3.0.1": + version: 3.0.1 + resolution: "@ionic/utils-subprocess@npm:3.0.1" dependencies: "@ionic/utils-array": 2.1.6 "@ionic/utils-fs": 3.1.7 - "@ionic/utils-process": 2.1.11 - "@ionic/utils-stream": 3.1.6 - "@ionic/utils-terminal": 2.3.4 + "@ionic/utils-process": 2.1.12 + "@ionic/utils-stream": 3.1.7 + "@ionic/utils-terminal": 2.3.5 cross-spawn: ^7.0.3 debug: ^4.0.0 tslib: ^2.0.1 - checksum: 26959f40d6bf287f258063ede17da3fe392eb2817db81ef77593f3ea6ac59710d5b3bc437a1713624899c885514797bc1d9170083e31cd271035d2e2694598ea + checksum: 24fee310d3293361a130cacdf2b3dde079f402cd099ce3b121d708e7bce7819a83353172c1c2400afd5cdcd0117c252586e3469d800a828ea8c08754ea5cb3e1 languageName: node linkType: hard -"@ionic/utils-terminal@npm:2.3.4": - version: 2.3.4 - resolution: "@ionic/utils-terminal@npm:2.3.4" +"@ionic/utils-terminal@npm:2.3.3": + version: 2.3.3 + resolution: "@ionic/utils-terminal@npm:2.3.3" dependencies: "@types/slice-ansi": ^4.0.0 debug: ^4.0.0 @@ -4891,7 +4924,7 @@ __metadata: tslib: ^2.0.1 untildify: ^4.0.0 wrap-ansi: ^7.0.0 - checksum: d32fbeb6c7b355717a28ea2b0741c50c2fee5f959c25373f17887f6d8150523bffc54caaa1cd8c585809f94bdcbfd7f13ade63d02a9f122e93ff7d4ca1645698 + checksum: c551a2c8c094405c1a636638a1e1f2cbac0afe29e9a20c726d7571f20aec5e177cf7ed38c785735808c9bccf7e4f71bd04ee291865dc6f70ac009074fa064974 languageName: node linkType: hard @@ -14802,7 +14835,7 @@ __metadata: "@capacitor-firebase/performance": ^5.3.0 "@capacitor/android": ^5.5.1 "@capacitor/app": ^5.0.6 - "@capacitor/cli": ^5.5.1 + "@capacitor/cli": ^6.1.2 "@capacitor/clipboard": ^5.0.6 "@capacitor/core": ^5.5.1 "@capacitor/device": ^5.0.6 From 04c09a42155466d9f4fc34c2d34e09b4bb6195f5 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 14:39:26 +0100 Subject: [PATCH 073/204] chore: Capacitor 6 migration - updates after running yarn install --- package.json | 2 +- yarn.lock | 134 +++++++++++++++++++++++++-------------------------- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index 2b1458cce..4ff01e51c 100644 --- a/package.json +++ b/package.json @@ -181,4 +181,4 @@ "node": ">=18.x" }, "packageManager": "yarn@3.3.1" -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index a1047aafa..72b75027e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2086,25 +2086,25 @@ __metadata: languageName: node linkType: hard -"@capacitor/android@npm:^5.5.1": - version: 5.7.0 - resolution: "@capacitor/android@npm:5.7.0" +"@capacitor/android@npm:^6.0.0": + version: 6.1.2 + resolution: "@capacitor/android@npm:6.1.2" peerDependencies: - "@capacitor/core": ^5.7.0 - checksum: 94d1266dba7c23a297edfb597347c9ad77dca6e812024ac2a2898590423dc6fc4b71b52f7e3d5a3967a75fcbc33d3712625b58905b624a95dc6d9fca5c733c98 + "@capacitor/core": ^6.1.0 + checksum: 5738cd4777a992b09a2d791c0e90f3933e27cf22a0c5793ac60d34ba4541585c22d602f9252340b2df02eb4a004ec0d4043d1cb86a51825b18169109a27ca984 languageName: node linkType: hard -"@capacitor/app@npm:^5.0.6": - version: 5.0.7 - resolution: "@capacitor/app@npm:5.0.7" +"@capacitor/app@npm:^6.0.0": + version: 6.0.1 + resolution: "@capacitor/app@npm:6.0.1" peerDependencies: - "@capacitor/core": ^5.0.0 - checksum: 29a2615f3c11f8a4e060179418dab5b799207e7c516317853e3505327799d5ad8e8db2b76eaba3363578e417b5f0200dd99375bfaabf0b57420688299b501235 + "@capacitor/core": ^6.0.0 + checksum: 3fa08f10421de609e900f8f95fadb674afb54cf7670a7b925b1b98c4c89f9a0676e31da3916347a098347cec48538b1297165ab0bb3525b6acc96c126475591a languageName: node linkType: hard -"@capacitor/cli@npm:^6.1.2": +"@capacitor/cli@npm:^6.0.0": version: 6.1.2 resolution: "@capacitor/cli@npm:6.1.2" dependencies: @@ -2133,84 +2133,84 @@ __metadata: languageName: node linkType: hard -"@capacitor/clipboard@npm:^5.0.6": - version: 5.0.7 - resolution: "@capacitor/clipboard@npm:5.0.7" +"@capacitor/clipboard@npm:^6.0.0": + version: 6.0.1 + resolution: "@capacitor/clipboard@npm:6.0.1" peerDependencies: - "@capacitor/core": ^5.0.0 - checksum: e56e0fcd6d5bd82f978763843809b1bafa66976223e8b69b9eaa00eed666f100c2873db94aa8e1a84ecf04f050a8ddb44c9299dbc7ced18635cd005af643997e + "@capacitor/core": ^6.0.0 + checksum: 10c33561676bf24fc189527370acc2a46232ae176fbe1375324340c96ac1c287b2615cb45d76f2c91e3173adc4b94936749fe54ef58c89d586d419623dd542da languageName: node linkType: hard -"@capacitor/core@npm:^5.5.1": - version: 5.7.0 - resolution: "@capacitor/core@npm:5.7.0" +"@capacitor/core@npm:^6.0.0": + version: 6.1.2 + resolution: "@capacitor/core@npm:6.1.2" dependencies: tslib: ^2.1.0 - checksum: 3c7a0ed4bfd3942c333f141adfa6d356d2cf26adef82d5a3a0c73f19137d8285f270f8146bc232ea75705714dac19c9cf9968ba32ac33f6c3fbee89a0e5d9227 + checksum: 51e5f575c4d96290902c6421fd6c4493e4e865cc95cfd1a207bf80956945cc06e724beb3fc9bfb21f44b0a3559c6a0064137b9dcf2b03b2550bb496c7043dc4e languageName: node linkType: hard -"@capacitor/device@npm:^5.0.6": - version: 5.0.7 - resolution: "@capacitor/device@npm:5.0.7" +"@capacitor/device@npm:^6.0.0": + version: 6.0.1 + resolution: "@capacitor/device@npm:6.0.1" peerDependencies: - "@capacitor/core": ^5.0.0 - checksum: 5e5d5caa394b9c986b2e0f4f4d85cec6a814e7008e3b6ea410883857b86ecd9b146f2996266d6a1086582bc9471941620f33f4b6dc4683f5b53dc30253b0fcf0 + "@capacitor/core": ^6.0.0 + checksum: 65431aba87ca7b5a405897eebea33a27920b7efc19160a0a4d0b0a16fa77590613369c65e5abce904a1c027bcc4280d3cd71535dbcc4ae72c86d3fcb4868dcc5 languageName: node linkType: hard -"@capacitor/filesystem@npm:^5.1.4": - version: 5.2.1 - resolution: "@capacitor/filesystem@npm:5.2.1" +"@capacitor/filesystem@npm:^6.0.0": + version: 6.0.1 + resolution: "@capacitor/filesystem@npm:6.0.1" peerDependencies: - "@capacitor/core": ^5.1.1 - checksum: d043feaf0a9608e15b307f06421603f20c865fa6ff2108d1ae4efa486e9b9979215e8f4f036109a2d12dd1dfb76b0cd4bf4f9c471f375480f03c55f9dc01acd4 + "@capacitor/core": ^6.0.0 + checksum: 92e4caa6c66c35a244585002318a6945927bfe6894b4b1b36e16363001301698dba39cfd2e5148807c65b60f9334d69c155b2b48a33ef5edf843c41cf23d509a languageName: node linkType: hard -"@capacitor/ios@npm:^5.7.2": - version: 5.7.2 - resolution: "@capacitor/ios@npm:5.7.2" +"@capacitor/ios@npm:^6.0.0": + version: 6.1.2 + resolution: "@capacitor/ios@npm:6.1.2" peerDependencies: - "@capacitor/core": ^5.7.0 - checksum: 63d54bc5e44da159730928ad352dff2f9f13fbef132c8ba151b26d4c460fbca9fae32e8be3022d99409d9f3a8ab654637a837e961b092c73efa1e55b3501c9f1 + "@capacitor/core": ^6.1.0 + checksum: 452ff6149ca573c29f90fd9be3add3e4e87d32cedecc6e5d9bc79704330c35b10fc161eecfcf00922c80c15c7ddf090abf03fd322c7d376e6529882b00a9212d languageName: node linkType: hard -"@capacitor/local-notifications@npm:^5.0.6": - version: 5.0.7 - resolution: "@capacitor/local-notifications@npm:5.0.7" +"@capacitor/local-notifications@npm:^6.0.0": + version: 6.1.0 + resolution: "@capacitor/local-notifications@npm:6.1.0" peerDependencies: - "@capacitor/core": ^5.0.0 - checksum: ed68c6b824c8f90b7819a1a9d61897dea0f828957582be3725e9d3c7969e2f23fe62d8a2904c119f1558cb43d6c288604f4d2bdf18656de3fc49e872d778d59e + "@capacitor/core": ^6.0.0 + checksum: 34ea1de959f8362c4d7d42a7621bb2e1df1f6c41054a643ee148303dd406d6d8c9395318e358e7b717b42b505fe1e9abfb45828a85c42d644334ffcf532f2bba languageName: node linkType: hard -"@capacitor/push-notifications@npm:^5.1.0": - version: 5.1.1 - resolution: "@capacitor/push-notifications@npm:5.1.1" +"@capacitor/push-notifications@npm:^6.0.0": + version: 6.0.2 + resolution: "@capacitor/push-notifications@npm:6.0.2" peerDependencies: - "@capacitor/core": ^5.0.0 - checksum: 3f817e9a1a3f2b81e108c405d0c960c7da53a2dd03697e99b9b314667412fb0d690d6a1aa8b484a8a7b15f5eb24ff285e7e8f2f6b2186e86e3a267fd70124f41 + "@capacitor/core": ^6.0.0 + checksum: 293aa7180eb6ff182902ab4655e6cbf10d78788b80ee079a6f9d2011b1f5fa9c66620896ef54ee98ab34621df29b75acbd6c1b20d1153d68527970f2c486d592 languageName: node linkType: hard -"@capacitor/share@npm:^5.0.6": - version: 5.0.7 - resolution: "@capacitor/share@npm:5.0.7" +"@capacitor/share@npm:^6.0.0": + version: 6.0.2 + resolution: "@capacitor/share@npm:6.0.2" peerDependencies: - "@capacitor/core": ^5.0.0 - checksum: a0e2633e154e4edcbe4b0581838e3c17c21e83850efff4f91ea37d833856352eb6843831364196a921bf9ad90b3d12886410b72dbc0d1be34e693abf21161122 + "@capacitor/core": ^6.0.0 + checksum: edc0c665ee751a597a399587f0298a5190ec568f8ae776903fdfce9e50e46318c9a1e9a5b05db57e67c8a63dad0194e44e3f60e445bbe8f84e3ed78793203492 languageName: node linkType: hard -"@capacitor/splash-screen@npm:^5.0.6": - version: 5.0.7 - resolution: "@capacitor/splash-screen@npm:5.0.7" +"@capacitor/splash-screen@npm:^6.0.0": + version: 6.0.2 + resolution: "@capacitor/splash-screen@npm:6.0.2" peerDependencies: - "@capacitor/core": ^5.0.0 - checksum: e8062bfbe5e86221b29c60644587094f050428208dae431bcda08d5f171f42b0cc724e2b548ecff08c45239ea400baf3ef592ae012ac615c398ffc9c8e2d7f63 + "@capacitor/core": ^6.0.0 + checksum: 9093c476084681d2b60d9a9a740ceaad6c9147b7b68bc08def9da6b56ff5a0aee28f8216c7f6d1a1aeb1f378e5f1cee44521d3517a8df928d3bde1c5d943dc05 languageName: node linkType: hard @@ -14833,18 +14833,18 @@ __metadata: "@capacitor-firebase/authentication": ^5.3.0 "@capacitor-firebase/crashlytics": ^5.4.1 "@capacitor-firebase/performance": ^5.3.0 - "@capacitor/android": ^5.5.1 - "@capacitor/app": ^5.0.6 - "@capacitor/cli": ^6.1.2 - "@capacitor/clipboard": ^5.0.6 - "@capacitor/core": ^5.5.1 - "@capacitor/device": ^5.0.6 - "@capacitor/filesystem": ^5.1.4 - "@capacitor/ios": ^5.7.2 - "@capacitor/local-notifications": ^5.0.6 - "@capacitor/push-notifications": ^5.1.0 - "@capacitor/share": ^5.0.6 - "@capacitor/splash-screen": ^5.0.6 + "@capacitor/android": ^6.0.0 + "@capacitor/app": ^6.0.0 + "@capacitor/cli": ^6.0.0 + "@capacitor/clipboard": ^6.0.0 + "@capacitor/core": ^6.0.0 + "@capacitor/device": ^6.0.0 + "@capacitor/filesystem": ^6.0.0 + "@capacitor/ios": ^6.0.0 + "@capacitor/local-notifications": ^6.0.0 + "@capacitor/push-notifications": ^6.0.0 + "@capacitor/share": ^6.0.0 + "@capacitor/splash-screen": ^6.0.0 "@capawesome/capacitor-app-update": ^5.0.1 "@compodoc/compodoc": ^1.1.23 "@ionic-native/core": ^5.36.0 From 349cf2838fce3dc051633f1e380103fb90da9cde Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 14:40:16 +0100 Subject: [PATCH 074/204] chore: Capacitor 6 migration - updates after running npx cap sync ios --- ios/App/Podfile.lock | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 6c81f443d..f5dfbe739 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -1,19 +1,19 @@ PODS: - - Capacitor (5.7.2): + - Capacitor (6.1.2): - CapacitorCordova - - CapacitorApp (5.0.7): + - CapacitorApp (6.0.1): - Capacitor - CapacitorBlobWriter (0.0.1): - Capacitor - GCDWebServer (~> 3.0) - - CapacitorClipboard (5.0.7): + - CapacitorClipboard (6.0.1): - Capacitor - CapacitorCommunityFileOpener (1.0.5): - Capacitor - - CapacitorCordova (5.7.2) - - CapacitorDevice (5.0.7): + - CapacitorCordova (6.1.2) + - CapacitorDevice (6.0.1): - Capacitor - - CapacitorFilesystem (5.2.1): + - CapacitorFilesystem (6.0.1): - Capacitor - CapacitorFirebaseAuthentication (5.4.0): - Capacitor @@ -28,13 +28,13 @@ PODS: - CapacitorFirebasePerformance (5.4.0): - Capacitor - FirebasePerformance (~> 10.8) - - CapacitorLocalNotifications (5.0.7): + - CapacitorLocalNotifications (6.1.0): - Capacitor - - CapacitorPushNotifications (5.1.1): + - CapacitorPushNotifications (6.0.2): - Capacitor - - CapacitorShare (5.0.7): + - CapacitorShare (6.0.2): - Capacitor - - CapacitorSplashScreen (5.0.7): + - CapacitorSplashScreen (6.0.2): - Capacitor - CapawesomeCapacitorAppUpdate (5.1.0): - Capacitor @@ -220,21 +220,21 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capawesome/capacitor-app-update" SPEC CHECKSUMS: - Capacitor: fc7ef6d935eafb0df9eaaf109ca69be16c51a2d2 - CapacitorApp: 17fecd0e6cb23feafac7eb0939417389038b0979 + Capacitor: 679f9673fdf30597493a6362a5d5bf233d46abc2 + CapacitorApp: 0bc633b4eae40a1f32cd2834788fad3bc42da6a1 CapacitorBlobWriter: 110eeaf80611f19bf01a8a05ff3672149ed0baad - CapacitorClipboard: 45e5e25f2271f98712985d422776cdc5a779cca1 + CapacitorClipboard: 756cd7e83e8d5d19b0c74f40b57517c287bd5fe2 CapacitorCommunityFileOpener: 8ae87db61961a6166cb929cc16e3cc719ea58da1 - CapacitorCordova: 70b13b8fddb6f35d8adcfe06cb5045c07f35f6de - CapacitorDevice: fc91bdb484dc0e70755e9b621cd557afe642613a - CapacitorFilesystem: 9f3e3c7fea2fff12f46dd5b07a2914f2103e4cfc + CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd + CapacitorDevice: 7097a1deb4224b77fd13a6e60a355d0062a5d772 + CapacitorFilesystem: 37fb3aa5c945b4539ab11c74a5c57925a302bf24 CapacitorFirebaseAuthentication: 64eaf9727b8f29f84069595ddc239c8e5365f49a CapacitorFirebaseCrashlytics: 643d7e63836ae9608e10b9c3e40dfbae8679589e CapacitorFirebasePerformance: 964b215796522c83767a78ad629b9581558b391a - CapacitorLocalNotifications: c58afadd159f6bc540ef9b3cbdbc82510a2bf112 - CapacitorPushNotifications: 2327900bc002f5ff49ee6d2231796d0635b4a1b0 - CapacitorShare: c6a1ebbf0114ff9e863b966cd6052678fa25d480 - CapacitorSplashScreen: dd3de3f3644710fa2a697cfb91ec262eece4d242 + CapacitorLocalNotifications: 6bac9e948b2b8852506c6d74abb2cde140250f86 + CapacitorPushNotifications: ccd797926c030acad3d5498ef452c735c90a2c89 + CapacitorShare: 591ae4693d85686ceb590db8e8b44aa014ec6490 + CapacitorSplashScreen: 250df9ef8014fac5c7c1fd231f0f8b1d8f0b5624 CapawesomeCapacitorAppUpdate: 04f2c4b7942ccc72a86bcd553ab3c546df422838 FirebaseABTesting: 66d2594b36d4ff6e7d3c8719802100990de05857 FirebaseAppCheckInterop: 58db3e9494751399cf3e7b7e3e705cff71099153 From 0bf8b609d5af6e7e87d5b32fc5632d87aaaafb72 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 14:55:18 +0100 Subject: [PATCH 075/204] chore: cap 6 migration - gradle upgrade (via Android Studio's AGP Upgrade Assistant) --- android/build.gradle | 2 +- android/gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 0bb5d5205..aa2d7edaf 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.1' + classpath 'com.android.tools.build:gradle:8.3.1' classpath 'com.google.gms:google-services:4.4.0' // NOTE: Do not place your application dependencies here; they belong diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 9b0a13f0f..309b4e18d 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 6dfc84edc27d7b14e3c9710b748f14552f2e1f2f Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 15:09:09 +0100 Subject: [PATCH 076/204] chore: upgrade @ionic/angular-toolkit --- package.json | 2 +- yarn.lock | 118 +++++++++++++++++++++++++-------------------------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 4ff01e51c..2b75c1088 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "@angular/language-service": "~17.2.2", "@capacitor/cli": "^6.0.0", "@compodoc/compodoc": "^1.1.23", - "@ionic/angular-toolkit": "^10.0.0", + "@ionic/angular-toolkit": "^11.0.1", "@ionic/cli": "^7.1.5", "@schematics/angular": "~17.0.3", "@swc/helpers": "^0.5.1", diff --git a/yarn.lock b/yarn.lock index 72b75027e..54a8be34d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -200,14 +200,14 @@ __metadata: languageName: node linkType: hard -"@angular-devkit/core@npm:16.2.12, @angular-devkit/core@npm:^16.0.0": - version: 16.2.12 - resolution: "@angular-devkit/core@npm:16.2.12" +"@angular-devkit/core@npm:17.0.10": + version: 17.0.10 + resolution: "@angular-devkit/core@npm:17.0.10" dependencies: ajv: 8.12.0 ajv-formats: 2.1.1 jsonc-parser: 3.2.0 - picomatch: 2.3.1 + picomatch: 3.0.1 rxjs: 7.8.1 source-map: 0.7.4 peerDependencies: @@ -215,18 +215,18 @@ __metadata: peerDependenciesMeta: chokidar: optional: true - checksum: 9ffde5156bfa90cbd76f6f707afab8700916b68cf70c3f27db9df2a70c7193e6f92c2cc6b89a536c557b68977d677c8fdedf7065b2fffa5abe9d5b6ef67acb19 + checksum: 909c113dc0bfe1c2ff74509089bce3ee508eba8f3948011b3ef3f7f9903add48b09edb3c2da48fe502eb50377cd7c1a1b6507bb89d2ed7d8e8b5d1d12353442e languageName: node linkType: hard -"@angular-devkit/core@npm:17.0.10": - version: 17.0.10 - resolution: "@angular-devkit/core@npm:17.0.10" +"@angular-devkit/core@npm:17.2.1, @angular-devkit/core@npm:~17.2.1": + version: 17.2.1 + resolution: "@angular-devkit/core@npm:17.2.1" dependencies: ajv: 8.12.0 ajv-formats: 2.1.1 - jsonc-parser: 3.2.0 - picomatch: 3.0.1 + jsonc-parser: 3.2.1 + picomatch: 4.0.1 rxjs: 7.8.1 source-map: 0.7.4 peerDependencies: @@ -234,13 +234,13 @@ __metadata: peerDependenciesMeta: chokidar: optional: true - checksum: 909c113dc0bfe1c2ff74509089bce3ee508eba8f3948011b3ef3f7f9903add48b09edb3c2da48fe502eb50377cd7c1a1b6507bb89d2ed7d8e8b5d1d12353442e + checksum: 2ac5852d8d7cb3ae809c70df3c2a1a8cc6ad3ee20246091e5ed2aa0871ee1ec3871f77eb7b10300af5781f56cd667f88ff737d12db34561a0f59640afa92024e languageName: node linkType: hard -"@angular-devkit/core@npm:17.2.1, @angular-devkit/core@npm:~17.2.1": - version: 17.2.1 - resolution: "@angular-devkit/core@npm:17.2.1" +"@angular-devkit/core@npm:17.3.8, @angular-devkit/core@npm:^17.0.0": + version: 17.3.8 + resolution: "@angular-devkit/core@npm:17.3.8" dependencies: ajv: 8.12.0 ajv-formats: 2.1.1 @@ -253,7 +253,7 @@ __metadata: peerDependenciesMeta: chokidar: optional: true - checksum: 2ac5852d8d7cb3ae809c70df3c2a1a8cc6ad3ee20246091e5ed2aa0871ee1ec3871f77eb7b10300af5781f56cd667f88ff737d12db34561a0f59640afa92024e + checksum: c6d41c56fcfa560f592c0fa8ec30addb50e77bf3be543ad3bee2ed01b7932457156d5ca72d008678a83101a3dcd125c44f2d45063c8685e6e6c914e925b69c26 languageName: node linkType: hard @@ -299,19 +299,6 @@ __metadata: languageName: node linkType: hard -"@angular-devkit/schematics@npm:16.2.12, @angular-devkit/schematics@npm:^16.0.0": - version: 16.2.12 - resolution: "@angular-devkit/schematics@npm:16.2.12" - dependencies: - "@angular-devkit/core": 16.2.12 - jsonc-parser: 3.2.0 - magic-string: 0.30.1 - ora: 5.4.1 - rxjs: 7.8.1 - checksum: 475ce9b5d0a95622a0e3541b719cbfcea2a4ba9cf2b92dbcf799626b0e4548384fbe9a66bc95d08bc529ae649dbec0cf0a93779c1a3b47d6b9cce50fc322eb46 - languageName: node - linkType: hard - "@angular-devkit/schematics@npm:17.0.10": version: 17.0.10 resolution: "@angular-devkit/schematics@npm:17.0.10" @@ -338,6 +325,19 @@ __metadata: languageName: node linkType: hard +"@angular-devkit/schematics@npm:17.3.8, @angular-devkit/schematics@npm:^17.0.0": + version: 17.3.8 + resolution: "@angular-devkit/schematics@npm:17.3.8" + dependencies: + "@angular-devkit/core": 17.3.8 + jsonc-parser: 3.2.1 + magic-string: 0.30.8 + ora: 5.4.1 + rxjs: 7.8.1 + checksum: a7e2aedb0970a8a243924b122ae030c33dfd5cb9acd818ff7cb3be132b73f048448003152fe1898bd34926580d4f293e9ec8597a9fc45c965460642012489235 + languageName: node + linkType: hard + "@angular-eslint/builder@npm:17.2.1": version: 17.2.1 resolution: "@angular-eslint/builder@npm:17.2.1" @@ -4628,14 +4628,14 @@ __metadata: languageName: node linkType: hard -"@ionic/angular-toolkit@npm:^10.0.0": - version: 10.1.1 - resolution: "@ionic/angular-toolkit@npm:10.1.1" +"@ionic/angular-toolkit@npm:^11.0.1": + version: 11.0.1 + resolution: "@ionic/angular-toolkit@npm:11.0.1" dependencies: - "@angular-devkit/core": ^16.0.0 - "@angular-devkit/schematics": ^16.0.0 - "@schematics/angular": ^16.0.0 - checksum: 555d741d7a4819759adf7f330480914dee312a513e6b92f758763b90c3701f97fcafcc6da9136cffd1ded62ae09f0e18da9103d14ac1ecef693ee4a7e733fc4f + "@angular-devkit/core": ^17.0.0 + "@angular-devkit/schematics": ^17.0.0 + "@schematics/angular": ^17.0.0 + checksum: 5d72932b9a60cff71cdf2058ca3ecc35ad505abed095e52e8144e05ffa8b90643c2dcbbf61e0f18c340b7e6cbc3d917d2eb3db264452949e5063fa6bc78eddd7 languageName: node linkType: hard @@ -6349,14 +6349,14 @@ __metadata: languageName: node linkType: hard -"@schematics/angular@npm:^16.0.0": - version: 16.2.12 - resolution: "@schematics/angular@npm:16.2.12" +"@schematics/angular@npm:^17.0.0": + version: 17.3.8 + resolution: "@schematics/angular@npm:17.3.8" dependencies: - "@angular-devkit/core": 16.2.12 - "@angular-devkit/schematics": 16.2.12 - jsonc-parser: 3.2.0 - checksum: 905285d66df42a660e37d88f26c35a962988d6489c46209ce1c47064cd740b700161fc68588447a1fa5552015ea37c0771ff552795f1bf51aec6bc94860d6beb + "@angular-devkit/core": 17.3.8 + "@angular-devkit/schematics": 17.3.8 + jsonc-parser: 3.2.1 + checksum: f3fdad7569d2b4c119e1e7d725f8e0701476006a33d141ee6d7fd1781f09b69e80b22486513280c0cd6d5e901a201e4bfa9efb15c6566f135e5e57f6fc3d2512 languageName: node linkType: hard @@ -14853,7 +14853,7 @@ __metadata: "@ionic-native/media": ^5.36.0 "@ionic-native/status-bar": ^5.36.0 "@ionic/angular": ^7.7.3 - "@ionic/angular-toolkit": ^10.0.0 + "@ionic/angular-toolkit": ^11.0.1 "@ionic/cli": ^7.1.5 "@ionic/pwa-elements": ^3.2.2 "@schematics/angular": ~17.0.3 @@ -19179,15 +19179,6 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:0.30.1": - version: 0.30.1 - resolution: "magic-string@npm:0.30.1" - dependencies: - "@jridgewell/sourcemap-codec": ^1.4.15 - checksum: 7bc7e4493e32a77068f3753bf8652d4ab44142122eb7fb9fa871af83bef2cd2c57518a6769701cd5d0379bd624a13bc8c72ca25ac5655b27e5a61adf1fd38db2 - languageName: node - linkType: hard - "magic-string@npm:0.30.5": version: 0.30.5 resolution: "magic-string@npm:0.30.5" @@ -19206,6 +19197,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:0.30.8": + version: 0.30.8 + resolution: "magic-string@npm:0.30.8" + dependencies: + "@jridgewell/sourcemap-codec": ^1.4.15 + checksum: 79922f4500d3932bb587a04440d98d040170decf432edc0f91c0bf8d41db16d364189bf800e334170ac740918feda62cd39dcc170c337dc18050cfcf00a5f232 + languageName: node + linkType: hard + "make-dir@npm:^2.1.0": version: 2.1.0 resolution: "make-dir@npm:2.1.0" @@ -21701,13 +21701,6 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:2.3.1, picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": - version: 2.3.1 - resolution: "picomatch@npm:2.3.1" - checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf - languageName: node - linkType: hard - "picomatch@npm:3.0.1": version: 3.0.1 resolution: "picomatch@npm:3.0.1" @@ -21722,6 +21715,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf + languageName: node + linkType: hard + "pidtree@npm:0.6.0": version: 0.6.0 resolution: "pidtree@npm:0.6.0" From 952c579c17cbb03b30127198fce9a0b9305b2862 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 15:17:36 +0100 Subject: [PATCH 077/204] upgrade @capawesome/capacitor-app-update --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 2b75c1088..c66c7fa8a 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@capacitor/push-notifications": "^6.0.0", "@capacitor/share": "^6.0.0", "@capacitor/splash-screen": "^6.0.0", - "@capawesome/capacitor-app-update": "^5.0.1", + "@capawesome/capacitor-app-update": "^6.0.0", "@ionic-native/core": "^5.36.0", "@ionic-native/device": "^5.36.0", "@ionic-native/http": "^5.36.0", diff --git a/yarn.lock b/yarn.lock index 54a8be34d..d0279628e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2214,12 +2214,12 @@ __metadata: languageName: node linkType: hard -"@capawesome/capacitor-app-update@npm:^5.0.1": - version: 5.1.0 - resolution: "@capawesome/capacitor-app-update@npm:5.1.0" +"@capawesome/capacitor-app-update@npm:^6.0.0": + version: 6.0.0 + resolution: "@capawesome/capacitor-app-update@npm:6.0.0" peerDependencies: - "@capacitor/core": ^5.0.0 - checksum: f2e829492e0f65bef77b9e8901e1927c07656d6834ce3b59d68236a89abc01c9846c1b0cf6df271d6fd1b713ef06552f73f582f823b00a387c2c7b8a84151118 + "@capacitor/core": ^6.0.0 + checksum: 8fe893f8fcb953b2520e38e395502820d3f633d3a77159584890631393c5beb42462d78a1139c7aa84a46c5316589ca2b1cf4d19222583a9e589416d7df848db languageName: node linkType: hard @@ -14845,7 +14845,7 @@ __metadata: "@capacitor/push-notifications": ^6.0.0 "@capacitor/share": ^6.0.0 "@capacitor/splash-screen": ^6.0.0 - "@capawesome/capacitor-app-update": ^5.0.1 + "@capawesome/capacitor-app-update": ^6.0.0 "@compodoc/compodoc": ^1.1.23 "@ionic-native/core": ^5.36.0 "@ionic-native/device": ^5.36.0 From e5ecb4a508b1a07c652b6be7161d44227dd8c229 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 15:19:56 +0100 Subject: [PATCH 078/204] chore: upgrade @capacitor-community/file-opener --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index c66c7fa8a..4c94cb9bc 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@angular/platform-browser": "~17.2.2", "@angular/platform-browser-dynamic": "~17.2.2", "@angular/router": "~17.2.2", - "@capacitor-community/file-opener": "^1.0.5", + "@capacitor-community/file-opener": "^6.0.0", "@capacitor-firebase/authentication": "^5.3.0", "@capacitor-firebase/crashlytics": "^5.4.1", "@capacitor-firebase/performance": "^5.3.0", diff --git a/yarn.lock b/yarn.lock index d0279628e..a3c8765b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2039,12 +2039,12 @@ __metadata: languageName: node linkType: hard -"@capacitor-community/file-opener@npm:^1.0.5": - version: 1.0.5 - resolution: "@capacitor-community/file-opener@npm:1.0.5" +"@capacitor-community/file-opener@npm:^6.0.0": + version: 6.0.0 + resolution: "@capacitor-community/file-opener@npm:6.0.0" peerDependencies: - "@capacitor/core": ^3.0.0 || ^4.0.0 || ^5.0.0 - checksum: c98847e7f083df313911ad85eedbaf07869ea09d88acb6eff738f5c162b614b12683adaa4571d71b1dd808d29e1ec9e4365372482c8a7d16e21bbd3f6b63a306 + "@capacitor/core": ^6.0.0 + checksum: 008200294bf280bdc10a1fea0d883b0846eb0c5dd336818e4cdd40950a153d45610362327db194b516ac8ffc917c1c061103edaef3b00ce012cdb1c8b8fe1d54 languageName: node linkType: hard @@ -14829,7 +14829,7 @@ __metadata: "@angular/platform-browser": ~17.2.2 "@angular/platform-browser-dynamic": ~17.2.2 "@angular/router": ~17.2.2 - "@capacitor-community/file-opener": ^1.0.5 + "@capacitor-community/file-opener": ^6.0.0 "@capacitor-firebase/authentication": ^5.3.0 "@capacitor-firebase/crashlytics": ^5.4.1 "@capacitor-firebase/performance": ^5.3.0 From 776a4656413b803602db4ee8b583578c169a8072 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 15:21:21 +0100 Subject: [PATCH 079/204] chore: upgrade @capacitor-firebase plugins --- package.json | 6 +++--- yarn.lock | 40 ++++++++++++++++++++-------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 4c94cb9bc..33131d231 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,9 @@ "@angular/platform-browser-dynamic": "~17.2.2", "@angular/router": "~17.2.2", "@capacitor-community/file-opener": "^6.0.0", - "@capacitor-firebase/authentication": "^5.3.0", - "@capacitor-firebase/crashlytics": "^5.4.1", - "@capacitor-firebase/performance": "^5.3.0", + "@capacitor-firebase/authentication": "^6.1.0", + "@capacitor-firebase/crashlytics": "^6.1.0", + "@capacitor-firebase/performance": "^6.1.0", "@capacitor/android": "^6.0.0", "@capacitor/app": "^6.0.0", "@capacitor/clipboard": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index a3c8765b4..8b6c2de56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2048,41 +2048,41 @@ __metadata: languageName: node linkType: hard -"@capacitor-firebase/authentication@npm:^5.3.0": - version: 5.4.0 - resolution: "@capacitor-firebase/authentication@npm:5.4.0" +"@capacitor-firebase/authentication@npm:^6.1.0": + version: 6.1.0 + resolution: "@capacitor-firebase/authentication@npm:6.1.0" peerDependencies: - "@capacitor/core": ^5.0.0 - firebase: ^9.0.0 || ^10.0.0 + "@capacitor/core": ^6.0.0 + firebase: ^10.9.0 peerDependenciesMeta: firebase: optional: true - checksum: 4eebfa95392d76c2e7a24ab815c7be2b4a1c7a16a7f8f67a1d72db0ccf4f3eab92965aa3f2811bbaacf15ca42c5e05d3b5a78c5deb383accaa86324b104a0482 + checksum: c70c82576c46333d8d56c03dbe51ceab798cddd7974dbc2aa0f6e287059deea245d17be6343302096a09bd91d8e13b0bf8d6e8417b65ecb86e62573674492df0 languageName: node linkType: hard -"@capacitor-firebase/crashlytics@npm:^5.4.1": - version: 5.4.1 - resolution: "@capacitor-firebase/crashlytics@npm:5.4.1" +"@capacitor-firebase/crashlytics@npm:^6.1.0": + version: 6.1.0 + resolution: "@capacitor-firebase/crashlytics@npm:6.1.0" peerDependencies: - "@capacitor/core": ^5.0.0 + "@capacitor/core": ^6.0.0 peerDependenciesMeta: firebase: optional: true - checksum: d7b4d6b75e693653931e5a0ace47a546cd2b8c2122acbc066dc04ffacce0e6a5e1a1569610305f7dc2cb877218449f8d2c856ab41f15a797bc89ec734eaf64e6 + checksum: 9d20b204545e7bb6fed9b858270342634bd8772c84b516ba467d1a6b13a870edcdc1048def10c50f2edebd7fd0c896a2f7728176594cda670d807ed0b5e0f045 languageName: node linkType: hard -"@capacitor-firebase/performance@npm:^5.3.0": - version: 5.4.0 - resolution: "@capacitor-firebase/performance@npm:5.4.0" +"@capacitor-firebase/performance@npm:^6.1.0": + version: 6.1.0 + resolution: "@capacitor-firebase/performance@npm:6.1.0" peerDependencies: - "@capacitor/core": ^5.0.0 - firebase: ^9.0.0 || ^10.0.0 + "@capacitor/core": ^6.0.0 + firebase: ^10.9.0 peerDependenciesMeta: firebase: optional: true - checksum: a3af39624d7bee054a00ff937d636a16db8d6974a35912ac014d4e73f743119f8a57429896ae6ee946ee7346b279a57ca122365f0406353eb2f22f3dd4fcfe95 + checksum: 8aa094e6cece67ed8101cc0d78b2c9a0f3aa9f027de765095963ad95604be4bda70155573a8b8ef190ae571c878ae3fe3f63c688a56268f79840faddd16acaac languageName: node linkType: hard @@ -14830,9 +14830,9 @@ __metadata: "@angular/platform-browser-dynamic": ~17.2.2 "@angular/router": ~17.2.2 "@capacitor-community/file-opener": ^6.0.0 - "@capacitor-firebase/authentication": ^5.3.0 - "@capacitor-firebase/crashlytics": ^5.4.1 - "@capacitor-firebase/performance": ^5.3.0 + "@capacitor-firebase/authentication": ^6.1.0 + "@capacitor-firebase/crashlytics": ^6.1.0 + "@capacitor-firebase/performance": ^6.1.0 "@capacitor/android": ^6.0.0 "@capacitor/app": ^6.0.0 "@capacitor/cli": ^6.0.0 From 9591be2060299d7353deb2d61f377d79b2a47277 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 23 Aug 2024 15:22:14 +0100 Subject: [PATCH 080/204] chore: upgrade capacitor-blob-writer --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 33131d231..33d320d5a 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@supabase/supabase-js": "^2.39.0", "@types/file-saver": "^2.0.7", "bootstrap-datepicker": "^1.10.0", - "capacitor-blob-writer": "^1.1.14", + "capacitor-blob-writer": "^1.1.17", "clone": "^2.1.2", "core-js": "^3.33.3", "data-models": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 8b6c2de56..56e0463e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10269,13 +10269,13 @@ __metadata: languageName: node linkType: hard -"capacitor-blob-writer@npm:^1.1.14": - version: 1.1.14 - resolution: "capacitor-blob-writer@npm:1.1.14" +"capacitor-blob-writer@npm:^1.1.17": + version: 1.1.17 + resolution: "capacitor-blob-writer@npm:1.1.17" peerDependencies: "@capacitor/core": ">=3.0.0" "@capacitor/filesystem": ">=1.0.0" - checksum: 5af741c985ec7ac3e73b2fd5ebd091a63428e51df453fdb868fbfd43ea4b50bb67cdacc904a9ace581501d739648650db68edc3bb5251297aa117c37ce707d7c + checksum: 7a2fa2113a00c32e547b7883264823301f8d9ea76259c2b48385e490884db7b3988b6f93c89dd1d37a0b24c3bc043675d5e13fa3f8b59e73374ebff13374d268 languageName: node linkType: hard @@ -14873,7 +14873,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^6.13.1 "@typescript-eslint/parser": ^6.13.1 bootstrap-datepicker: ^1.10.0 - capacitor-blob-writer: ^1.1.14 + capacitor-blob-writer: ^1.1.17 clone: ^2.1.2 codelyzer: ^6.0.2 concurrently: ^6.2.0 From 99b89aa1ec78d314315fe75b906466fdc5fa500d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 24 Aug 2024 12:50:55 -0700 Subject: [PATCH 081/204] chore: code tidying --- .../task-card/task-card.component.ts | 12 ++++++---- .../instance/template-action.service.ts | 16 +++++--------- .../shared/services/task/task.service.spec.ts | 22 +++++++++---------- src/app/shared/services/task/task.service.ts | 3 ++- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/app/shared/components/template/components/task-card/task-card.component.ts b/src/app/shared/components/template/components/task-card/task-card.component.ts index 02f4aff97..b634413f4 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.ts +++ b/src/app/shared/components/template/components/task-card/task-card.component.ts @@ -115,10 +115,7 @@ export class TmplTaskCardComponent extends TemplateBaseComponent implements OnIn ngOnInit() { this.getParams(); - this.highlighted = - this.taskGroupId && !this.taskId - ? this.taskService.checkHighlightedTaskGroup(this.taskGroupId) - : false; + this.highlighted = this.checkGroupHighlighted(); this.checkProgressStatus(); } @@ -161,4 +158,11 @@ export class TmplTaskCardComponent extends TemplateBaseComponent implements OnIn this.triggerActions("completed"); } } + + private checkGroupHighlighted() { + if (this.taskGroupId && !this.taskId) { + return this.taskService.checkHighlightedTaskGroup(this.taskGroupId); + } + return false; + } } diff --git a/src/app/shared/components/template/services/instance/template-action.service.ts b/src/app/shared/components/template/services/instance/template-action.service.ts index 87a5ee113..6345256db 100644 --- a/src/app/shared/components/template/services/instance/template-action.service.ts +++ b/src/app/shared/components/template/services/instance/template-action.service.ts @@ -15,7 +15,6 @@ import { DBSyncService } from "src/app/shared/services/db/db-sync.service"; import { AuthService } from "src/app/shared/services/auth/auth.service"; import { SkinService } from "src/app/shared/services/skin/skin.service"; import { ThemeService } from "src/app/feature/theme/services/theme.service"; -import { TaskService } from "src/app/shared/services/task/task.service"; import { getGlobalService } from "src/app/shared/services/global.service"; import { SyncServiceBase } from "src/app/shared/services/syncService.base"; import { TemplateActionRegistry } from "./template-action.registry"; @@ -35,7 +34,10 @@ export class TemplateActionService extends SyncServiceBase { private actionsQueue: FlowTypes.TemplateRowAction[] = []; private actionsQueueProcessing$ = new BehaviorSubject(false); - constructor(private injector: Injector, public container?: TemplateContainerComponent) { + constructor( + private injector: Injector, + public container?: TemplateContainerComponent + ) { super("TemplateAction"); } // Retrive all services on demand from global injector @@ -72,9 +74,7 @@ export class TemplateActionService extends SyncServiceBase { private get themeService() { return getGlobalService(this.injector, ThemeService); } - private get taskService() { - return getGlobalService(this.injector, TaskService); - } + private get campaignService() { return getGlobalService(this.injector, CampaignService); } @@ -84,11 +84,7 @@ export class TemplateActionService extends SyncServiceBase { } private async ensurePublicServicesReady() { - await this.ensureAsyncServicesReady([ - this.templateTranslateService, - this.dbSyncService, - this.taskService, - ]); + await this.ensureAsyncServicesReady([this.templateTranslateService, this.dbSyncService]); this.ensureSyncServicesReady([ this.serverService, this.templateNavService, diff --git a/src/app/shared/services/task/task.service.spec.ts b/src/app/shared/services/task/task.service.spec.ts index 7d5796c0c..e883e750e 100644 --- a/src/app/shared/services/task/task.service.spec.ts +++ b/src/app/shared/services/task/task.service.spec.ts @@ -160,10 +160,10 @@ describe("TaskService", () => { }); it("evaluates highlighted task group correctly after init", async () => { await service.ready(); - expect(service.evaluateHighlightedTaskGroup().previousHighlightedTaskGroup).toBe( + expect(service["evaluateHighlightedTaskGroup"]().previousHighlightedTaskGroup).toBe( MOCK_DATA.data_list[taskGroupsListName].rows[0].id ); - expect(service.evaluateHighlightedTaskGroup().newHighlightedTaskGroup).toBe( + expect(service["evaluateHighlightedTaskGroup"]().newHighlightedTaskGroup).toBe( MOCK_DATA.data_list[taskGroupsListName].rows[0].id ); }); @@ -178,7 +178,7 @@ describe("TaskService", () => { }); it("can set a task group's completed status", async () => { await service.ready(); - await service.setTaskGroupCompletedField( + await service["setTaskGroupCompletedField"]( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, true ); @@ -191,40 +191,40 @@ describe("TaskService", () => { it("completing the highlighted task causes the next highest priority task to be highlighted upon re-evaluation", async () => { await service.ready(); // Complete highlighted task - await service.setTaskGroupCompletedField( + await service["setTaskGroupCompletedField"]( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, true ); const { previousHighlightedTaskGroup, newHighlightedTaskGroup } = - service.evaluateHighlightedTaskGroup(); + service["evaluateHighlightedTaskGroup"](); expect(previousHighlightedTaskGroup).toBe(MOCK_DATA.data_list[taskGroupsListName].rows[0].id); expect(newHighlightedTaskGroup).toBe(MOCK_DATA.data_list[taskGroupsListName].rows[2].id); }); it("when all tasks are completed, the highlighted task group is set to ''", async () => { await service.ready(); // Complete all tasks - await service.setTaskGroupCompletedField( + await service["setTaskGroupCompletedField"]( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, true ); - await service.setTaskGroupCompletedField( + await service["setTaskGroupCompletedField"]( MOCK_DATA.data_list[taskGroupsListName].rows[1].completed_field, true ); - await service.setTaskGroupCompletedField( + await service["setTaskGroupCompletedField"]( MOCK_DATA.data_list[taskGroupsListName].rows[2].completed_field, true ); - expect(service.evaluateHighlightedTaskGroup().newHighlightedTaskGroup).toBe(""); + expect(service["evaluateHighlightedTaskGroup"]().newHighlightedTaskGroup).toBe(""); }); it("schedules campaign notifications on change of highlighted task", async () => { await service.ready(); // Complete highlighted task - await service.setTaskGroupCompletedField( + await service["setTaskGroupCompletedField"]( MOCK_DATA.data_list[taskGroupsListName].rows[0].completed_field, true ); - service.evaluateHighlightedTaskGroup(); + service["evaluateHighlightedTaskGroup"](); await _wait(50); // scheduleCampaignNotifications() should be called once on init (since the highlighted task group changes), // and again on the evaluation called above diff --git a/src/app/shared/services/task/task.service.ts b/src/app/shared/services/task/task.service.ts index a8731b4ec..08cb1af72 100644 --- a/src/app/shared/services/task/task.service.ts +++ b/src/app/shared/services/task/task.service.ts @@ -44,6 +44,7 @@ export class TaskService extends AsyncServiceBase { } /** + * Determine which task group should be highlighted. * The highlighted task group should always be the ID of the highest * priority task_group that is not completed and not skipped * NB "highest priority" is defined as having the lowest numerical value for the "number" column @@ -88,7 +89,7 @@ export class TaskService extends AsyncServiceBase { return { previousHighlightedTaskGroup, newHighlightedTaskGroup }; } - /** Get the id of the task group stored as higlighted */ + /** Get the id of the task group stored as highlighted */ public getHighlightedTaskGroup() { return this.templateFieldService.getField(this.highlightedTaskField); } From afe416012524b7c86591d097f36487b756aea9ef Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 24 Aug 2024 13:35:05 -0700 Subject: [PATCH 082/204] feat: add jest test runner --- packages/scripts/jest.config.js | 7 ++++ packages/scripts/package.json | 9 +++-- yarn.lock | 63 ++++++++++++++++++++++++++++----- 3 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 packages/scripts/jest.config.js diff --git a/packages/scripts/jest.config.js b/packages/scripts/jest.config.js new file mode 100644 index 000000000..f5d30d13b --- /dev/null +++ b/packages/scripts/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest",{}], + }, +}; \ No newline at end of file diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 8900e2783..35e9503c9 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -11,7 +11,7 @@ "start": "ts-node src/commands/index.ts", "build": "ts-node build.ts", "dev": "ts-node-dev --transpile-only --respawn --watch src src/commands/index.ts", - "test": "jasmine-ts --config=jasmine.json", + "test": "jest", "test:watch": "nodemon --ext ts --exec 'jasmine-ts --config=jasmine.json'" }, "author": "", @@ -47,14 +47,13 @@ "@swc/core": "^1.3.29", "@types/fs-extra": "^9.0.4", "@types/inquirer": "^7.3.1", - "@types/jasmine": "^3.10.6", + "@types/jest": "^29.5.12", "@types/node-rsa": "^1.1.1", "@types/semver": "^7.3.9", - "jasmine": "^3.99.0", - "jasmine-spec-reporter": "^7.0.0", - "jasmine-ts": "^0.4.0", + "jest": "^29.7.0", "mock-fs": "^5.2.0", "nodemon": "^2.0.19", + "ts-jest": "^29.2.5", "ts-node": "^10.8.0", "ts-node-dev": "^2.0.0", "tsup": "^7.2.0" diff --git a/yarn.lock b/yarn.lock index eb2962f24..12b946668 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7382,7 +7382,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.6": +"@types/jest@npm:^29.5.12, @types/jest@npm:^29.5.6": version: 29.5.12 resolution: "@types/jest@npm:29.5.12" dependencies: @@ -9999,7 +9999,7 @@ __metadata: languageName: node linkType: hard -"bs-logger@npm:0.x": +"bs-logger@npm:0.x, bs-logger@npm:^0.2.6": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" dependencies: @@ -12578,7 +12578,7 @@ __metadata: languageName: node linkType: hard -"ejs@npm:^3.1.7": +"ejs@npm:^3.1.10, ejs@npm:^3.1.7": version: 3.1.10 resolution: "ejs@npm:3.1.10" dependencies: @@ -18881,7 +18881,7 @@ __metadata: languageName: node linkType: hard -"lodash.memoize@npm:4.x": +"lodash.memoize@npm:4.x, lodash.memoize@npm:^4.1.2": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2" checksum: 9ff3942feeccffa4f1fafa88d32f0d24fdc62fd15ded5a74a5f950ff5f0c6f61916157246744c620173dddf38d37095a92327d5fd3861e2063e736a5c207d089 @@ -19201,7 +19201,7 @@ __metadata: languageName: node linkType: hard -"make-error@npm:1.x, make-error@npm:^1.1.1": +"make-error@npm:1.x, make-error@npm:^1.1.1, make-error@npm:^1.3.6": version: 1.3.6 resolution: "make-error@npm:1.3.6" checksum: b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 @@ -23448,7 +23448,7 @@ __metadata: "@swc/core": ^1.3.29 "@types/fs-extra": ^9.0.4 "@types/inquirer": ^7.3.1 - "@types/jasmine": ^3.10.6 + "@types/jest": ^29.5.12 "@types/node-rsa": ^1.1.1 "@types/semver": ^7.3.9 actions: "workspace:*" @@ -23460,9 +23460,7 @@ __metadata: data-models: "workspace:*" fs-extra: ^9.0.1 inquirer: ^7.3.3 - jasmine: ^3.99.0 - jasmine-spec-reporter: ^7.0.0 - jasmine-ts: ^0.4.0 + jest: ^29.7.0 log-update: ^4.0.0 mock-fs: ^5.2.0 node-rsa: ^1.1.1 @@ -23473,6 +23471,7 @@ __metadata: shared: "workspace:*" simple-git: ^3.7.1 subtitles-parser-vtt: ^0.1.0 + ts-jest: ^29.2.5 ts-morph: ^15.0.0 ts-node: ^10.8.0 ts-node-dev: ^2.0.0 @@ -23569,6 +23568,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.6.3": + version: 7.6.3 + resolution: "semver@npm:7.6.3" + bin: + semver: bin/semver.js + checksum: 4110ec5d015c9438f322257b1c51fe30276e5f766a3f64c09edd1d7ea7118ecbc3f379f3b69032bacf13116dc7abc4ad8ce0d7e2bd642e26b0d271b56b61a7d8 + languageName: node + linkType: hard + "semver@npm:~7.0.0": version: 7.0.0 resolution: "semver@npm:7.0.0" @@ -25559,6 +25567,43 @@ __metadata: languageName: node linkType: hard +"ts-jest@npm:^29.2.5": + version: 29.2.5 + resolution: "ts-jest@npm:29.2.5" + dependencies: + bs-logger: ^0.2.6 + ejs: ^3.1.10 + fast-json-stable-stringify: ^2.1.0 + jest-util: ^29.0.0 + json5: ^2.2.3 + lodash.memoize: ^4.1.2 + make-error: ^1.3.6 + semver: ^7.6.3 + yargs-parser: ^21.1.1 + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/transform": ^29.0.0 + "@jest/types": ^29.0.0 + babel-jest: ^29.0.0 + jest: ^29.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/transform": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + bin: + ts-jest: cli.js + checksum: d60d1e1d80936f6002b1bb27f7e062408bc733141b9d666565503f023c340a3196d506c836a4316c5793af81a5f910ab49bb9c13f66e2dc66de4e0f03851dbca + languageName: node + linkType: hard + "ts-loader@npm:^8.4.0": version: 8.4.0 resolution: "ts-loader@npm:8.4.0" From 656253bb80b4c4ad69da854d5181c3d08f71b014 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 24 Aug 2024 15:17:25 -0700 Subject: [PATCH 083/204] refactor: global test setup --- packages/scripts/jest.config.js | 6 ++++-- packages/scripts/package.json | 5 ++--- packages/scripts/test/helpers/reporters.ts | 11 ----------- packages/scripts/test/helpers/utils.ts | 8 ++++---- packages/scripts/test/setup.ts | 7 +++++++ 5 files changed, 17 insertions(+), 20 deletions(-) delete mode 100644 packages/scripts/test/helpers/reporters.ts create mode 100644 packages/scripts/test/setup.ts diff --git a/packages/scripts/jest.config.js b/packages/scripts/jest.config.js index f5d30d13b..37a5b6e94 100644 --- a/packages/scripts/jest.config.js +++ b/packages/scripts/jest.config.js @@ -2,6 +2,8 @@ module.exports = { testEnvironment: "node", transform: { - "^.+.tsx?$": ["ts-jest",{}], + "^.+.tsx?$": ["ts-jest", {}], }, -}; \ No newline at end of file + globalSetup: "/test/setup.ts", +}; + diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 35e9503c9..8bea98701 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -11,8 +11,8 @@ "start": "ts-node src/commands/index.ts", "build": "ts-node build.ts", "dev": "ts-node-dev --transpile-only --respawn --watch src src/commands/index.ts", - "test": "jest", - "test:watch": "nodemon --ext ts --exec 'jasmine-ts --config=jasmine.json'" + "test": "jest --", + "test:watch": "jest --watchAll --" }, "author": "", "license": "ISC", @@ -52,7 +52,6 @@ "@types/semver": "^7.3.9", "jest": "^29.7.0", "mock-fs": "^5.2.0", - "nodemon": "^2.0.19", "ts-jest": "^29.2.5", "ts-node": "^10.8.0", "ts-node-dev": "^2.0.0", diff --git a/packages/scripts/test/helpers/reporters.ts b/packages/scripts/test/helpers/reporters.ts deleted file mode 100644 index 6420c40ad..000000000 --- a/packages/scripts/test/helpers/reporters.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { SpecReporter } from "jasmine-spec-reporter"; - -jasmine.getEnv().clearReporters(); // remove default reporter logs -jasmine.getEnv().addReporter( - new SpecReporter({ - // add jasmine-spec-reporter - spec: { - displayPending: true, - }, - }) -); diff --git a/packages/scripts/test/helpers/utils.ts b/packages/scripts/test/helpers/utils.ts index 58d9a2f71..8578b3b52 100644 --- a/packages/scripts/test/helpers/utils.ts +++ b/packages/scripts/test/helpers/utils.ts @@ -6,14 +6,14 @@ import { Logger } from "shared/src/utils/logging/console-logger"; /** Mock function that will replace default `Logger` function to instead just record any invocations */ export function useMockErrorLogger() { - const mockErrorLogger = jasmine.createSpy("mockErrorLogger", Logger.error); - spyOn(Logger, "error").and.callFake(mockErrorLogger); + const mockErrorLogger = jest.fn(); + jest.spyOn(Logger, "error").mockImplementation(mockErrorLogger); return mockErrorLogger; } /** Mock function that will replace default `Logger` function to instead just record any invocations */ export function useMockWarningLogger() { - const mockWarningLogger = jasmine.createSpy("mockWarningLogger", Logger.warning); - spyOn(Logger, "warning").and.callFake(mockWarningLogger); + const mockWarningLogger = jest.fn(); + jest.spyOn(Logger, "warning").mockImplementation(mockWarningLogger); return mockWarningLogger; } diff --git a/packages/scripts/test/setup.ts b/packages/scripts/test/setup.ts new file mode 100644 index 000000000..c3f75ba47 --- /dev/null +++ b/packages/scripts/test/setup.ts @@ -0,0 +1,7 @@ +import { createChildFileLogger } from "shared/src/utils/logging/file-logger"; + +export default () => { + // Create a logger instance so that parallel test calls don't try to create/empty + // directory when initiating for first time + createChildFileLogger(); +}; From b797075c512f88a967792a33dced5806e082d2d2 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 24 Aug 2024 20:28:50 -0700 Subject: [PATCH 084/204] refactor: script loggers --- .../shared/src/utils/logging/file-logger.ts | 97 ++++++++----------- .../shared/src/utils/logging/memory-logger.ts | 47 +++++++++ 2 files changed, 90 insertions(+), 54 deletions(-) create mode 100644 packages/shared/src/utils/logging/memory-logger.ts diff --git a/packages/shared/src/utils/logging/file-logger.ts b/packages/shared/src/utils/logging/file-logger.ts index 06987d5a0..3ebed0661 100644 --- a/packages/shared/src/utils/logging/file-logger.ts +++ b/packages/shared/src/utils/logging/file-logger.ts @@ -1,35 +1,20 @@ import winston from "winston"; import path from "path"; -import { emptyDirSync, ensureDirSync, truncateSync } from "fs-extra"; +import { truncateSync } from "fs-extra"; import { _wait } from "../async-utils"; -import { Writable } from "stream"; import { existsSync } from "fs"; import { SCRIPTS_LOGS_DIR } from "../../paths"; +import { getGlobalMemoryLoggerTransport } from "./memory-logger"; const logLevels = ["debug", "info", "warning", "error"] as const; type ILogLevel = (typeof logLevels)[number]; -interface ILogEntry { - level: ILogLevel; - message?: string; - details?: any; - source?: string; -} - -// Declare a history variable that can be written to via a stream -let logHistory = ""; /** Retrieve all logs from current session for a given variable */ -export function getLogs(level: ILogLevel, message?: string) { - const logEntries: ILogEntry[] = logHistory - .split("\n") - .filter((v) => v) - .map((v) => JSON.parse(v)); - const logLevelEntries = logEntries.filter((entry) => entry.level === level); - if (message) { - return logLevelEntries.filter((entry) => entry.message === message); - } - return logLevelEntries; +export function getLogs(level: ILogLevel) { + const logger = getGlobalMemoryLoggerTransport(); + return logger.get(level); } + export function getLogFiles() { const logFiles: { [level in ILogLevel]: string } = {} as any; for (const level of logLevels) { @@ -38,40 +23,52 @@ export function getLogFiles() { return logFiles; } -export function clearLogs(attempt = 0) { - logHistory = ""; - try { - const logFiles = getLogFiles(); - for (const logFile of Object.values(logFiles)) { - if (existsSync(logFile)) { - truncateSync(logFile); +/** + * Clear existing log data. Clears all in-memory logs and optional persisted file + * @param includeFiles - clear log files written to disk. + * Operation will fail if open during parallel operations + */ +export function clearLogs(includeFiles = false, attempt = 0) { + // Clear in-memory logs + const memoryLogger = getGlobalMemoryLoggerTransport(); + memoryLogger.clear(); + // Clear file-based logs + if (includeFiles) { + try { + const logFiles = getLogFiles(); + for (const logFile of Object.values(logFiles)) { + if (existsSync(logFile)) { + truncateSync(logFile); + } } + } catch (error) { + attempt++; + if (attempt > 5) { + throw error; + } + console.log("could not clear logs, retrying...", attempt); + return clearLogs(includeFiles, attempt); } - } catch (error) { - attempt++; - if (attempt > 5) { - throw error; - } - console.log("could not clear logs, retrying...", attempt); - return clearLogs(attempt); } } /** - * Create loggers that write to file based on level and also save all logs to a single string + * Create loggers that write to file based on level and also save to memory * for easy querying */ -function getGlobalFileLogger() { +export function getGlobalFileLogger() { const g = global as any; if (g.logger) { return g.logger as winston.Logger; } - // setup files - logHistory = ""; - ensureDirSync(SCRIPTS_LOGS_DIR); - emptyDirSync(SCRIPTS_LOGS_DIR); + // remove any previous log files + clearLogs(true); + + // create in-memory logger transport + const memoryLogger = getGlobalMemoryLoggerTransport(); + + // create file-write logger transports const logFiles = getLogFiles(); - // file transports const fileTransports = logLevels.map( (level) => new winston.transports.File({ @@ -80,21 +77,13 @@ function getGlobalFileLogger() { format: winston.format.prettyPrint(), }) ); + + // create unified logger const logger = winston.createLogger({ level: "info", - transports: fileTransports, - }); - // stream (memory) transport - const logStream = new Writable(); - logStream._write = (chunk, encoding, next) => { - logHistory = logHistory += chunk.toString(); - next(); - }; - const streamTransport = new winston.transports.Stream({ - stream: logStream, - format: winston.format.json(), + transports: [memoryLogger, ...fileTransports], }); - logger.add(streamTransport); + g.logger = logger; return logger; } diff --git a/packages/shared/src/utils/logging/memory-logger.ts b/packages/shared/src/utils/logging/memory-logger.ts new file mode 100644 index 000000000..257280133 --- /dev/null +++ b/packages/shared/src/utils/logging/memory-logger.ts @@ -0,0 +1,47 @@ +import Transport from "winston-transport"; + +const logLevels = ["debug", "info", "warning", "error"] as const; +type ILogLevel = (typeof logLevels)[number]; + +interface ILogEntry { + level: ILogLevel; + message?: string; + details?: any; + source?: string; +} + +/** + * In-memory log transport capable of returning all stored logs + * Should be added as a transport to the default logging instance + **/ +export class MemoryLogger extends Transport { + private cache: { [level in ILogLevel]: ILogEntry[] }; + + constructor() { + super(); + this.clear(); + } + public get(level: ILogLevel) { + return this.cache[level]; + } + clear() { + this.cache = { + debug: [], + error: [], + info: [], + warning: [], + }; + } + log(entry: ILogEntry, callback) { + const { level, details, message, source } = entry; + this.cache[level].push({ level, details, message, source }); + callback(); + } +} + +/** Provide access to a shared global instance of memory logger */ +export function getGlobalMemoryLoggerTransport() { + const g = global as any; + g["logCache"] ??= new MemoryLogger(); + return g["logCache"] as MemoryLogger; +} From 8ff341edd35868e92ebbb4d4bcf155ad94dadadb Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 25 Aug 2024 11:01:47 -0700 Subject: [PATCH 085/204] refactor: log utils --- .../commands/app-data/convert/convert.spec.ts | 32 ++++++++----------- .../processors/flowParser/flowParser.spec.ts | 2 +- .../utils/app-data-override.utils.spec.ts | 8 +++-- .../app-data/postProcess/assets.spec.ts | 11 ++++--- packages/scripts/test/helpers/utils.ts | 27 ++++++++-------- packages/scripts/test/setup.ts | 4 +-- .../shared/src/utils/logging/file-logger.ts | 1 + 7 files changed, 42 insertions(+), 43 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/convert.spec.ts b/packages/scripts/src/commands/app-data/convert/convert.spec.ts index 5bcbcec9d..6b85bc503 100644 --- a/packages/scripts/src/commands/app-data/convert/convert.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/convert.spec.ts @@ -3,9 +3,8 @@ import { AppDataConverter } from "./index"; import path, { resolve } from "path"; import { SCRIPTS_TEST_DATA_DIR } from "../../../paths"; -import { emptyDirSync, existsSync, readdirSync, readJSONSync } from "fs-extra"; -import { clearLogs, getLogs } from "./utils"; -import { useMockErrorLogger } from "../../../../test/helpers/utils"; +import { emptyDirSync, existsSync, readdirSync, readJSONSync, ensureDirSync } from "fs-extra"; +import { clearLogs } from "shared"; // Folders used for tests const paths = { @@ -14,20 +13,17 @@ const paths = { cacheFolder: path.resolve(SCRIPTS_TEST_DATA_DIR, "cache"), }; +/** yarn workspace scripts test -t convert.spec.ts */ describe("App Data Converter", () => { let converter: AppDataConverter; beforeAll(() => { - if (existsSync(paths.outputFolder)) { - path.resolve(SCRIPTS_TEST_DATA_DIR, "output"); - } + ensureDirSync(paths.outputFolder); + emptyDirSync(paths.outputFolder); }); beforeEach(() => { - clearLogs(); converter = new AppDataConverter(paths); }); - afterAll(() => { - emptyDirSync(path.resolve(SCRIPTS_TEST_DATA_DIR, "output")); - }); + it("Uses child caches", async () => { const cacheFolders = readdirSync(paths.cacheFolder); expect(cacheFolders.length).toBeGreaterThan(1); @@ -37,10 +33,10 @@ describe("App Data Converter", () => { const cacheFolders = readdirSync(paths.cacheFolder); expect(cacheFolders.length).toEqual(1); // only contents file }); - it("Processes test_input xlsx without error", async () => { - await converter.run(); - const errorLogs = getLogs("error"); - expect(errorLogs.length).toEqual(0); + it.only("Processes test_input xlsx without error", async () => { + const { errors, result } = await converter.run(); + expect(errors.length).toEqual(0); + expect(Object.values(result).length).toBeGreaterThan(0); }); it("Populates output to folder by data type", async () => { await converter.run(); @@ -76,20 +72,18 @@ const errorPaths = { }; describe("App Data Converter - Error Checking", () => { let errorConverter: AppDataConverter; - let errorLogger: jasmine.Spy; beforeAll(() => { if (existsSync(paths.outputFolder)) { emptyDirSync(paths.outputFolder); } }); beforeEach(() => { - errorLogger = useMockErrorLogger(); errorConverter = new AppDataConverter(errorPaths); }); it("Tracks conversion errors", async () => { - await errorConverter.run(); - const loggerErrors = getLogs("error"); - const errorMessages = loggerErrors.map((err) => err.message); + clearLogs(true); + const { errors } = await errorConverter.run(); + const errorMessages = errors.map((err) => err.message); expect(errorMessages).toEqual([ "Duplicate flow name", "No parser available for flow_type: test_invalid_type", diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.spec.ts index c20869712..0acf740a1 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.spec.ts @@ -77,7 +77,7 @@ describe("FlowParser Processor", () => { rows: null, }; await processor.process([brokenFlow]); - const errorLogs = getLogs("error", "Template parse error"); + const errorLogs = getLogs("error").filter(({ message }) => message === "Template parse error"); expect(errorLogs).toEqual([ { source: "flowParser", diff --git a/packages/scripts/src/commands/app-data/convert/utils/app-data-override.utils.spec.ts b/packages/scripts/src/commands/app-data/convert/utils/app-data-override.utils.spec.ts index 0deb9a73c..3b0b93731 100644 --- a/packages/scripts/src/commands/app-data/convert/utils/app-data-override.utils.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/utils/app-data-override.utils.spec.ts @@ -1,5 +1,5 @@ import { FlowTypes } from "data-models"; -import { useMockWarningLogger } from "../../../../../test/helpers/utils"; +import { useMockLogger } from "../../../../../test/helpers/utils"; import { assignFlowOverrides } from "./app-data-override.utils"; const TEST_INPUTS: FlowTypes.FlowTypeWithData[] = [ @@ -24,6 +24,7 @@ const TEST_INPUTS: FlowTypes.FlowTypeWithData[] = [ }, ]; +/** yarn workspace scripts test -t app-data-override.utils.spec.ts */ describe("App Data Override", () => { it("Assigns flow override mapping", () => { const output = assignFlowOverrides(TEST_INPUTS); @@ -36,7 +37,7 @@ describe("App Data Override", () => { }); it("Logs warning on missing override target", () => { - const warningLogger = useMockWarningLogger(); + const loggerSpy = useMockLogger(true); assignFlowOverrides([ ...TEST_INPUTS, { @@ -47,7 +48,8 @@ describe("App Data Override", () => { override_condition: "test_condition_3", }, ]); - expect(warningLogger).toHaveBeenCalledOnceWith({ + expect(loggerSpy.warning).toHaveBeenCalledTimes(1); + expect(loggerSpy.warning).toHaveBeenCalledWith({ msg1: "Override target does not exist: missing_list", msg2: "override_invalid", }); diff --git a/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts b/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts index 7a8f0d5f3..cf6a6a5ba 100644 --- a/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts +++ b/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts @@ -11,7 +11,7 @@ import mockFs from "mock-fs"; import { ActiveDeployment } from "../../deployment/get"; import path, { resolve } from "path"; import { IAssetEntryHashmap } from "data-models/deployment.model"; -import { useMockErrorLogger } from "../../../../test/helpers/utils"; +import { useMockLogger } from "../../../../test/helpers/utils"; /** Mock file system folders for use in tests */ const mockDirs = { @@ -106,7 +106,7 @@ describe("Assets PostProcess", () => { mockLocalAssets({ "test.jpg": mockFile }); runAssetsPostProcessor(); const contents = readAppAssetContents(); - expect("test.jpg" in contents).toBeTrue(); + expect("test.jpg" in contents).toEqual(true); }); it("Populates global assets from named or root folder", () => { @@ -274,7 +274,7 @@ describe("Assets PostProcess", () => { /** QA tests */ it("throws error on duplicate overrides", () => { - const errorLogger = useMockErrorLogger(); + const mockLogger = useMockLogger(); mockLocalAssets({ "test.jpg": mockFile, theme_test: { @@ -288,7 +288,8 @@ describe("Assets PostProcess", () => { filter_language_codes: ["tz_sw"], app_themes_available: ["test"], }); - expect(errorLogger).toHaveBeenCalledOnceWith({ + expect(mockLogger.error).toHaveBeenCalledTimes(1); + expect(mockLogger.error).toHaveBeenCalledWith({ msg1: "Duplicate overrides detected", msg2: "test.jpg [theme_test] [tz_sw]", logOnly: true, @@ -358,5 +359,5 @@ function stubDeploymentConfig(stub: IDeploymentConfigStub = {}) { APP_THEMES: { available: app_themes_available }, } as any, }; - spyOn(ActiveDeployment, "get").and.returnValue(stubDeployment as IDeploymentConfigJson); + jest.spyOn(ActiveDeployment, "get").mockReturnValue(stubDeployment as IDeploymentConfigJson); } diff --git a/packages/scripts/test/helpers/utils.ts b/packages/scripts/test/helpers/utils.ts index 8578b3b52..a25740541 100644 --- a/packages/scripts/test/helpers/utils.ts +++ b/packages/scripts/test/helpers/utils.ts @@ -1,19 +1,20 @@ import { Logger } from "shared/src/utils/logging/console-logger"; /************************************************************* - * Test utilties + * Test utilities *************************************************************/ -/** Mock function that will replace default `Logger` function to instead just record any invocations */ -export function useMockErrorLogger() { - const mockErrorLogger = jest.fn(); - jest.spyOn(Logger, "error").mockImplementation(mockErrorLogger); - return mockErrorLogger; -} - -/** Mock function that will replace default `Logger` function to instead just record any invocations */ -export function useMockWarningLogger() { - const mockWarningLogger = jest.fn(); - jest.spyOn(Logger, "warning").mockImplementation(mockWarningLogger); - return mockWarningLogger; +/** + * Create spy to track usage of console Logger method + * @param callOriginal specify whether to still call original Logger + * methods after spy intercept + */ +export function useMockLogger(callOriginal = true) { + const error = jest.spyOn(Logger, "error"); + const warning = jest.spyOn(Logger, "warning"); + if (!callOriginal) { + error.mockImplementation(jest.fn()); + warning.mockImplementation(jest.fn()); + } + return { error, warning }; } diff --git a/packages/scripts/test/setup.ts b/packages/scripts/test/setup.ts index c3f75ba47..f5ce2ca01 100644 --- a/packages/scripts/test/setup.ts +++ b/packages/scripts/test/setup.ts @@ -1,7 +1,7 @@ -import { createChildFileLogger } from "shared/src/utils/logging/file-logger"; +import { getGlobalFileLogger } from "shared/src/utils/logging/file-logger"; export default () => { // Create a logger instance so that parallel test calls don't try to create/empty // directory when initiating for first time - createChildFileLogger(); + getGlobalFileLogger(); }; diff --git a/packages/shared/src/utils/logging/file-logger.ts b/packages/shared/src/utils/logging/file-logger.ts index 3ebed0661..ddd9e8513 100644 --- a/packages/shared/src/utils/logging/file-logger.ts +++ b/packages/shared/src/utils/logging/file-logger.ts @@ -88,6 +88,7 @@ export function getGlobalFileLogger() { return logger; } +/** Create a child instance of the file logger with additional context meta */ export function createChildFileLogger(meta = {}) { const logger = getGlobalFileLogger(); return logger.child(meta); From b164b6ca328def36c54fcfdfb961cfff9cfbbb70 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 25 Aug 2024 13:38:27 -0700 Subject: [PATCH 086/204] refactor: replace mock-fs specs with memfs --- cspell.config.yml | 1 + packages/scripts/package.json | 2 +- .../app-data/postProcess/assets.spec.ts | 58 ++++++------- yarn.lock | 86 +++++++++++++++++-- 4 files changed, 108 insertions(+), 39 deletions(-) diff --git a/cspell.config.yml b/cspell.config.yml index e02f642f4..07d789a63 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -20,6 +20,7 @@ "linenums", "lottie", "matomo", + "memfs", "metabase", "mkdocs", "mycompany", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 8bea98701..83cdd1492 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -51,7 +51,7 @@ "@types/node-rsa": "^1.1.1", "@types/semver": "^7.3.9", "jest": "^29.7.0", - "mock-fs": "^5.2.0", + "memfs": "^4.11.1", "ts-jest": "^29.2.5", "ts-node": "^10.8.0", "ts-node-dev": "^2.0.0", diff --git a/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts b/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts index cf6a6a5ba..67595835d 100644 --- a/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts +++ b/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts @@ -4,15 +4,22 @@ import { AssetsPostProcessor } from "./assets"; import type { IDeploymentConfigJson } from "../../deployment/common"; import type { RecursivePartial } from "data-models/appConfig"; -import fs, { readJsonSync, readdirSync } from "fs-extra"; -import mockFs from "mock-fs"; +import { readJsonSync, readdirSync, statSync, existsSync } from "fs-extra"; +import { vol } from "memfs"; // Use default imports to allow spying on functions and replacing with mock methods import { ActiveDeployment } from "../../deployment/get"; -import path, { resolve } from "path"; +import { posix } from "path"; import { IAssetEntryHashmap } from "data-models/deployment.model"; import { useMockLogger } from "../../../../test/helpers/utils"; +// Mock all fs calls to use memfs implementation +jest.mock("fs", () => require("memfs")); + +// HACK - resolve all paths using posix (/) file system as memfs does not support +// windows paths https://github.com/streamich/memfs/issues/327 +const { resolve } = posix; + /** Mock file system folders for use in tests */ const mockDirs = { appAssets: "mock/app_data/assets", @@ -23,19 +30,13 @@ const { file: mockFile, entry: mockFileEntry } = createMockFile(); // create moc /** Parse the contents.json file populated to the app assets folder and return */ function readAppAssetContents() { - const contentsPath = path.resolve(mockDirs.appAssets, "contents.json"); + const contentsPath = resolve(mockDirs.appAssets, "contents.json"); return readJsonSync(contentsPath) as IAssetEntryHashmap; } /** Create mock entries on file system corresponding to local assets folder */ -function mockLocalAssets(assets: Record) { - return mockFs({ - mock: { - local: { - assets, - }, - }, - }); +function mockLocalAssets(assets: Record = {}) { + vol.fromNestedJSON(assets, mockDirs.localAssets); } function createMockFile(size_kb: number = 1024) { @@ -44,33 +45,33 @@ function createMockFile(size_kb: number = 1024) { return { file, entry }; } +/** yarn workspace scripts test -t assets.spec.ts */ describe("Assets PostProcess", () => { - /** Initial setup */ - // replace prettier codeTidying method - // replace `Logger` function with created spy method - beforeAll(() => {}); // Populate a fake file system before each test. This will automatically be called for any fs operations // Restore regular file functionality after each test. beforeEach(() => { - mockLocalAssets({}); + mockLocalAssets(); }); afterEach(() => { - mockFs.restore(); + vol.reset(); }); /** Mock setup testing (can be removed once working consistenctly) */ it("mocks file system for testing", () => { mockLocalAssets({ folder: { "file.jpg": mockFile } }); - const testFilePath = path.resolve(mockDirs.localAssets, "folder", "file.jpg"); - expect(fs.statSync(testFilePath).size).toEqual(1 * 1024 * 1024); + const testFilePath = resolve(mockDirs.localAssets, "folder", "file.jpg"); + console.log({ testFilePath }); + console.log(existsSync(testFilePath)); + expect(existsSync(testFilePath)).toEqual(true); + expect(statSync(testFilePath).size).toEqual(1 * 1024 * 1024); }); /** Main tests */ it("Copies assets from local to app", () => { mockLocalAssets({ folder: { "file.jpg": mockFile } }); runAssetsPostProcessor(); - const testFilePath = path.resolve(mockDirs.appAssets, "folder", "file.jpg"); - expect(fs.statSync(testFilePath).size).toEqual(1 * 1024 * 1024); + const testFilePath = resolve(mockDirs.appAssets, "folder", "file.jpg"); + expect(statSync(testFilePath).size).toEqual(1 * 1024 * 1024); }); it("Supports multiple input folders", () => { @@ -98,8 +99,8 @@ describe("Assets PostProcess", () => { const expectedFiles = ["folder/file_a.jpg", "folder/file_b.jpg", "folder/file_c.jpg"]; expect(Object.keys(contents)).toEqual(expectedFiles); // test file_b overidden from source_b - const overiddenFilePath = path.resolve(mockDirs.appAssets, "folder", "file_b.jpg"); - expect(fs.statSync(overiddenFilePath).size).toEqual(1 * 1024 * overrideFileSize); + const overiddenFilePath = resolve(mockDirs.appAssets, "folder", "file_b.jpg"); + expect(statSync(overiddenFilePath).size).toEqual(1 * 1024 * overrideFileSize); }); it("populates contents json", () => { @@ -122,7 +123,6 @@ describe("Assets PostProcess", () => { "test1.jpg": { ...mockFileEntry, filePath: "global/test1.jpg" }, "test3.jpg": mockFileEntry, }); - mockFs.restore(); }); it("Populates assets with no overrides", () => { @@ -199,7 +199,7 @@ describe("Assets PostProcess", () => { theme_ignored: { "test.jpg": mockFile }, }); runAssetsPostProcessor({ app_themes_available: ["testTheme"] }); - expect(readdirSync(mockDirs.appAssets)).toEqual([ + expect(readdirSync(mockDirs.appAssets).sort()).toEqual([ "contents.json", "test.jpg", "theme_testTheme", @@ -234,7 +234,7 @@ describe("Assets PostProcess", () => { ke_sw: { "test.jpg": mockFile }, }); runAssetsPostProcessor({ filter_language_codes: ["tz_sw"] }); - expect(readdirSync(mockDirs.appAssets)).toEqual(["contents.json", "test.jpg", "tz_sw"]); + expect(readdirSync(mockDirs.appAssets).sort()).toEqual(["contents.json", "test.jpg", "tz_sw"]); }); it("supports nested lang and theme folders", () => { @@ -312,8 +312,8 @@ describe("Assets PostProcess", () => { it("warns on untracked assets", () => { const { localAssets } = mockDirs; - const untrackedPath = path.resolve(localAssets, "tz_sw", "untracked.jpg"); - fs.writeFileSync(untrackedPath, mockFile); + const untrackedPath = resolve(localAssets, "tz_sw", "untracked.jpg"); + writeFileSync(untrackedPath, mockFile); runAssetsPostProcessor(); expect(mockWarningLogger).toHaveBeenCalledWith({ msg1: "Translated assets found without corresponding global", diff --git a/yarn.lock b/yarn.lock index 12b946668..6dcbd99cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5254,6 +5254,38 @@ __metadata: languageName: node linkType: hard +"@jsonjoy.com/base64@npm:^1.1.1": + version: 1.1.2 + resolution: "@jsonjoy.com/base64@npm:1.1.2" + peerDependencies: + tslib: 2 + checksum: 00dbf9cbc6ecb3af0e58288a305cc4ee3dfca9efa24443d98061756e8f6de4d6d2d3764bdfde07f2b03e6ce56db27c8a59b490bd134bf3d8122b4c6b394c7010 + languageName: node + linkType: hard + +"@jsonjoy.com/json-pack@npm:^1.0.3": + version: 1.1.0 + resolution: "@jsonjoy.com/json-pack@npm:1.1.0" + dependencies: + "@jsonjoy.com/base64": ^1.1.1 + "@jsonjoy.com/util": ^1.1.2 + hyperdyperid: ^1.2.0 + thingies: ^1.20.0 + peerDependencies: + tslib: 2 + checksum: 5c89a01814d5a7464639c3cbd4dbbcbf19165e9e6d6cc3cc985f8a7594fc2c5ac3a29e4f49f9ddf029979ec26ab980960a250db044173798509d0ea388c2ae26 + languageName: node + linkType: hard + +"@jsonjoy.com/util@npm:^1.1.2, @jsonjoy.com/util@npm:^1.3.0": + version: 1.3.0 + resolution: "@jsonjoy.com/util@npm:1.3.0" + peerDependencies: + tslib: 2 + checksum: a805ca7cf5fc05c6244324a955d96a28797fb8efd60cf22a809a57059de78e4367c72ffb367c82a7ea6ce5622e56f9c696393c5561fbac0fd3c9dc1534d62968 + languageName: node + linkType: hard + "@kwsites/file-exists@npm:^1.1.1": version: 1.1.1 resolution: "@kwsites/file-exists@npm:1.1.1" @@ -16107,6 +16139,13 @@ __metadata: languageName: node linkType: hard +"hyperdyperid@npm:^1.2.0": + version: 1.2.0 + resolution: "hyperdyperid@npm:1.2.0" + checksum: 210029d1c86926f09109f6317d143f8b056fc38e8dd11b0c3e3205fc6c6ff8429fb55b4b9c2bce065462719ed9d34366eced387aaa0035d93eb76b306a8547ef + languageName: node + linkType: hard + "i18next@npm:^23.7.6": version: 23.8.2 resolution: "i18next@npm:23.8.2" @@ -19362,6 +19401,18 @@ __metadata: languageName: node linkType: hard +"memfs@npm:^4.11.1": + version: 4.11.1 + resolution: "memfs@npm:4.11.1" + dependencies: + "@jsonjoy.com/json-pack": ^1.0.3 + "@jsonjoy.com/util": ^1.3.0 + tree-dump: ^1.0.1 + tslib: ^2.0.0 + checksum: 20f43af194c4bfc54d469bd63619569a78e7d529566be6fc0755e0a028af8c16d72f260c3f6d29664e0b8626e8f8e49ae7c96d7a7e5f67c472ebddf9a308834d + languageName: node + linkType: hard + "memoizee@npm:^0.4.15": version: 0.4.15 resolution: "memoizee@npm:0.4.15" @@ -19859,13 +19910,6 @@ __metadata: languageName: node linkType: hard -"mock-fs@npm:^5.2.0": - version: 5.2.0 - resolution: "mock-fs@npm:5.2.0" - checksum: c25835247bd26fa4e0189addd61f98973f61a72741e4d2a5694b143a2069b84978443a7ac0fdb1a71aead99273ec22ff4e9c968de11bbd076db020264c5b8312 - languageName: node - linkType: hard - "modifyjs@npm:0.3.1": version: 0.3.1 resolution: "modifyjs@npm:0.3.1" @@ -23462,9 +23506,8 @@ __metadata: inquirer: ^7.3.3 jest: ^29.7.0 log-update: ^4.0.0 - mock-fs: ^5.2.0 + memfs: ^4.11.1 node-rsa: ^1.1.1 - nodemon: ^2.0.19 open: ^8 p-queue: ^6.6.2 semver: ^7.5.2 @@ -25283,6 +25326,15 @@ __metadata: languageName: node linkType: hard +"thingies@npm:^1.20.0": + version: 1.21.0 + resolution: "thingies@npm:1.21.0" + peerDependencies: + tslib: ^2 + checksum: 283a2785e513dc892822dd0bbadaa79e873a7fc90b84798164717bf7cf837553e0b4518d8027b2307d8f6fc6caab088fa717112cd9196c6222763cc3cc1b7e79 + languageName: node + linkType: hard + "throttleit@npm:^1.0.0": version: 1.0.1 resolution: "throttleit@npm:1.0.1" @@ -25495,6 +25547,15 @@ __metadata: languageName: node linkType: hard +"tree-dump@npm:^1.0.1": + version: 1.0.2 + resolution: "tree-dump@npm:1.0.2" + peerDependencies: + tslib: 2 + checksum: 3b0cae6cd74c208da77dac1c65e6a212f5678fe181f1dfffbe05752be188aa88e56d5d5c33f5701d1f603ffcf33403763f722c9e8e398085cde0c0994323cb8d + languageName: node + linkType: hard + "tree-kill@npm:1.2.2, tree-kill@npm:^1.2.2": version: 1.2.2 resolution: "tree-kill@npm:1.2.2" @@ -25872,6 +25933,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.0.0": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28 + languageName: node + linkType: hard + "tsup@npm:^7.2.0": version: 7.2.0 resolution: "tsup@npm:7.2.0" From 486f8ab106be9546b9488ca27e8090cdadcb1f75 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 25 Aug 2024 13:53:16 -0700 Subject: [PATCH 087/204] chore: fix broken tests --- .../convert/cacheStrategy/jsonFile.spec.ts | 4 ++-- .../scripts/src/commands/app-data/convert/index.ts | 5 ++++- .../app-data/convert/processors/base.spec.ts | 14 ++++++++++++-- .../flowParser/parsers/data_list.parser.spec.ts | 1 + .../convert/processors/xlsxWorkbook.spec.ts | 2 +- .../commands/app-data/postProcess/sheets.spec.ts | 7 ++++--- 6 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/cacheStrategy/jsonFile.spec.ts b/packages/scripts/src/commands/app-data/convert/cacheStrategy/jsonFile.spec.ts index c45bb6988..445cf8ae8 100644 --- a/packages/scripts/src/commands/app-data/convert/cacheStrategy/jsonFile.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/cacheStrategy/jsonFile.spec.ts @@ -89,11 +89,11 @@ describe("Json File Cache", () => { // Add entry const data = Math.random(); const { entryName, filePath } = cache.add(data); - expect(existsSync(filePath)).toBeTrue(); + expect(existsSync(filePath)).toEqual(true); expect(cache.get(entryName)).toEqual(data); // Remove entry cache.remove(entryName); - expect(existsSync(filePath)).toBeFalse(); + expect(existsSync(filePath)).toEqual(false); expect(cache.get(entryName)).toBeUndefined(); }); diff --git a/packages/scripts/src/commands/app-data/convert/index.ts b/packages/scripts/src/commands/app-data/convert/index.ts index eb54adcfe..9f5e4142b 100644 --- a/packages/scripts/src/commands/app-data/convert/index.ts +++ b/packages/scripts/src/commands/app-data/convert/index.ts @@ -69,7 +69,10 @@ export class AppDataConverter { cache: JsonFileCache; - constructor(private options: IConverterOptions, testOverrides: Partial = {}) { + constructor( + private options: IConverterOptions, + testOverrides: Partial = {} + ) { console.log(chalk.yellow("App Data Convert")); // optional overrides, used for tests if (testOverrides.version) this.version = testOverrides.version; diff --git a/packages/scripts/src/commands/app-data/convert/processors/base.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/base.spec.ts index 20485e66d..fa79342bf 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/base.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/base.spec.ts @@ -23,9 +23,15 @@ class TestProcessor extends BaseProcessor { } } let processor: TestProcessor; + +/** yarn workspace scripts test -t base.spec.ts */ describe("Base Processor", () => { beforeAll(() => { - processor = new TestProcessor({ namespace: "BaseProcessor", paths }); + processor = new TestProcessor({ + namespace: "BaseProcessor", + paths, + cacheVersion: new Date().getTime(), + }); processor.cache.clear(); }); afterAll(() => { @@ -58,7 +64,11 @@ describe("Deferred Processor", () => { } let deferredProcessor: DeferredProcessor; beforeEach(() => { - deferredProcessor = new DeferredProcessor({ namespace: "BaseProcessor", paths }); + deferredProcessor = new DeferredProcessor({ + namespace: "BaseProcessor", + paths, + cacheVersion: new Date().getTime(), + }); deferredProcessor.cache.clear(); }); afterAll(() => { diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/data_list.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/data_list.parser.spec.ts index 96da0dbd1..dceb9a7ce 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/data_list.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/data_list.parser.spec.ts @@ -15,6 +15,7 @@ const testFlow = { ], }; +/** yarn workspace scripts test -t data_list.parser.spec.ts **/ describe("data_list Parser (single)", () => { let outputRows: any[]; beforeAll(() => { diff --git a/packages/scripts/src/commands/app-data/convert/processors/xlsxWorkbook.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/xlsxWorkbook.spec.ts index 4d3d17a50..fb1e1c026 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/xlsxWorkbook.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/xlsxWorkbook.spec.ts @@ -32,7 +32,7 @@ describe("XLSX Workbook Processor", () => { // an array of arrays it("Converts XLSXs to array of JSON sheet arrays", async () => { const outputs = await processor.process(testInputs); - expect(Array.isArray(outputs)).toBeTrue(); + expect(Array.isArray(outputs)).toEqual(true); expect(outputs.length).toEqual(testInputs.length); // each entry may contain multiple sheets from workbook const testInputsheets = outputs[0]; diff --git a/packages/scripts/src/commands/app-data/postProcess/sheets.spec.ts b/packages/scripts/src/commands/app-data/postProcess/sheets.spec.ts index b29884105..f325d6607 100644 --- a/packages/scripts/src/commands/app-data/postProcess/sheets.spec.ts +++ b/packages/scripts/src/commands/app-data/postProcess/sheets.spec.ts @@ -1,9 +1,6 @@ import { AssetsPostProcessor } from "./assets"; import type { IDeploymentConfigJson } from "../../deployment/common"; -import fs from "fs-extra"; -import mockFs from "mock-fs"; - // Use default imports to allow spying on functions and replacing with mock methods import { ActiveDeployment } from "../../deployment/get"; import path from "path"; @@ -17,3 +14,7 @@ const mockDirs = { }; // TODO +/** yarn workspace scripts test -t sheets.spec.ts */ +describe.skip("PostProcess Sheets", () => { + it.skip("TODO", () => {}); +}); From 49d6fb56dde2f3180c68bf0313a3a329e05e1f9e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 25 Aug 2024 18:07:19 -0700 Subject: [PATCH 088/204] fix: convert spec --- .../commands/app-data/convert/convert.spec.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/convert.spec.ts b/packages/scripts/src/commands/app-data/convert/convert.spec.ts index 6b85bc503..0a764ceca 100644 --- a/packages/scripts/src/commands/app-data/convert/convert.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/convert.spec.ts @@ -1,16 +1,17 @@ import { AppDataConverter } from "./index"; -import path, { resolve } from "path"; +import { resolve } from "path"; import { SCRIPTS_TEST_DATA_DIR } from "../../../paths"; import { emptyDirSync, existsSync, readdirSync, readJSONSync, ensureDirSync } from "fs-extra"; import { clearLogs } from "shared"; // Folders used for tests +// TODO - consider using memfs instead of scripts file cache const paths = { - inputFolders: [path.resolve(SCRIPTS_TEST_DATA_DIR, "input", "sheets")], - outputFolder: path.resolve(SCRIPTS_TEST_DATA_DIR, "output", "sheets"), - cacheFolder: path.resolve(SCRIPTS_TEST_DATA_DIR, "cache"), + inputFolders: [resolve(SCRIPTS_TEST_DATA_DIR, "input", "sheets")], + outputFolder: resolve(SCRIPTS_TEST_DATA_DIR, "output", "sheets"), + cacheFolder: resolve(SCRIPTS_TEST_DATA_DIR, "cache"), }; /** yarn workspace scripts test -t convert.spec.ts */ @@ -19,23 +20,28 @@ describe("App Data Converter", () => { beforeAll(() => { ensureDirSync(paths.outputFolder); emptyDirSync(paths.outputFolder); + ensureDirSync(paths.cacheFolder); + emptyDirSync(paths.cacheFolder); }); beforeEach(() => { converter = new AppDataConverter(paths); }); it("Uses child caches", async () => { + await converter.run(); const cacheFolders = readdirSync(paths.cacheFolder); + // expect contents file and cached conversions expect(cacheFolders.length).toBeGreaterThan(1); }); it("Clears child caches on version change", async () => { - const updatedConverter = new AppDataConverter(paths, { version: -1 }); + const updatedConverter = new AppDataConverter(paths, { version: new Date().getTime() }); + // no need to run the converter, simply creating should clear the cache const cacheFolders = readdirSync(paths.cacheFolder); - expect(cacheFolders.length).toEqual(1); // only contents file + expect(cacheFolders).toHaveLength(1); // only contents file }); - it.only("Processes test_input xlsx without error", async () => { + it("Processes test_input xlsx without error", async () => { const { errors, result } = await converter.run(); - expect(errors.length).toEqual(0); + expect(errors).toHaveLength(0); expect(Object.values(result).length).toBeGreaterThan(0); }); it("Populates output to folder by data type", async () => { @@ -48,7 +54,7 @@ describe("App Data Converter", () => { ...paths, inputFolders: [ ...paths.inputFolders, - path.resolve(SCRIPTS_TEST_DATA_DIR, "input", "sheets_additional"), + resolve(SCRIPTS_TEST_DATA_DIR, "input", "sheets_additional"), ], }); await multipleSourceConverter.run(); @@ -66,9 +72,9 @@ describe("App Data Converter", () => { // Folders used for error tests const errorPaths = { - inputFolders: [path.resolve(SCRIPTS_TEST_DATA_DIR, "input", "errorChecking")], - outputFolder: path.resolve(SCRIPTS_TEST_DATA_DIR, "output", "errorChecking"), - cacheFolder: path.resolve(SCRIPTS_TEST_DATA_DIR, "cache"), + inputFolders: [resolve(SCRIPTS_TEST_DATA_DIR, "input", "errorChecking")], + outputFolder: resolve(SCRIPTS_TEST_DATA_DIR, "output", "errorChecking"), + cacheFolder: resolve(SCRIPTS_TEST_DATA_DIR, "cache"), }; describe("App Data Converter - Error Checking", () => { let errorConverter: AppDataConverter; From e892467b2919e1eea494d6b5e6885519c5b83068 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 25 Aug 2024 18:07:30 -0700 Subject: [PATCH 089/204] chore: add jest lint rules --- packages/scripts/.eslintrc.json | 8 +++ packages/scripts/package.json | 1 + yarn.lock | 99 ++++++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/packages/scripts/.eslintrc.json b/packages/scripts/.eslintrc.json index 8c737ad1c..b35fa3d4e 100644 --- a/packages/scripts/.eslintrc.json +++ b/packages/scripts/.eslintrc.json @@ -2,5 +2,13 @@ "parserOptions": { "sourceType": "module", "ecmaVersion": "latest" + }, + "plugins": ["jest"], + "rules": { + "jest/no-disabled-tests": "warn", + "jest/no-focused-tests": "error", + "jest/no-identical-title": "error", + "jest/prefer-to-have-length": "warn", + "jest/valid-expect": "error" } } diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 83cdd1492..c5536fbd4 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -50,6 +50,7 @@ "@types/jest": "^29.5.12", "@types/node-rsa": "^1.1.1", "@types/semver": "^7.3.9", + "eslint-plugin-jest": "^28.8.0", "jest": "^29.7.0", "memfs": "^4.11.1", "ts-jest": "^29.2.5", diff --git a/yarn.lock b/yarn.lock index 6dcbd99cf..0fcad1599 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8099,6 +8099,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.2.0": + version: 8.2.0 + resolution: "@typescript-eslint/scope-manager@npm:8.2.0" + dependencies: + "@typescript-eslint/types": 8.2.0 + "@typescript-eslint/visitor-keys": 8.2.0 + checksum: c42fdd44bf06fcf0767ebee33b0d9199365066afa43e8f8fe7243c4b6ecb8d9056126df98d5ce771b4ff9f91132974c0348754ee1862cb6d5ae78e6608530650 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/type-utils@npm:5.62.0" @@ -8178,6 +8188,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.2.0": + version: 8.2.0 + resolution: "@typescript-eslint/types@npm:8.2.0" + checksum: 915fd7667308cb3fe3a50bbeb5b7cfa34ece87732a4e1107e6b4afcde64e6885dc3fcae0a0ccc417e90cd55090e4eeccc1310225be8706a58f522a899be8e626 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.57.1": version: 5.57.1 resolution: "@typescript-eslint/typescript-estree@npm:5.57.1" @@ -8252,6 +8269,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.2.0": + version: 8.2.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.2.0" + dependencies: + "@typescript-eslint/types": 8.2.0 + "@typescript-eslint/visitor-keys": 8.2.0 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + minimatch: ^9.0.4 + semver: ^7.6.0 + ts-api-utils: ^1.3.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 9bddd72398d24c5fb1a8c6d0481886928d80e6798ae357778574ac2c8b6c6e18cc32e42865167f0698fede9ad5abbdeced0d0b1b45486cf4eeff7ae30bb5b87d + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" @@ -8304,6 +8340,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0": + version: 8.2.0 + resolution: "@typescript-eslint/utils@npm:8.2.0" + dependencies: + "@eslint-community/eslint-utils": ^4.4.0 + "@typescript-eslint/scope-manager": 8.2.0 + "@typescript-eslint/types": 8.2.0 + "@typescript-eslint/typescript-estree": 8.2.0 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: c3b35fc9de40d94c717fd6e0ce77212d78c4a0377dbdc716d82ce1babeb61891e91e566c9108b336fd74095c810f164ce23eb9adc51471975ffec360e332ecff + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.57.1": version: 5.57.1 resolution: "@typescript-eslint/visitor-keys@npm:5.57.1" @@ -8344,6 +8394,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.2.0": + version: 8.2.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.2.0" + dependencies: + "@typescript-eslint/types": 8.2.0 + eslint-visitor-keys: ^3.4.3 + checksum: 2f701efa1b63bc4141bbe3a38f0f0b51cbdcd3df3d8ca87232ac1d5cf16e957682302da74106952edb87e6435f2ab99e3b4f66103b94a21c2b5aa7d030926f06 + languageName: node + linkType: hard + "@ungap/promise-all-settled@npm:1.1.2": version: 1.1.2 resolution: "@ungap/promise-all-settled@npm:1.1.2" @@ -13550,6 +13610,24 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jest@npm:^28.8.0": + version: 28.8.0 + resolution: "eslint-plugin-jest@npm:28.8.0" + dependencies: + "@typescript-eslint/utils": ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependencies: + "@typescript-eslint/eslint-plugin": ^6.0.0 || ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + jest: "*" + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + jest: + optional: true + checksum: c3b39fbb8a1f3843bd6a5d05215e3c896d439fcb1a9959a1e892184c95da33ad2edd37b1c3a76199803ef78b5e6a9cdc0e67f1ac90405461619fe2d3b8d5a278 + languageName: node + linkType: hard + "eslint-plugin-jsdoc@npm:40.1.1": version: 40.1.1 resolution: "eslint-plugin-jsdoc@npm:40.1.1" @@ -19700,6 +19778,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.4": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: ^2.0.1 + checksum: 2c035575eda1e50623c731ec6c14f65a85296268f749b9337005210bb2b34e2705f8ef1a358b188f69892286ab99dc42c8fb98a57bde55c8d81b3023c19cea28 + languageName: node + linkType: hard + "minimist@npm:^1.1.3, minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" @@ -23502,6 +23589,7 @@ __metadata: commander: ^8.3.0 cordova-res: ^0.15.4 data-models: "workspace:*" + eslint-plugin-jest: ^28.8.0 fs-extra: ^9.0.1 inquirer: ^7.3.3 jest: ^29.7.0 @@ -23611,7 +23699,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.6.3": +"semver@npm:^7.6.0, semver@npm:^7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -25588,6 +25676,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^1.3.0": + version: 1.3.0 + resolution: "ts-api-utils@npm:1.3.0" + peerDependencies: + typescript: ">=4.2.0" + checksum: c746ddabfdffbf16cb0b0db32bb287236a19e583057f8649ee7c49995bb776e1d3ef384685181c11a1a480369e022ca97512cb08c517b2d2bd82c83754c97012 + languageName: node + linkType: hard + "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" From 1fb1f811ee2a7f68c89fa4c60d3d68b2493f7f5e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 25 Aug 2024 18:11:18 -0700 Subject: [PATCH 090/204] chore: update lint rules --- packages/scripts/.eslintrc.json | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/scripts/.eslintrc.json b/packages/scripts/.eslintrc.json index b35fa3d4e..ff2b5af3e 100644 --- a/packages/scripts/.eslintrc.json +++ b/packages/scripts/.eslintrc.json @@ -4,11 +4,12 @@ "ecmaVersion": "latest" }, "plugins": ["jest"], - "rules": { - "jest/no-disabled-tests": "warn", - "jest/no-focused-tests": "error", - "jest/no-identical-title": "error", - "jest/prefer-to-have-length": "warn", - "jest/valid-expect": "error" - } + "overrides": [ + { + "files": ["**/*.spec.ts"], + "plugins": ["jest"], + "extends": ["plugin:jest/recommended"], + "rules": {} + } + ] } From ca021d4609d27fb261ca6aac66b4f67261e517a0 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 25 Aug 2024 20:08:29 -0700 Subject: [PATCH 091/204] chore: code tidying --- .../commands/app-data/convert/convert.spec.ts | 35 +++++++++++-------- .../app-data/convert/processors/base.spec.ts | 13 ++----- .../processors/flowParser/flowParser.spec.ts | 18 ++-------- .../parsers/data_list.parser.spec.ts | 5 +-- .../app-data/postProcess/assets.spec.ts | 8 ++--- .../app-data/postProcess/sheets.spec.ts | 4 +-- packages/scripts/test/helpers/utils.ts | 10 ++++++ packages/scripts/test/setup.ts | 8 +++++ 8 files changed, 52 insertions(+), 49 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/convert.spec.ts b/packages/scripts/src/commands/app-data/convert/convert.spec.ts index 0a764ceca..dca8e8fe7 100644 --- a/packages/scripts/src/commands/app-data/convert/convert.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/convert.spec.ts @@ -2,18 +2,28 @@ import { AppDataConverter } from "./index"; import { resolve } from "path"; -import { SCRIPTS_TEST_DATA_DIR } from "../../../paths"; import { emptyDirSync, existsSync, readdirSync, readJSONSync, ensureDirSync } from "fs-extra"; import { clearLogs } from "shared"; -// Folders used for tests -// TODO - consider using memfs instead of scripts file cache +import { TEST_DATA_PATHS } from "../../../../test/helpers/utils"; +import { ActiveDeployment } from "../../deployment/get"; + +const { SHEETS_CACHE_FOLDER, SHEETS_INPUT_FOLDER, SHEETS_OUTPUT_FOLDER } = TEST_DATA_PATHS; const paths = { - inputFolders: [resolve(SCRIPTS_TEST_DATA_DIR, "input", "sheets")], - outputFolder: resolve(SCRIPTS_TEST_DATA_DIR, "output", "sheets"), - cacheFolder: resolve(SCRIPTS_TEST_DATA_DIR, "cache"), + inputFolders: [resolve(SHEETS_INPUT_FOLDER, "sheets")], + outputFolder: resolve(SHEETS_OUTPUT_FOLDER, "sheets"), + cacheFolder: resolve(SHEETS_CACHE_FOLDER), }; +// HACK - avoid loading active deployment +jest.spyOn(ActiveDeployment, "get").mockReturnValue({ + app_data: { + sheets_filter_function: () => true, + assets_filter_function: () => true, + output_path: paths.outputFolder, + }, +} as any); + /** yarn workspace scripts test -t convert.spec.ts */ describe("App Data Converter", () => { let converter: AppDataConverter; @@ -27,7 +37,7 @@ describe("App Data Converter", () => { converter = new AppDataConverter(paths); }); - it("Uses child caches", async () => { + it.only("Uses child caches", async () => { await converter.run(); const cacheFolders = readdirSync(paths.cacheFolder); // expect contents file and cached conversions @@ -52,10 +62,7 @@ describe("App Data Converter", () => { it("Supports input from multiple source folders", async () => { const multipleSourceConverter = new AppDataConverter({ ...paths, - inputFolders: [ - ...paths.inputFolders, - resolve(SCRIPTS_TEST_DATA_DIR, "input", "sheets_additional"), - ], + inputFolders: [...paths.inputFolders, resolve(SHEETS_INPUT_FOLDER, "sheets_additional")], }); await multipleSourceConverter.run(); const replaceDataListPath = resolve( @@ -72,9 +79,9 @@ describe("App Data Converter", () => { // Folders used for error tests const errorPaths = { - inputFolders: [resolve(SCRIPTS_TEST_DATA_DIR, "input", "errorChecking")], - outputFolder: resolve(SCRIPTS_TEST_DATA_DIR, "output", "errorChecking"), - cacheFolder: resolve(SCRIPTS_TEST_DATA_DIR, "cache"), + inputFolders: [resolve(SHEETS_INPUT_FOLDER, "errorChecking")], + outputFolder: resolve(SHEETS_OUTPUT_FOLDER, "errorChecking"), + cacheFolder: resolve(SHEETS_CACHE_FOLDER), }; describe("App Data Converter - Error Checking", () => { let errorConverter: AppDataConverter; diff --git a/packages/scripts/src/commands/app-data/convert/processors/base.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/base.spec.ts index fa79342bf..49d87018a 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/base.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/base.spec.ts @@ -1,14 +1,7 @@ -import path from "path"; import BaseProcessor from "./base"; -import { SCRIPTS_WORKSPACE_PATH } from "../../../../paths"; import { clearLogs, getLogs } from "../utils"; -const testDataDir = path.resolve(SCRIPTS_WORKSPACE_PATH, "test", "data"); -const paths = { - SHEETS_CACHE_FOLDER: path.resolve(testDataDir, "cache"), - SHEETS_INPUT_FOLDER: path.resolve(testDataDir, "input"), - SHEETS_OUTPUT_FOLDER: path.resolve(testDataDir, "output"), -}; +import { TEST_DATA_PATHS } from "../../../../../test/helpers/utils"; const testData = [ { @@ -29,7 +22,7 @@ describe("Base Processor", () => { beforeAll(() => { processor = new TestProcessor({ namespace: "BaseProcessor", - paths, + paths: TEST_DATA_PATHS, cacheVersion: new Date().getTime(), }); processor.cache.clear(); @@ -66,7 +59,7 @@ describe("Deferred Processor", () => { beforeEach(() => { deferredProcessor = new DeferredProcessor({ namespace: "BaseProcessor", - paths, + paths: TEST_DATA_PATHS, cacheVersion: new Date().getTime(), }); deferredProcessor.cache.clear(); diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.spec.ts index 0acf740a1..4112af899 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/flowParser.spec.ts @@ -1,19 +1,7 @@ -import path from "path"; import { FlowTypes } from "data-models"; -import { SCRIPTS_WORKSPACE_PATH } from "../../../../../paths"; import { clearLogs, getLogs } from "../../utils"; import { FlowParserProcessor } from "./flowParser"; - -const testDataDir = path.resolve(SCRIPTS_WORKSPACE_PATH, "test", "data"); -const paths = { - SHEETS_CACHE_FOLDER: path.resolve(testDataDir, "cache"), - SHEETS_INPUT_FOLDER: path.resolve(testDataDir, "input"), - SHEETS_OUTPUT_FOLDER: path.resolve(testDataDir, "output"), -}; -// Export method to allow use in parser-specific tests (to test on multiple instances of a flow type) -export function getTestFlowParserProcessor() { - return new FlowParserProcessor(paths); -} +import { TEST_DATA_PATHS } from "../../../../../../test/helpers/utils"; // NOTE - inputs are just to test general structure and not run actual parser code const testInputs: FlowTypes.FlowTypeWithData[] = [ @@ -48,7 +36,7 @@ const testInputs: FlowTypes.FlowTypeWithData[] = [ let processor: FlowParserProcessor; describe("FlowParser Processor", () => { beforeAll(() => { - processor = getTestFlowParserProcessor(); + processor = new FlowParserProcessor(TEST_DATA_PATHS); processor.cache.clear(); }); beforeEach(() => { @@ -113,7 +101,7 @@ describe("FlowParser Processor", () => { /** Additional tests for data pipe integration */ describe("FlowParser Processor - Data Pipes", () => { beforeAll(() => { - processor = getTestFlowParserProcessor(); + processor = new FlowParserProcessor(TEST_DATA_PATHS); processor.cache.clear(); }); beforeEach(() => { diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/data_list.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/data_list.parser.spec.ts index dceb9a7ce..ec33ef481 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/data_list.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/data_list.parser.spec.ts @@ -1,5 +1,6 @@ import { DataListParser } from "."; -import { getTestFlowParserProcessor } from "../flowParser.spec"; +import { TEST_DATA_PATHS } from "../../../../../../../test/helpers/utils"; +import { FlowParserProcessor } from "../flowParser"; const testFlow = { flow_type: "data_list", @@ -45,7 +46,7 @@ describe("data_list Parser (single)", () => { }); describe("data_list Parser (multiple)", () => { - const parser = getTestFlowParserProcessor(); + const parser = new FlowParserProcessor(TEST_DATA_PATHS); beforeAll(() => { parser.cache.clear(); }); diff --git a/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts b/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts index 67595835d..8a1c99862 100644 --- a/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts +++ b/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts @@ -9,17 +9,13 @@ import { vol } from "memfs"; // Use default imports to allow spying on functions and replacing with mock methods import { ActiveDeployment } from "../../deployment/get"; -import { posix } from "path"; +import { resolve } from "path"; import { IAssetEntryHashmap } from "data-models/deployment.model"; import { useMockLogger } from "../../../../test/helpers/utils"; // Mock all fs calls to use memfs implementation jest.mock("fs", () => require("memfs")); -// HACK - resolve all paths using posix (/) file system as memfs does not support -// windows paths https://github.com/streamich/memfs/issues/327 -const { resolve } = posix; - /** Mock file system folders for use in tests */ const mockDirs = { appAssets: "mock/app_data/assets", @@ -157,7 +153,7 @@ describe("Assets PostProcess", () => { theme_test: { global: { ...mockFileEntry, filePath: "theme_test/test.jpg" } }, }); }); - it("Populates combined theme and language overrides in any folder order ", () => { + it("Populates combined theme and language overrides in any folder order", () => { mockLocalAssets({ "test1.jpg": mockFile, "test2.jpg": mockFile, diff --git a/packages/scripts/src/commands/app-data/postProcess/sheets.spec.ts b/packages/scripts/src/commands/app-data/postProcess/sheets.spec.ts index f325d6607..625658983 100644 --- a/packages/scripts/src/commands/app-data/postProcess/sheets.spec.ts +++ b/packages/scripts/src/commands/app-data/postProcess/sheets.spec.ts @@ -15,6 +15,6 @@ const mockDirs = { // TODO /** yarn workspace scripts test -t sheets.spec.ts */ -describe.skip("PostProcess Sheets", () => { - it.skip("TODO", () => {}); +describe("PostProcess Sheets", () => { + it.skip("TODO", () => expect(true).toEqual(true)); }); diff --git a/packages/scripts/test/helpers/utils.ts b/packages/scripts/test/helpers/utils.ts index a25740541..29de4769e 100644 --- a/packages/scripts/test/helpers/utils.ts +++ b/packages/scripts/test/helpers/utils.ts @@ -1,3 +1,5 @@ +import { resolve } from "path"; +import { SCRIPTS_WORKSPACE_PATH } from "shared"; import { Logger } from "shared/src/utils/logging/console-logger"; /************************************************************* @@ -18,3 +20,11 @@ export function useMockLogger(callOriginal = true) { } return { error, warning }; } + +const testDataDir = resolve(SCRIPTS_WORKSPACE_PATH, "test", "data"); +/** Common paths used for test data */ +export const TEST_DATA_PATHS = { + SHEETS_CACHE_FOLDER: resolve(testDataDir, "cache"), + SHEETS_INPUT_FOLDER: resolve(testDataDir, "input"), + SHEETS_OUTPUT_FOLDER: resolve(testDataDir, "output"), +}; diff --git a/packages/scripts/test/setup.ts b/packages/scripts/test/setup.ts index f5ce2ca01..3594f74ed 100644 --- a/packages/scripts/test/setup.ts +++ b/packages/scripts/test/setup.ts @@ -1,7 +1,15 @@ import { getGlobalFileLogger } from "shared/src/utils/logging/file-logger"; +import { TEST_DATA_PATHS } from "./helpers/utils"; +import { ensureDirSync, emptyDirSync } from "fs-extra"; export default () => { // Create a logger instance so that parallel test calls don't try to create/empty // directory when initiating for first time getGlobalFileLogger(); + + // Ensure test data folders are cleaned + ensureDirSync(TEST_DATA_PATHS.SHEETS_CACHE_FOLDER); + ensureDirSync(TEST_DATA_PATHS.SHEETS_OUTPUT_FOLDER); + emptyDirSync(TEST_DATA_PATHS.SHEETS_CACHE_FOLDER); + emptyDirSync(TEST_DATA_PATHS.SHEETS_CACHE_FOLDER); }; From 6cb29c8bda98e6882ced33913d35dce11b00106b Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 25 Aug 2024 20:08:40 -0700 Subject: [PATCH 092/204] fix: run tests in series --- packages/scripts/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/scripts/package.json b/packages/scripts/package.json index c5536fbd4..eff6f9ca5 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -11,8 +11,8 @@ "start": "ts-node src/commands/index.ts", "build": "ts-node build.ts", "dev": "ts-node-dev --transpile-only --respawn --watch src src/commands/index.ts", - "test": "jest --", - "test:watch": "jest --watchAll --" + "test": "jest --runInBand --", + "test:watch": "jest --runInBand --watchAll --" }, "author": "", "license": "ISC", From 493cf59bc8c487cad4f50377bdc8f1c5dd4163b9 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 25 Aug 2024 20:24:40 -0700 Subject: [PATCH 093/204] chore: code tidying --- packages/scripts/src/commands/app-data/convert/convert.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scripts/src/commands/app-data/convert/convert.spec.ts b/packages/scripts/src/commands/app-data/convert/convert.spec.ts index dca8e8fe7..8532c8e3e 100644 --- a/packages/scripts/src/commands/app-data/convert/convert.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/convert.spec.ts @@ -37,7 +37,7 @@ describe("App Data Converter", () => { converter = new AppDataConverter(paths); }); - it.only("Uses child caches", async () => { + it("Uses child caches", async () => { await converter.run(); const cacheFolders = readdirSync(paths.cacheFolder); // expect contents file and cached conversions From c9ba940a434c0d6072220f25f2882d2056ec678e Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 27 Aug 2024 10:58:52 +0100 Subject: [PATCH 094/204] chore: make 'wavy' the default variant of progress-path component --- .../components/progress-path/progress-path.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.ts b/src/app/shared/components/template/components/progress-path/progress-path.component.ts index 5e697b091..3610df4c5 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.ts +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.ts @@ -3,7 +3,7 @@ import { TemplateBaseComponent } from "../base"; import { getStringParamFromTemplateRow } from "src/app/shared/utils"; interface IProgressPathParams { - /** TEMPLATE_PARAMETER: "variant" */ + /** TEMPLATE_PARAMETER: "variant". Default "wavy" */ variant: "basic" | "wavy"; } @@ -39,17 +39,17 @@ export class TmplProgressPathComponent extends TemplateBaseComponent implements } private getParams() { - this.params.variant = getStringParamFromTemplateRow(this._row, "variant", "basic") + this.params.variant = getStringParamFromTemplateRow(this._row, "variant", "wavy") .split(",") .join(" ") as IProgressPathParams["variant"]; - this.pathVariant = this.params.variant.includes("wavy") ? "wavy" : "basic"; + this.pathVariant = this.params.variant.includes("basic") ? "basic" : "wavy"; } /** * Generate a base SVG segment used to connect 2 progress items together * Roughly a horizontal line and smooth bend, adjusted for sizing */ - private generateSVGPath(variant: "basic" | "wavy" = "basic") { + private generateSVGPath(variant: "basic" | "wavy" = "wavy") { // arbitrary values used to make base width/height fit const { widthPx, xOffset, yOffset, textContentHeight } = SIZING; From ec7603587e02fbe4072c92de4c1129de89b6f26e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 28 Aug 2024 22:31:04 -0700 Subject: [PATCH 095/204] docs: data generators --- .../docs/authors/advanced/data-generators.md | 101 ++++++++++++++++++ documentation/mkdocs.yml | 1 + 2 files changed, 102 insertions(+) create mode 100644 documentation/docs/authors/advanced/data-generators.md diff --git a/documentation/docs/authors/advanced/data-generators.md b/documentation/docs/authors/advanced/data-generators.md new file mode 100644 index 000000000..195a4a43b --- /dev/null +++ b/documentation/docs/authors/advanced/data-generators.md @@ -0,0 +1,101 @@ +# Data Generators + +As seen in [Looping Data](../looping-data), it is possible to use data_lists as a basis for iterative content loops within a single template. + +Data generators extend this idea further, to create multiple content flows from a single input data_list. The generated outputs can be templates, other data_lists or any other flow type. + +## Defining Generators +A generator minimally requires an `input_data_list` source and a `generator` flow type + +*example content_list* + +| flow_type | flow_name | parameter_list | +| --------- |--------- |-------------- | +| data_list | module_list | | +| generator | module_home_gen | input_data_list: module_list | + +**Input Parameters** + +A full list of available parameters is listed below: + +| parameter | description | default | +| ------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------- | +| input_data_list | Source data_list to loop over for generator | (none) | +| output_flow_type | Specify flow_type to save output as | template | +| output_flow_subtype | Specify flow_subtype to save output as | generated | +| output_flow_name | Specify flow_name to save template as. Can reference data from input data_list | {{flow_name}}_{{@gen.id}} | + +!!! Tip + Each row of the input data_list will be processed to generate an output flow. The `@gen` dynamic prefix can be used to refer to values from within the input list row. + +## Example +In this example we will use start with a list of modules, and use a generator to create individual templates to show content from the module + +### Input + +*module_list* + +| id | title | description | +| --------- |--------- |-------------- | +| module_1 | Welcome | Welcome to the course... | +| module_2 | Let's get started | In this unit we will... | + +*module_home_gen* + +The generator provides a simple template to display the module title and text descriptions, and a button that will later be used for navigation + +| type | name | value | +| ----------- | --------| --------------------- | +| title | | @gen.title | +| text | | @gen.description | +| button | | Continue | + + +### Output +Each row of input data will be processed to produce an output template flow + +*module_home_gen_module_1* + +| type | name | value | +| ----------- | --------| --------------------- | +| title | | Welcome | +| text | | Welcome to the course... | +| button | | Continue | + +*module_home_gen_module_2* + +| type | name | value | +| ----------- | --------| --------------------- | +| title | | Let's get started | +| text | | In this unit we will... | +| button | | Continue | + +We could further develop this example to include images, add actions for the button click, mark content as hidden or conditional on data etc. + +## Special Cases + +**Generate output with dynamic references** +If the generated output includes dynamic references that need to be preserved then it may be necessary to double-wrap the reference in the generator. + +For example, a generator is used to create `data_pipe` flows for further processing data. +References to `@row` local processing must use `{{@row}}` to populate correctly. + +| operation | args_list | +| -------------- | ---------------------------------------------- | +| append_columns | task_child: {@gen.id}_{{@row.id}}_article_tasks; | + + +## Additional Info + + +[Google Sheet Demo](#) - *Not currently available* + + +[Live Preview Demo](#) - *Not currently available* + +See details in original [RFC Proposal Doc](https://docs.google.com/document/d/1cK_Mk3nTZIKxux8bygKvujUFkVENMOE_xIgM6w9PlRw/edit) + + + + + diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 6cfc06109..0719cdda8 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -17,6 +17,7 @@ nav: - authors/feedback.md - Advanced: - authors/advanced/looping-data.md + - authors/advanced/data-generators.md - authors/advanced/overrides.md - authors/local-sheets.md - Contributors: From 307d08ac75cfc3c893b0e2bb5a87aaf8cddb15c2 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 29 Aug 2024 09:42:59 +0100 Subject: [PATCH 096/204] fix: circle task card variant fires click action trigger --- .../template/components/task-card/task-card.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/task-card/task-card.component.html b/src/app/shared/components/template/components/task-card/task-card.component.html index 889c13fb0..3b926810d 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.html +++ b/src/app/shared/components/template/components/task-card/task-card.component.html @@ -77,7 +77,7 @@

} @else { -
+
Date: Thu, 29 Aug 2024 14:35:55 +0100 Subject: [PATCH 097/204] fix: progress bar should not be visible on task-card button variant --- .../template/components/task-card/task-card.component.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/task-card/task-card.component.scss b/src/app/shared/components/template/components/task-card/task-card.component.scss index e533dffb9..67b5b7d63 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.scss +++ b/src/app/shared/components/template/components/task-card/task-card.component.scss @@ -44,6 +44,11 @@ align-items: center; padding: var(--small-padding); min-height: 56px; + + plh-task-progress-bar { + display: none; + } + .content-wrapper { height: 100%; flex-direction: row-reverse; @@ -193,7 +198,7 @@ plh-task-progress-bar { position: absolute; - visibility: none; + display: none; } } .title-wrapper { From fb3b4774fb90ca73eb29fc7c5c72d27f0c0f4ba9 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 29 Aug 2024 15:47:56 +0100 Subject: [PATCH 098/204] fix: progress-path component handles RTL languages --- .../components/progress-path/progress-path.component.html | 5 ++++- .../components/progress-path/progress-path.component.scss | 8 ++++++++ .../components/progress-path/progress-path.component.ts | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.html b/src/app/shared/components/template/components/progress-path/progress-path.component.html index 9cfbcd983..b6784eded 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.html +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.html @@ -17,7 +17,10 @@
-
+
diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.scss b/src/app/shared/components/template/components/progress-path/progress-path.component.scss index 45f87ba71..99b3f87ee 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.scss +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.scss @@ -29,6 +29,10 @@ $path-stroke-width: 28px; stroke: $path-background; stroke-width: $path-stroke-width; } + // For right-to-left languages, flip the path to match the content position + &[data-language-direction~="rtl"] { + transform: scaleX(-1); + } } .progress-path-child-content-wrapper { @@ -53,6 +57,10 @@ $path-stroke-width: 28px; transform: scaleX(-1); margin-left: 0px; margin-right: $path-segment-spacing; + // For right-to-left languages, flip the path to match the content position + &[data-language-direction~="rtl"] { + transform: none; + } } } diff --git a/src/app/shared/components/template/components/progress-path/progress-path.component.ts b/src/app/shared/components/template/components/progress-path/progress-path.component.ts index 3610df4c5..7e07642b1 100644 --- a/src/app/shared/components/template/components/progress-path/progress-path.component.ts +++ b/src/app/shared/components/template/components/progress-path/progress-path.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { TemplateBaseComponent } from "../base"; import { getStringParamFromTemplateRow } from "src/app/shared/utils"; +import { TemplateTranslateService } from "../../services/template-translate.service"; interface IProgressPathParams { /** TEMPLATE_PARAMETER: "variant". Default "wavy" */ @@ -33,6 +34,10 @@ export class TmplProgressPathComponent extends TemplateBaseComponent implements public contentHeight: string; public width = `${SIZING.widthPx}px`; + constructor(public templateTranslateService: TemplateTranslateService) { + super(); + } + ngOnInit() { this.getParams(); this.generateSVGPath(this.pathVariant); From 5d331a8ad98a389e3c1238062c06c0bfdc3d74d4 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 29 Aug 2024 09:01:19 -0700 Subject: [PATCH 099/204] chore: update default node version to 20 --- .github/workflows/deprecated/build-and-upload.yml | 2 +- .github/workflows/deprecated/pr-build.yml | 2 +- .github/workflows/deprecated/sourcemaps-upload.yml | 2 +- .github/workflows/deprecated/test-e2e.yml | 2 +- .github/workflows/documentation-lint.yml | 2 +- .github/workflows/reusable-android-build.yml | 2 +- .github/workflows/reusable-app-build.yml | 2 +- .github/workflows/reusable-content-sync.yml | 2 +- .github/workflows/test-visual.yml | 2 +- .github/workflows/web-build.yml | 2 +- .nvmrc | 2 +- README.md | 4 ++-- documentation/docs/index.md | 4 ++-- packages/actions/templates/app-build/template.yml | 2 +- 14 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deprecated/build-and-upload.yml b/.github/workflows/deprecated/build-and-upload.yml index 8a9debdb5..761bb6faa 100644 --- a/.github/workflows/deprecated/build-and-upload.yml +++ b/.github/workflows/deprecated/build-and-upload.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16] + node-version: [20] steps: - name: Install crcmod diff --git a/.github/workflows/deprecated/pr-build.yml b/.github/workflows/deprecated/pr-build.yml index 115e29aab..d83084062 100644 --- a/.github/workflows/deprecated/pr-build.yml +++ b/.github/workflows/deprecated/pr-build.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16] + node-version: [20] steps: - name: Get PR Number diff --git a/.github/workflows/deprecated/sourcemaps-upload.yml b/.github/workflows/deprecated/sourcemaps-upload.yml index 5410a65c8..23b47fb20 100644 --- a/.github/workflows/deprecated/sourcemaps-upload.yml +++ b/.github/workflows/deprecated/sourcemaps-upload.yml @@ -31,7 +31,7 @@ jobs: - name: Node ${{ matrix.node-version }} uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20.17.0 cache: 'yarn' - name: Populate environment config env: diff --git a/.github/workflows/deprecated/test-e2e.yml b/.github/workflows/deprecated/test-e2e.yml index bc99c7900..b66dece18 100644 --- a/.github/workflows/deprecated/test-e2e.yml +++ b/.github/workflows/deprecated/test-e2e.yml @@ -28,7 +28,7 @@ jobs: - name: Node ${{ matrix.node-version }} uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20.17.0 cache: 'yarn' - name: Populate firebaseConfig.ts env: diff --git a/.github/workflows/documentation-lint.yml b/.github/workflows/documentation-lint.yml index 225d27eff..4ac85a0e7 100644 --- a/.github/workflows/documentation-lint.yml +++ b/.github/workflows/documentation-lint.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20.17.0 cache: 'yarn' - run: yarn install --immutable - name: Spellcheck diff --git a/.github/workflows/reusable-android-build.yml b/.github/workflows/reusable-android-build.yml index 8ef127275..8ac269678 100644 --- a/.github/workflows/reusable-android-build.yml +++ b/.github/workflows/reusable-android-build.yml @@ -85,7 +85,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.17.0 - uses: actions/cache/restore@v3 id: cache with: diff --git a/.github/workflows/reusable-app-build.yml b/.github/workflows/reusable-app-build.yml index 674de36f3..9732a8e09 100644 --- a/.github/workflows/reusable-app-build.yml +++ b/.github/workflows/reusable-app-build.yml @@ -83,7 +83,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.17.0 - name: Cache node modules uses: actions/cache@v3 diff --git a/.github/workflows/reusable-content-sync.yml b/.github/workflows/reusable-content-sync.yml index 531ddc937..bcccc4793 100644 --- a/.github/workflows/reusable-content-sync.yml +++ b/.github/workflows/reusable-content-sync.yml @@ -75,7 +75,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.17.0 - name: Cache node modules uses: actions/cache@v3 diff --git a/.github/workflows/test-visual.yml b/.github/workflows/test-visual.yml index 35b4f688d..f928b80fa 100644 --- a/.github/workflows/test-visual.yml +++ b/.github/workflows/test-visual.yml @@ -35,7 +35,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.17.0 - uses: actions/cache/restore@v3 id: cache with: diff --git a/.github/workflows/web-build.yml b/.github/workflows/web-build.yml index e317a1535..815ba51f7 100644 --- a/.github/workflows/web-build.yml +++ b/.github/workflows/web-build.yml @@ -97,7 +97,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.17.0 ############################################################################# # Node Modules diff --git a/.nvmrc b/.nvmrc index 0828ab794..85aee5a53 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18 \ No newline at end of file +v20 \ No newline at end of file diff --git a/README.md b/README.md index d71c085b8..1293fd2ca 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ 2. Download and install [Git LFS](https://git-lfs.github.com/) This will be used to download any required binary assets, such as images or pdfs -3. Download and install [Node](https://nodejs.org/en/download/) (choose v18.20.4) - This is the programming language required to run the project +3. Download and install [Node](https://nodejs.org/en/download/) + This is the programming language required to run the project. We currently support any of the versions prefixed `v20.x.x` or `v18.x.x` 4. Download and Install [Yarn](https://classic.yarnpkg.com/en/docs/install) This manages all 3rd-party code dependencies diff --git a/documentation/docs/index.md b/documentation/docs/index.md index 50f7fa3b9..457492a9a 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -8,8 +8,8 @@ 2. Download and install [Git LFS](https://git-lfs.github.com/) This will be used to download any required binary assets, such as images or pdfs -3. Download and install [Node](https://nodejs.org/en/download/) (choose v18.20.4) - This is the programming language required to run the project +3. Download and install [Node](https://nodejs.org/en/download/) + This is the programming language required to run the project. We currently support any of the versions prefixed `v20.x.x` or `v18.x.x` 4. Download and Install [Yarn](https://classic.yarnpkg.com/en/docs/install) This manages all 3rd-party code dependencies diff --git a/packages/actions/templates/app-build/template.yml b/packages/actions/templates/app-build/template.yml index 9f0d24a55..d0913ceeb 100644 --- a/packages/actions/templates/app-build/template.yml +++ b/packages/actions/templates/app-build/template.yml @@ -45,7 +45,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.17.0 - name: Cache node modules uses: actions/cache@v3 with: From 446287b5cc6629e1ca26b54ba12043539e323577 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 29 Aug 2024 09:31:14 -0700 Subject: [PATCH 100/204] chore: update list of tests TODOs --- .../processors/flowParser/parsers/default.parser.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts index a15ec3b51..4f88afdb8 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/default.parser.spec.ts @@ -1,7 +1,12 @@ import { DefaultParser } from "./default.parser"; import { FlowTypes } from "data-models"; -import { getTestFlowParserProcessor } from "../flowParser.spec"; +/** + * yarn workspace scripts test -t default.parser.spec.ts + * + * TODO - add tests for rest of functionality, e.g. `@default` syntax, translated fields, + * nested group extract, special field types, `@row` self-reference, metadata fields etc. + */ describe("Default Parser", () => { const parser = new DefaultParser({ processedFlowHashmap: {} } as any); it("Cleans field values - handles strings consisting only of whitespace", () => { From 51e52c5782d0fe8857385f800ba1cd72b9d7c084 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 29 Aug 2024 14:49:57 -0700 Subject: [PATCH 101/204] fix: task-card circle border --- .../template/components/task-card/task-card.component.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/template/components/task-card/task-card.component.scss b/src/app/shared/components/template/components/task-card/task-card.component.scss index 67b5b7d63..a45cc5362 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.scss +++ b/src/app/shared/components/template/components/task-card/task-card.component.scss @@ -153,6 +153,7 @@ .circle-card-wrapper { $circle-width: 100px; + $circle-clip-width: $circle-width / 2 - 4px; width: 100%; display: flex; @@ -191,8 +192,10 @@ border: 1px solid rgba(black, 0.07); filter: drop-shadow(var(--ion-default-box-shadow)); - .img { - width: $circle-width; + img { + width: 100%; + height: 100%; + clip-path: circle($circle-clip-width at center); } } From 4b4222b1b609c8255ce7d41e960a6b435f6178f2 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 29 Aug 2024 15:09:16 -0700 Subject: [PATCH 102/204] fix: task-card border --- .../template/components/task-card/task-card.component.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/shared/components/template/components/task-card/task-card.component.scss b/src/app/shared/components/template/components/task-card/task-card.component.scss index a45cc5362..4cde12d86 100644 --- a/src/app/shared/components/template/components/task-card/task-card.component.scss +++ b/src/app/shared/components/template/components/task-card/task-card.component.scss @@ -153,7 +153,6 @@ .circle-card-wrapper { $circle-width: 100px; - $circle-clip-width: $circle-width / 2 - 4px; width: 100%; display: flex; @@ -195,7 +194,7 @@ img { width: 100%; height: 100%; - clip-path: circle($circle-clip-width at center); + clip-path: circle(#{$circle-width / 2 - 4px} at center); } } From eaf89d03c51e6e4ce3ff2c9265bd70062114c3ea Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 30 Aug 2024 09:09:42 -0700 Subject: [PATCH 103/204] fix: audio player seek --- .../template/components/audio/audio.component.html | 6 +++--- .../template/components/audio/audio.component.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/shared/components/template/components/audio/audio.component.html b/src/app/shared/components/template/components/audio/audio.component.html index a692f46d7..4d7fc3cd4 100644 --- a/src/app/shared/components/template/components/audio/audio.component.html +++ b/src/app/shared/components/template/components/audio/audio.component.html @@ -19,7 +19,7 @@

{{ params.title }}

class="audio-range" max="100" aria-readonly="true" - [value]="progress" + [value]="progress()" (ionChange)="checkChange()" (touchstart)="checkFocus()" (touchend)="seek()" @@ -28,10 +28,10 @@

{{ params.title }}

- {{ +currentTimeSong * 1000 | date : "mm:ss" }} + {{ +currentTimeSong * 1000 | date: "mm:ss" }}
- {{ !player ? "00:00" : (player.duration() * 1000 | date : "mm:ss") }} + {{ !player ? "00:00" : (player.duration() * 1000 | date: "mm:ss") }}
diff --git a/src/app/shared/components/template/components/audio/audio.component.ts b/src/app/shared/components/template/components/audio/audio.component.ts index 68b91f4a0..8fe948fe2 100644 --- a/src/app/shared/components/template/components/audio/audio.component.ts +++ b/src/app/shared/components/template/components/audio/audio.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { Component, Input, OnDestroy, OnInit, signal, ViewChild } from "@angular/core"; import { FlowTypes } from "../../../../model"; import { getBooleanParamFromTemplateRow, @@ -66,7 +66,7 @@ export class TmplAudioComponent /** @ignore */ errorTxt: string | null; /** @ignore */ - progress = 0; + progress = signal(0); /** @ignore */ rangeBarTouched: boolean = false; /** @ignore */ @@ -178,7 +178,7 @@ export class TmplAudioComponent return; } let seek: any = this.player.seek(); - this.progress = (seek / this.player.duration()) * 100 || 0; + this.progress.set((seek / this.player.duration()) * 100 || 0); this.currentTimeSong = this.player.seek() ? (this.player.seek() as any).toString() : "0"; }, 1000); } @@ -199,7 +199,7 @@ export class TmplAudioComponent customUpdateWhenRewind() { if (!this.isPlayed) { let seek: any = this.player.seek(); - this.progress = (seek / this.player.duration()) * 100 || 0; + this.progress.set((seek / this.player.duration()) * 100 || 0); this.currentTimeSong = this.player.seek() ? (this.player.seek() as any).toString() : "0"; } } From a1fc889fc57693e9657e1185fadaee86523762be Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 2 Sep 2024 14:02:03 -0700 Subject: [PATCH 104/204] fix: warning log invalid naming --- packages/scripts/src/commands/app-data/convert/index.ts | 4 ++-- packages/shared/src/utils/logging/file-logger.ts | 2 +- packages/shared/src/utils/logging/memory-logger.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/index.ts b/packages/scripts/src/commands/app-data/convert/index.ts index 9f5e4142b..6852fd2e5 100644 --- a/packages/scripts/src/commands/app-data/convert/index.ts +++ b/packages/scripts/src/commands/app-data/convert/index.ts @@ -153,9 +153,9 @@ export class AppDataConverter { private logOutputs(result: IParsedWorkbookData) { this.writeOutputJsons(result); logSheetsSummary(result); - const warnings = getLogs("warning"); + const warnings = getLogs("warn"); if (warnings.length > 0) { - const warningLogFile = getLogFiles().warning; + const warningLogFile = getLogFiles().warn; logWarning({ msg1: `Completed with ${warnings.length} warnings`, msg2: warningLogFile, diff --git a/packages/shared/src/utils/logging/file-logger.ts b/packages/shared/src/utils/logging/file-logger.ts index ddd9e8513..f1c954a2e 100644 --- a/packages/shared/src/utils/logging/file-logger.ts +++ b/packages/shared/src/utils/logging/file-logger.ts @@ -6,7 +6,7 @@ import { existsSync } from "fs"; import { SCRIPTS_LOGS_DIR } from "../../paths"; import { getGlobalMemoryLoggerTransport } from "./memory-logger"; -const logLevels = ["debug", "info", "warning", "error"] as const; +const logLevels = ["debug", "info", "warn", "error"] as const; type ILogLevel = (typeof logLevels)[number]; /** Retrieve all logs from current session for a given variable */ diff --git a/packages/shared/src/utils/logging/memory-logger.ts b/packages/shared/src/utils/logging/memory-logger.ts index 257280133..7f6fb16a5 100644 --- a/packages/shared/src/utils/logging/memory-logger.ts +++ b/packages/shared/src/utils/logging/memory-logger.ts @@ -1,6 +1,6 @@ import Transport from "winston-transport"; -const logLevels = ["debug", "info", "warning", "error"] as const; +const logLevels = ["debug", "info", "warn", "error"] as const; type ILogLevel = (typeof logLevels)[number]; interface ILogEntry { @@ -29,7 +29,7 @@ export class MemoryLogger extends Transport { debug: [], error: [], info: [], - warning: [], + warn: [], }; } log(entry: ILogEntry, callback) { From b406b80358fccd64daeeb6addbab212a92033d84 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 2 Sep 2024 15:25:37 -0700 Subject: [PATCH 105/204] fix: action cdr --- .../template/services/instance/template-action.service.ts | 4 ++++ .../components/template/template-container.component.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/template/services/instance/template-action.service.ts b/src/app/shared/components/template/services/instance/template-action.service.ts index 6345256db..7f8b8231d 100644 --- a/src/app/shared/components/template/services/instance/template-action.service.ts +++ b/src/app/shared/components/template/services/instance/template-action.service.ts @@ -111,6 +111,10 @@ export class TemplateActionService extends SyncServiceBase { if (!this.container?.parent) { await this.templateNavService.handleNavActionsFromChild(actions, this.container); } + // HACK - ensure components checked for updates after processing + if (this.container?.cdr) { + this.container.cdr.markForCheck(); + } } /** Optional method child component can add to handle post-action callback */ public async handleActionsCallback(actions: FlowTypes.TemplateRowAction[], results: any) {} diff --git a/src/app/shared/components/template/template-container.component.ts b/src/app/shared/components/template/template-container.component.ts index cb53da0dc..1fe517044 100644 --- a/src/app/shared/components/template/template-container.component.ts +++ b/src/app/shared/components/template/template-container.component.ts @@ -61,7 +61,7 @@ export class TemplateContainerComponent implements OnInit, OnDestroy, ITemplateC private componentDestroyed$ = new Subject(); debugMode: boolean; - private get cdr() { + public get cdr() { return this.injector.get(ChangeDetectorRef); } From aaa6aba32d12ed12148fe1538c4228dbe5aa1324 Mon Sep 17 00:00:00 2001 From: Chris Marsh <84872334+ChrisMarsh82@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:26:22 +0100 Subject: [PATCH 106/204] Update reusable-deploy-web-preview.yml Unable to inherit secrets from outside the organisation. Trying to pass them explicitly. --- .github/workflows/reusable-deploy-web-preview.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-deploy-web-preview.yml b/.github/workflows/reusable-deploy-web-preview.yml index f95164d88..81cb8d2f0 100644 --- a/.github/workflows/reusable-deploy-web-preview.yml +++ b/.github/workflows/reusable-deploy-web-preview.yml @@ -36,6 +36,10 @@ on: description: Firebase site for hosting type: string default: ${{vars.FIREBASE_HOSTING_TARGET}} + firebase-service-account: + description: Service account passed from outside organisation + type: string + default: ${{secrets.FIREBASE_SERVICE_ACCOUNT}} jobs: build_action: @@ -101,7 +105,7 @@ jobs: uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" - firebaseServiceAccount: "${{ env.FIREBASE_SERVICE_ACCOUNT }}" + firebaseServiceAccount: "${{ inputs.firebase-service-account }}" projectId: "${{ env.FIREBASE_PROJECT_ID }}" channelId: "${{ env.FIREBASE_HOSTING_CHANNEL }}" target: "${{inputs.firebase-host}}" From e30e704e14aba057f3c04e5d546163d135300ca5 Mon Sep 17 00:00:00 2001 From: Chris Marsh <84872334+ChrisMarsh82@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:52:43 +0100 Subject: [PATCH 107/204] Update reusable-deploy-web-preview.yml --- .github/workflows/reusable-deploy-web-preview.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/reusable-deploy-web-preview.yml b/.github/workflows/reusable-deploy-web-preview.yml index 81cb8d2f0..bd6dabb6c 100644 --- a/.github/workflows/reusable-deploy-web-preview.yml +++ b/.github/workflows/reusable-deploy-web-preview.yml @@ -36,10 +36,9 @@ on: description: Firebase site for hosting type: string default: ${{vars.FIREBASE_HOSTING_TARGET}} - firebase-service-account: - description: Service account passed from outside organisation - type: string - default: ${{secrets.FIREBASE_SERVICE_ACCOUNT}} + secrets: # Declare secrets you expect to receive + FIREBASE_SERVICE_ACCOUNT: + required: true jobs: build_action: @@ -105,7 +104,7 @@ jobs: uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" - firebaseServiceAccount: "${{ inputs.firebase-service-account }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT }}" projectId: "${{ env.FIREBASE_PROJECT_ID }}" channelId: "${{ env.FIREBASE_HOSTING_CHANNEL }}" target: "${{inputs.firebase-host}}" From be33bb56a4cae522b3dd996d7fd697b805081c78 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 6 Sep 2024 11:11:52 -0700 Subject: [PATCH 108/204] feat: gh pages deploy action --- .../reusable-deploy-web-gh-pages.yml | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/reusable-deploy-web-gh-pages.yml diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml new file mode 100644 index 000000000..4e55fa2e7 --- /dev/null +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -0,0 +1,84 @@ +################################################################################## +# About +################################################################################## +# Reuseable workflow to be called from content repos. +# Build and deploy app to github pages +# +# Version : 1.0 +# +################################################################################## +# Configuration +################################################################################## + +# env: + +################################################################################## +# Main Code +################################################################################## +name: Deploy Web GH Pages +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +concurrency: + group: "deploy_web_gh_pages" + cancel-in-progress: true + +on: + workflow_call: + # TODO - remove after testing + push: + branches: feat/gh-pages-deploy-action + +jobs: + build: + uses: ./.github/workflows/reusable-app-build.yml + secrets: inherit + # TODO - remove after testing + with: + build-flags: --configuration "production,preview" + deployment-env: debug + + # Github pages doesn't support single-page-apps, so use post-build to add 404 workaround + # https://github.com/isaacs/github/issues/408 + # https://github.com/orgs/community/discussions/64096 + # TODO - could consider populating individual files for all known template paths (for better SEO) + post_build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Download Build Artifact + uses: actions/download-artifact@v3 + with: + name: www + - name: Extract Build folder + run: | + mkdir www + tar -xf artifact.tar --directory www + - name: Add fallback redirect + run: | + cp www/index.html www/404.html + - name: Upload updated artifact + uses: actions/upload-pages-artifact@v1.0.8 + with: + path: "www/" + name: www + + + deploy: + needs: post_build + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + outputs: + urls: ${{ steps.deploy.outputs.urls }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + +################################################################################## +# Useful Links +################################################################################## +# https://github.com/marketplace/actions/deploy-github-pages-site From 380ce061dcce899d880a6b4b74ce30b39f80b2f0 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 6 Sep 2024 11:19:36 -0700 Subject: [PATCH 109/204] feat: gh-pages deploy action --- .github/workflows/reusable-deploy-web-gh-pages.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index 4e55fa2e7..85fbab1e4 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -23,18 +23,11 @@ concurrency: on: workflow_call: - # TODO - remove after testing - push: - branches: feat/gh-pages-deploy-action jobs: build: uses: ./.github/workflows/reusable-app-build.yml secrets: inherit - # TODO - remove after testing - with: - build-flags: --configuration "production,preview" - deployment-env: debug # Github pages doesn't support single-page-apps, so use post-build to add 404 workaround # https://github.com/isaacs/github/issues/408 From a5c8de88a60848f369270bdbd56a5ace82cfab8d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 6 Sep 2024 11:37:17 -0700 Subject: [PATCH 110/204] fix: action needs --- .github/workflows/reusable-deploy-web-gh-pages.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index 85fbab1e4..4a907904f 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -34,6 +34,7 @@ jobs: # https://github.com/orgs/community/discussions/64096 # TODO - could consider populating individual files for all known template paths (for better SEO) post_build: + needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 From f731d7eed3203fb0573f0a4ce2a2d42e761089ee Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 6 Sep 2024 12:05:56 -0700 Subject: [PATCH 111/204] fix: artifact name --- .github/workflows/reusable-deploy-web-gh-pages.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index 4a907904f..cc17dcaf5 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -52,9 +52,7 @@ jobs: - name: Upload updated artifact uses: actions/upload-pages-artifact@v1.0.8 with: - path: "www/" - name: www - + path: "www/" deploy: needs: post_build From 09e82cb6941ac107f42dacb1e4dd2a550745e28a Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 6 Sep 2024 12:17:53 -0700 Subject: [PATCH 112/204] chore: bump action runner versions --- .github/workflows/reusable-deploy-web-gh-pages.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index cc17dcaf5..46e7e9207 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -37,9 +37,9 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: www - name: Extract Build folder @@ -50,7 +50,7 @@ jobs: run: | cp www/index.html www/404.html - name: Upload updated artifact - uses: actions/upload-pages-artifact@v1.0.8 + uses: actions/upload-pages-artifact@v3 with: path: "www/" From 7be58b8b4d43f9faddc23b3cfa2b7d5879b01e99 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 6 Sep 2024 12:18:03 -0700 Subject: [PATCH 113/204] fix: reusable build cache --- .github/workflows/reusable-app-build.yml | 37 +++++++++++++++--------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/.github/workflows/reusable-app-build.yml b/.github/workflows/reusable-app-build.yml index 674de36f3..934b34820 100644 --- a/.github/workflows/reusable-app-build.yml +++ b/.github/workflows/reusable-app-build.yml @@ -56,21 +56,21 @@ jobs: steps: - name: Check out app code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: "IDEMSInternational/open-app-builder.git" ref: ${{env.APP_CODE_BRANCH}} - name: Checkout parent repo if needed if: env.PARENT_DEPLOYMENT_REPO != '' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: ".idems_app/deployments/${{env.PARENT_DEPLOYMENT_NAME}}" repository: ${{env.PARENT_DEPLOYMENT_REPO}} ref: ${{env.PARENT_DEPLOYMENT_BRANCH}} - name: Checkout deployment - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{inputs.branch}} path: ".idems_app/deployments/${{env.DEPLOYMENT_NAME}}" @@ -81,30 +81,39 @@ jobs: run: echo "${{env.DEPLOYMENT_PRIVATE_KEY}}" > ./.idems_app/deployments/${{env.DEPLOYMENT_NAME}}/encrypted/private.key - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 18.x + node-version: 20.x - - name: Cache node modules - uses: actions/cache@v3 + ############################################################################# + # Node Modules + # Manually restore any previous cache to speed install + # As immutable install will not change cache only save new cache if not hit + # Uses fine-grained methods from https://github.com/actions/cache + ############################################################################# + - uses: actions/cache/restore@v4 + id: cache with: path: ./.yarn/cache - # If cachebusting required (e.g. breaking yarn changes on update) change `v1` to another number - key: ${{ runner.os }}-node-modules-yarn-v1-${{ hashFiles('**/yarn.lock') }} + key: ${{ runner.os }}-node-modules-yarn-v1-${{ hashFiles('yarn.lock') }} restore-keys: | ${{ runner.os }}-node-modules-yarn-v1- - - name: Install node modules - run: yarn install - + run: yarn install --immutable + - uses: actions/cache/save@v4 + if: steps.cache.outputs.cache-hit != 'true' + with: + path: ./.yarn/cache + key: ${{ runner.os }}-node-modules-yarn-v1-${{ hashFiles('yarn.lock') }} + - name: Set deployment run: yarn workflow deployment set $DEPLOYMENT_NAME --skip-refresh - + - name: Build run: yarn build ${{inputs.build-flags}} - name: Upload artifact - uses: actions/upload-pages-artifact@v1.0.8 + uses: actions/upload-pages-artifact@v3 with: path: "www/" name: www From 3868834f305b00922d5282a2c658611fed19139b Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 6 Sep 2024 12:39:54 -0700 Subject: [PATCH 114/204] chore: update config docs --- .github/workflows/reusable-deploy-web-gh-pages.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index 46e7e9207..bb09a40e4 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -8,6 +8,8 @@ # ################################################################################## # Configuration +# Must enable github pages in settings, e.g. +# https://github.com/{org}/{repo}/settings/pages ################################################################################## # env: From d8a2c36eed0c264b92c291bfd3d3bb8970af8693 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 7 Sep 2024 09:44:25 -0700 Subject: [PATCH 115/204] feat: base_href support --- .github/workflows/reusable-deploy-web-gh-pages.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index bb09a40e4..f3cfe1fe6 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -10,9 +10,19 @@ # Configuration # Must enable github pages in settings, e.g. # https://github.com/{org}/{repo}/settings/pages +# +# Variables +# GH_PAGES_BASE - url path that pages deployed to +# If using gh pages domain should be name of repo for page to display at +# https://{org}.github.io/{repo} +# +# If using a custom domain or subdomain can leave use folder name if deployed +# to a child path, e.g. https://my-domain.com/app, or leave blank if not using +# child folder, e.g. https://my-domain.com ################################################################################## -# env: +env: + GH_PAGES_BASE: ${{vars.GH_PAGES_BASE || ''}} ################################################################################## # Main Code @@ -30,6 +40,8 @@ jobs: build: uses: ./.github/workflows/reusable-app-build.yml secrets: inherit + with: + build-flags: --base-href ${{github.env.GH_PAGES_BASE}} # Github pages doesn't support single-page-apps, so use post-build to add 404 workaround # https://github.com/isaacs/github/issues/408 From caab999f9c21f3466867f48010bac5e184fbb887 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 7 Sep 2024 10:26:41 -0700 Subject: [PATCH 116/204] chore: update inputs --- .github/workflows/reusable-deploy-web-gh-pages.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index f3cfe1fe6..dda161c82 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -35,13 +35,20 @@ concurrency: on: workflow_call: + inputs: + GH_PAGES_BASE: + description: 'URL prefix used when deploying to folder path, e.g. /my-repo-name' + default: ${{github.env.GH_PAGES_BASE}} + required: false + type: string + jobs: build: uses: ./.github/workflows/reusable-app-build.yml secrets: inherit with: - build-flags: --base-href ${{github.env.GH_PAGES_BASE}} + build-flags: --base-href ${{inputs.GH_PAGES_BASE}} # Github pages doesn't support single-page-apps, so use post-build to add 404 workaround # https://github.com/isaacs/github/issues/408 From b9a09e354b0629c33dc403d850668a54184d8f8c Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 7 Sep 2024 10:39:33 -0700 Subject: [PATCH 117/204] chore: ci --- .github/workflows/reusable-deploy-web-gh-pages.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index dda161c82..c5a729b91 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -22,7 +22,7 @@ ################################################################################## env: - GH_PAGES_BASE: ${{vars.GH_PAGES_BASE || ''}} + GH_PAGES_BASE: ${{vars.GH_PAGES_BASE || 'test-env'}} ################################################################################## # Main Code @@ -38,7 +38,7 @@ on: inputs: GH_PAGES_BASE: description: 'URL prefix used when deploying to folder path, e.g. /my-repo-name' - default: ${{github.env.GH_PAGES_BASE}} + default: ${{vars.GH_PAGES_BASE || 'test-vars'}} required: false type: string From 8053a5c97a5bd309004fc4071983de872ddc7c79 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 7 Sep 2024 11:01:41 -0700 Subject: [PATCH 118/204] chore: code tidying --- .github/workflows/reusable-deploy-web-gh-pages.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index c5a729b91..29286d737 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -12,17 +12,16 @@ # https://github.com/{org}/{repo}/settings/pages # # Variables -# GH_PAGES_BASE - url path that pages deployed to +# +# GH_PAGES_BASE - url path that pages deployed to. # If using gh pages domain should be name of repo for page to display at -# https://{org}.github.io/{repo} +# https://{org}.github.io/{repo}, e.g. 'my-repo' # # If using a custom domain or subdomain can leave use folder name if deployed # to a child path, e.g. https://my-domain.com/app, or leave blank if not using # child folder, e.g. https://my-domain.com ################################################################################## -env: - GH_PAGES_BASE: ${{vars.GH_PAGES_BASE || 'test-env'}} ################################################################################## # Main Code @@ -38,7 +37,7 @@ on: inputs: GH_PAGES_BASE: description: 'URL prefix used when deploying to folder path, e.g. /my-repo-name' - default: ${{vars.GH_PAGES_BASE || 'test-vars'}} + default: ${{vars.GH_PAGES_BASE || ''}} required: false type: string From 53d9f2f67bd19a34700d152b969768cddd0254d6 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 8 Sep 2024 12:27:19 -0700 Subject: [PATCH 119/204] fix: base-href --- .github/workflows/reusable-deploy-web-gh-pages.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index 29286d737..cdf821944 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -47,7 +47,9 @@ jobs: uses: ./.github/workflows/reusable-app-build.yml secrets: inherit with: - build-flags: --base-href ${{inputs.GH_PAGES_BASE}} + # If base url provided pass to build command with both initial and trailing '/', i.e. /my-repo/ + # https://stackoverflow.com/a/54409380 + build-flags: ${{inputs.GH_PAGES_BASE && format('--base-href /{1}/',inputs.GH_PAGES_BASE) }} # Github pages doesn't support single-page-apps, so use post-build to add 404 workaround # https://github.com/isaacs/github/issues/408 From 2d538871f8e95d02d6c2cc42785f5a38ce3bee55 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 8 Sep 2024 12:32:14 -0700 Subject: [PATCH 120/204] fix: base-href format --- .github/workflows/reusable-deploy-web-gh-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-deploy-web-gh-pages.yml b/.github/workflows/reusable-deploy-web-gh-pages.yml index cdf821944..d87dc133a 100644 --- a/.github/workflows/reusable-deploy-web-gh-pages.yml +++ b/.github/workflows/reusable-deploy-web-gh-pages.yml @@ -49,7 +49,7 @@ jobs: with: # If base url provided pass to build command with both initial and trailing '/', i.e. /my-repo/ # https://stackoverflow.com/a/54409380 - build-flags: ${{inputs.GH_PAGES_BASE && format('--base-href /{1}/',inputs.GH_PAGES_BASE) }} + build-flags: ${{inputs.GH_PAGES_BASE && format('--base-href /{0}/',inputs.GH_PAGES_BASE) }} # Github pages doesn't support single-page-apps, so use post-build to add 404 workaround # https://github.com/isaacs/github/issues/408 From 2c427e67fd84a14b4a5fe514222c903752520f91 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 8 Sep 2024 13:09:59 -0700 Subject: [PATCH 121/204] fix: nested path back and menu buttons --- packages/data-models/appConfig.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/data-models/appConfig.ts b/packages/data-models/appConfig.ts index cdfa5b22c..8555bf476 100644 --- a/packages/data-models/appConfig.ts +++ b/packages/data-models/appConfig.ts @@ -114,9 +114,14 @@ const APP_HEADER_DEFAULTS = { activeRoute(location) === APP_ROUTE_DEFAULTS.home_route, }; -/** Utility function to return the active pathname without any sidebar routing e.g. /home(sidebar:alt) */ +/** + * Utility function to return the active pathname without any sidebar routing e.g. /home(sidebar:alt) + * or basename when deployed to subfolder path, e.g. /my-repo/template/home (provided by in head) + * */ const activeRoute = (location: Location) => { - return location.pathname.replace(/\(.+\)/, ""); + const baseHref = document.querySelector("base")?.getAttribute("href"); + const path = location.pathname.replace(baseHref, "/").replace(/\(.+\)/, ""); + return path; }; const APP_FOOTER_DEFAULTS: { templateName: string | null } = { From 03d1f7be66c16610f50d5d8eb3de4df67f873cbd Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Mon, 9 Sep 2024 09:14:54 +0100 Subject: [PATCH 122/204] ci: bump version to 0.16.35 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33d320d5a..826ec67f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "0.16.34", + "version": "0.16.35", "author": "IDEMS International", "license": "See LICENSE", "homepage": "https://idems.international/", From 5a35901d64a3fefd3b40c850b10cf3c3f4c50007 Mon Sep 17 00:00:00 2001 From: Chris Marsh <84872334+ChrisMarsh82@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:24:14 +0100 Subject: [PATCH 123/204] Create CNAME --- documentation/CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 documentation/CNAME diff --git a/documentation/CNAME b/documentation/CNAME new file mode 100644 index 000000000..a1cd0f212 --- /dev/null +++ b/documentation/CNAME @@ -0,0 +1 @@ +open-app-builder.org From 3a80c5259caa23567c3935f8f5b416ecafccc621 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 10 Sep 2024 14:58:50 +0100 Subject: [PATCH 124/204] chore: update comment in toggle-bar component code to reflect default styling on web --- .../components/template/components/toggle-bar/toggle-bar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts index c9d037718..79195b7c3 100644 --- a/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts +++ b/src/app/shared/components/template/components/toggle-bar/toggle-bar.ts @@ -10,7 +10,7 @@ import { interface IToggleParams { /** * TEMPLATE PARAMETER: "variant". Setting "ios" or "android" will style the toggle to match the respective - * platform, otherwise the default is to match the current device platform, using "ios" on web. + * platform, otherwise the default is to match the current device platform, using "android" on web. * */ variant: "" | "icon" | "in_button" | "ios" | "android"; /** TEMPLATE PARAMETER: "style". Legacy, use "variant" instead. */ From 839e456d775348e74b671a88075d053532f36da9 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 10 Sep 2024 13:55:39 -0700 Subject: [PATCH 125/204] refactor: separate core and runtime deployment config types --- packages/data-models/deployment.model.ts | 102 ++++++++++++----------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 6d02bcf98..0de56a00b 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -4,10 +4,60 @@ import type { IAppConfig } from "./appConfig"; /** Update version to force recompile next time deployment set (e.g. after default config update) */ export const DEPLOYMENT_CONFIG_VERSION = 20240314.0; -export interface IDeploymentConfig { +/** Configuration settings available to runtime application */ +export interface IDeploymentRuntimeConfig { + api: { + /** Name of target db for api operations. Default `plh` */ + db_name?: string; + /** + * Target endpoint for api. Default `https://apps-server.idems.international/api` + * Will be replaced when running locally as per `src\app\shared\services\server\interceptors.ts` + * */ + endpoint?: string; + }; + /** Optional override of any provided constants from data-models/constants */ + app_config: IAppConfig; + /** 3rd party integration for logging services */ + error_logging?: { + /** sentry/glitchtip logging dsn */ + dsn: string; + }; + /** + * Specify if using firebase for auth and crashlytics. + * Requires firebase config available through encrypted config */ + firebase: { + /** Project config as specified in firebase console (recommend loading from encrypted environment) */ + config?: { + apiKey: string; + authDomain: string; + databaseURL: string; + projectId: string; + storageBucket: string; + messagingSenderId: string; + appId: string; + measurementId: string; + }; + auth: { + /** Enables `auth` actions to allow user sign-in/out */ + enabled: boolean; + }; + crashlytics: { + /** Enables app crash reports to firebase crashlytics */ + enabled: boolean; + }; + }; + /** 3rd party integration for remote asset storage and sync */ + supabase: { + enabled: boolean; + url?: string; + publicApiKey?: string; + }; +} + +/** Deployment settings not available at runtime */ +interface IDeploymentCoreConfig { /** Friendly name used to identify the deployment name */ name: string; - google_drive: { /** @deprecated Use `sheets_folder_ids` array instead */ sheets_folder_id?: string; @@ -41,17 +91,6 @@ export interface IDeploymentConfig { icon_asset_foreground_path?: string; icon_asset_background_path?: string; }; - api: { - /** Name of target db for api operations. Default `plh` */ - db_name?: string; - /** - * Target endpoint for api. Default `https://apps-server.idems.international/api` - * Will be replaced when running locally as per `src\app\shared\services\server\interceptors.ts` - * */ - endpoint?: string; - }; - /** Optional override of any provided constants from data-models/constants */ - app_config: IAppConfig; app_data: { /** Folder to populate processed content. Default `./app_data` */ output_path: string; @@ -60,30 +99,6 @@ export interface IDeploymentConfig { /** filter function that receives basic file info such as relativePath and size. Default `(fileEntry)=>true`*/ assets_filter_function: (fileEntry: IContentsEntry) => boolean; }; - /** - * Specify if using firebase for auth and crashlytics. - * Requires firebase config available through encrypted config */ - firebase: { - /** Project config as specified in firebase console (recommend loading from encrypted environment) */ - config?: { - apiKey: string; - authDomain: string; - databaseURL: string; - projectId: string; - storageBucket: string; - messagingSenderId: string; - appId: string; - measurementId: string; - }; - auth: { - /** Enables `auth` actions to allow user sign-in/out */ - enabled: boolean; - }; - crashlytics: { - /** Enables app crash reports to firebase crashlytics */ - enabled: boolean; - }; - }; git: { /** Url of external git repo to store content */ content_repo?: string; @@ -96,12 +111,6 @@ export interface IDeploymentConfig { /** App Store app name, e.g. "Example App" */ app_name?: string; }; - /** 3rd party integration for remote asset storage and sync */ - supabase: { - enabled: boolean; - url?: string; - publicApiKey?: string; - }; translations: { /** List of all language codes to include. Default null (includes all) */ filter_language_codes?: string[]; @@ -120,11 +129,6 @@ export interface IDeploymentConfig { /** path for task working directory. Default `./tasks` */ task_cache_path: string; }; - /** 3rd party integration for logging services */ - error_logging?: { - /** sentry/glitchtip logging dsn */ - dsn: string; - }; /** track whether deployment processed from default config */ _validated: boolean; /** version number added from scripts to recompile on core changes */ @@ -133,6 +137,8 @@ export interface IDeploymentConfig { _parent_config?: Partial; } +export type IDeploymentConfig = IDeploymentCoreConfig & IDeploymentRuntimeConfig; + /** Deployment with additional metadata when set as active deployment */ export interface IDeploymentConfigJson extends IDeploymentConfig { _workspace_path: string; From 5dc4c7df7eb39a5e2e2f3a9beb762ee564c93c5c Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 10 Sep 2024 13:56:26 -0700 Subject: [PATCH 126/204] feat: copy deployment runtime config to app --- cspell.config.yml | 1 + .../scripts/src/tasks/providers/appData.ts | 25 +++++++++--- packages/shared/src/utils/file-utils.ts | 39 +++++++++++++++---- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/cspell.config.yml b/cspell.config.yml index c821848a7..991142f97 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -31,6 +31,7 @@ "rxdb", "sidemenu", "sourcemaps", + "supabase", "swiper", "templatename", "tmpl", diff --git a/packages/scripts/src/tasks/providers/appData.ts b/packages/scripts/src/tasks/providers/appData.ts index dc2dca96f..cf5e70aa3 100644 --- a/packages/scripts/src/tasks/providers/appData.ts +++ b/packages/scripts/src/tasks/providers/appData.ts @@ -4,6 +4,7 @@ import { parseCommand } from "../../commands"; import { WorkflowRunner } from "../../commands/workflow/run"; import { SRC_ASSETS_PATH } from "../../paths"; import { IContentsEntry, replicateDir } from "../../utils"; +import { IDeploymentConfigJson, IDeploymentRuntimeConfig } from "data-models"; /** Prepare sourcely cached assets for population to app */ const postProcessAssets = async (options: { sourceAssetsFolders: string[] }) => { @@ -30,24 +31,36 @@ const postProcessSheets = async (options: { */ const copyDeploymentDataToApp = () => { const { app_data } = WorkflowRunner.config; - const copiedFolders = ["assets", "sheets", "translations"]; - // omit index files + + // copy filtered subset of app_data + const copiedFolders = ["assets", "sheets", "translations", "config.json"]; const filter_fn = (entry: IContentsEntry) => { const [baseDir] = entry.relativePath.split("/"); return copiedFolders.includes(baseDir); }; - const source = app_data.output_path; - const target = path.resolve(SRC_ASSETS_PATH, "app_data"); - replicateDir(source, target, { filter_fn }); + // copy folders + const sourceFolder = app_data.output_path; + const targetFolder = path.resolve(SRC_ASSETS_PATH, "app_data"); + replicateDir(sourceFolder, targetFolder, { filter_fn }); // HACK - Angular webpack won't always live-reload when changes only made to asset files // so write an arbitrary variable that can be imported into the app and will trigger reload // https://github.com/angular/angular-cli/issues/22751 // https://github.com/webpack/webpack-dev-server/issues/3794 writeFileSync( - path.resolve(target, "index.ts"), + path.resolve(targetFolder, "index.ts"), `export const DEV_COMPILE_TIME = ${new Date().getTime()}` ); + + // write runtime deployment config + const configTarget = path.resolve(targetFolder, "deployment.json"); + const runtimeConfig = generateRuntimeConfig(WorkflowRunner.config); + writeFileSync(configTarget, JSON.stringify(runtimeConfig, null, 2)); }; +function generateRuntimeConfig(deploymentConfig: IDeploymentConfigJson): IDeploymentRuntimeConfig { + const { api, app_config, firebase, supabase, error_logging } = deploymentConfig; + return { api, app_config, firebase, supabase, error_logging }; +} + export default { postProcessAssets, postProcessSheets, copyDeploymentDataToApp }; diff --git a/packages/shared/src/utils/file-utils.ts b/packages/shared/src/utils/file-utils.ts index fb2fafe00..6cc01cd1a 100644 --- a/packages/shared/src/utils/file-utils.ts +++ b/packages/shared/src/utils/file-utils.ts @@ -2,7 +2,7 @@ import * as fs from "fs-extra"; import * as path from "path"; import * as os from "os"; import { createHash, randomUUID } from "crypto"; -import { logWarning } from "./logging.utils"; +import { Logger, logWarning } from "./logging.utils"; import { tmpdir } from "os"; /** @@ -149,12 +149,7 @@ export function generateFolderFlatMap( const relativePath = path.relative(folderPath, filePath).split(path.sep).join("/"); const shouldInclude = options.filterFn ? options.filterFn(relativePath) : true; if (shouldInclude) { - // generate size and md5 checksum stats - const { size, mtime } = fs.statSync(filePath); - const modifiedTime = mtime.toISOString(); - // write size in kb to 1 dpclear - const size_kb = Math.round(size / 102.4) / 10; - const md5Checksum = getFileMD5Checksum(filePath); + const { md5Checksum, modifiedTime, size_kb } = getFileStats(filePath); const entry: IContentsEntry = { relativePath, size_kb, md5Checksum, modifiedTime }; if (options.includeLocalPath) { entry.localPath = filePath; @@ -165,6 +160,16 @@ export function generateFolderFlatMap( return flatMap; } +function getFileStats(filePath: string) { + // generate size and md5 checksum stats + const { size, mtime } = fs.statSync(filePath); + const modifiedTime = mtime.toISOString(); + // write size in kb to 1 dpclear + const size_kb = Math.round(size / 102.4) / 10; + const md5Checksum = getFileMD5Checksum(filePath); + return { size_kb, md5Checksum, modifiedTime }; +} + export interface IContentsEntry { relativePath: string; size_kb: number; @@ -480,6 +485,26 @@ export function replicateDir( return ops; } +/** Copy a file from src to target, only replacing if unchanged */ +export function replicateFile(src: string, target: string) { + const srcExists = fs.pathExistsSync(src); + if (!srcExists) return Logger.error({ msg1: "File not found", msg2: src }); + const srcStats = getFileStats(src); + // skip if target file same contents as src + if (fs.pathExistsSync(target)) { + const targetStats = getFileStats(target); + if (srcStats.md5Checksum === targetStats.md5Checksum) { + return; + } + } + // copy from src to target + fs.ensureDir(path.dirname(target)); + fs.ensureDirSync(path.dirname(target)); + fs.copyFileSync(src, target); + const mtime = new Date(srcStats.modifiedTime); + fs.utimesSync(target, mtime, mtime); +} + /** * Copy all files from src to target folder, overriding target files with src * and keeping original modified times From 273b939050d3ef39b2191b425ef9dce579d02813 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 10 Sep 2024 14:46:44 -0700 Subject: [PATCH 127/204] chore: update deployment models --- packages/data-models/deployment.model.ts | 45 ++++++++++++------- .../scripts/src/tasks/providers/appData.ts | 15 ++++++- packages/scripts/tsconfig.json | 1 + 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 0de56a00b..06d083c76 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -2,10 +2,15 @@ import type { IGdriveEntry } from "../@idemsInternational/gdrive-tools"; import type { IAppConfig } from "./appConfig"; /** Update version to force recompile next time deployment set (e.g. after default config update) */ -export const DEPLOYMENT_CONFIG_VERSION = 20240314.0; +export const DEPLOYMENT_CONFIG_VERSION = 20240910.0; /** Configuration settings available to runtime application */ export interface IDeploymentRuntimeConfig { + /** version of open-app-builder used to compile */ + _core_version: string; + /** tag of content version provided by content git repo*/ + _content_version: string; + api: { /** Name of target db for api operations. Default `plh` */ db_name?: string; @@ -46,6 +51,8 @@ export interface IDeploymentRuntimeConfig { enabled: boolean; }; }; + /** Friendly name used to identify the deployment name */ + name: string; /** 3rd party integration for remote asset storage and sync */ supabase: { enabled: boolean; @@ -56,8 +63,6 @@ export interface IDeploymentRuntimeConfig { /** Deployment settings not available at runtime */ interface IDeploymentCoreConfig { - /** Friendly name used to identify the deployment name */ - name: string; google_drive: { /** @deprecated Use `sheets_folder_ids` array instead */ sheets_folder_id?: string; @@ -146,8 +151,29 @@ export interface IDeploymentConfigJson extends IDeploymentConfig { _config_version: number; } +export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { + _content_version: "", + _core_version: "", + name: "", + api: { + db_name: "plh", + endpoint: "https://apps-server.idems.international/api", + }, + app_config: {} as any, // populated by `getDefaultAppConstants()`, + + firebase: { + config: null, + auth: { enabled: false }, + crashlytics: { enabled: true }, + }, + supabase: { + enabled: false, + }, +}; + /** Full example of just all config once merged with defaults */ export const DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS: IDeploymentConfig = { + ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, name: "Full Config Example", google_drive: { assets_folder_id: "", @@ -157,11 +183,6 @@ export const DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS: IDeploymentConfig = { assets_filter_function: (gdriveEntry) => true, }, android: {}, - api: { - db_name: "plh", - endpoint: "https://apps-server.idems.international/api", - }, - app_config: {} as any, // populated by `getDefaultAppConstants()`, local_drive: { assets_path: "./assets", sheets_path: "./sheets", @@ -171,15 +192,7 @@ export const DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS: IDeploymentConfig = { sheets_filter_function: (flow) => true, assets_filter_function: (fileEntry) => true, }, - firebase: { - config: null, - auth: { enabled: false }, - crashlytics: { enabled: true }, - }, ios: {}, - supabase: { - enabled: false, - }, translations: { filter_language_codes: null, source_strings_path: "./app_data/translations_source/source_strings", diff --git a/packages/scripts/src/tasks/providers/appData.ts b/packages/scripts/src/tasks/providers/appData.ts index cf5e70aa3..368f2f34f 100644 --- a/packages/scripts/src/tasks/providers/appData.ts +++ b/packages/scripts/src/tasks/providers/appData.ts @@ -1,5 +1,6 @@ import { writeFileSync } from "fs-extra"; import path from "path"; +import packageJSON from "../../../../../package.json"; import { parseCommand } from "../../commands"; import { WorkflowRunner } from "../../commands/workflow/run"; import { SRC_ASSETS_PATH } from "../../paths"; @@ -59,8 +60,18 @@ const copyDeploymentDataToApp = () => { }; function generateRuntimeConfig(deploymentConfig: IDeploymentConfigJson): IDeploymentRuntimeConfig { - const { api, app_config, firebase, supabase, error_logging } = deploymentConfig; - return { api, app_config, firebase, supabase, error_logging }; + const { api, app_config, firebase, supabase, error_logging, git, name } = deploymentConfig; + + return { + api, + app_config, + firebase, + supabase, + error_logging, + _core_version: packageJSON.version, + _content_version: git.content_tag_latest || "", + name, + }; } export default { postProcessAssets, postProcessSheets, copyDeploymentDataToApp }; diff --git a/packages/scripts/tsconfig.json b/packages/scripts/tsconfig.json index 07a90aef3..40d3fa25f 100644 --- a/packages/scripts/tsconfig.json +++ b/packages/scripts/tsconfig.json @@ -7,6 +7,7 @@ "noEmit": true, "moduleResolution": "node", "module": "CommonJS", + "resolveJsonModule": true, "esModuleInterop": true, // Global type definitions. "typeRoots": ["./node_modules/@types"], From 9b53dc494a3de9fe9453bf64b08647f7a6319151 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 10 Sep 2024 16:05:54 -0700 Subject: [PATCH 128/204] feat: add deployment service and tests --- .../deployment/deployment.service.spec.ts | 43 ++++++++++++++++++ .../services/deployment/deployment.service.ts | 44 +++++++++++++++++++ src/test/utils.ts | 20 +++++++++ 3 files changed, 107 insertions(+) create mode 100644 src/app/shared/services/deployment/deployment.service.spec.ts create mode 100644 src/app/shared/services/deployment/deployment.service.ts create mode 100644 src/test/utils.ts diff --git a/src/app/shared/services/deployment/deployment.service.spec.ts b/src/app/shared/services/deployment/deployment.service.spec.ts new file mode 100644 index 000000000..05638f13d --- /dev/null +++ b/src/app/shared/services/deployment/deployment.service.spec.ts @@ -0,0 +1,43 @@ +import { DeploymentService } from "./deployment.service"; +import { HttpClient, HttpErrorResponse } from "@angular/common/http"; +import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "packages/data-models"; +import { asyncData, asyncError } from "src/test/utils"; + +const mockConfig: IDeploymentRuntimeConfig = { + ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, + name: "test", +}; + +/** + * Call standalone tests via: + * yarn ng test --include src/app/shared/services/deployment/deployment.service.spec.ts + */ +describe("Deployment Service", () => { + let service: DeploymentService; + let httpClientSpy: jasmine.SpyObj; + + beforeEach(async () => { + // NOTE - prefer use of spy to `HttpTestingController` as allows to specify responses + // in advance of request (controller must be called after start of init but before complete) + httpClientSpy = jasmine.createSpyObj("HttpClient", ["get"]); + service = new DeploymentService(httpClientSpy); + }); + + it("Loads deployment from assets json", async () => { + httpClientSpy.get.and.returnValue(asyncData(mockConfig)); + await service.ready(); + expect(service.config().name).toEqual(mockConfig.name); + }); + + it("Handles handles missing deployment json", async () => { + const errorResponse = new HttpErrorResponse({ + error: "test 404 error", + status: 404, + statusText: "Not Found", + }); + httpClientSpy.get.and.returnValue(asyncError(errorResponse)); + await service.ready(); + expect(service.config().name).toEqual(DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS.name); + // TODO - could also consider check that logger gets called + }); +}); diff --git a/src/app/shared/services/deployment/deployment.service.ts b/src/app/shared/services/deployment/deployment.service.ts new file mode 100644 index 000000000..5a5e4b58c --- /dev/null +++ b/src/app/shared/services/deployment/deployment.service.ts @@ -0,0 +1,44 @@ +import { Injectable, signal } from "@angular/core"; +import { AsyncServiceBase } from "../asyncService.base"; +import { HttpClient } from "@angular/common/http"; +import { catchError, map, of, firstValueFrom } from "rxjs"; +import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "packages/data-models"; + +@Injectable({ providedIn: "root" }) +/** + * Deployment runtime config settings + * + * NOTE - this is populated as a blocking service during init, + * so no need to await service initialisation before access + */ +export class DeploymentService extends AsyncServiceBase { + constructor(private http: HttpClient) { + super("Deployment Service"); + this.registerInitFunction(this.initialise); + } + + /** Private writeable config to allow population from JSON */ + private _config = signal(DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS); + + /** Read-only access to deployment runtime config */ + public config = this._config.asReadonly(); + + /** Load active deployment configuration from JSON file */ + private async initialise() { + const deployment = await firstValueFrom(this.loadDeployment()); + if (deployment) { + this._config.set(deployment); + } + } + + private loadDeployment() { + return this.http.get("assets/deployment.json").pipe( + catchError(() => { + console.warn("No deployment config available"); + return of(null); + }), + // HACK - json config converts functions to strings, not strongly typed + map((v) => v as IDeploymentRuntimeConfig) + ); + } +} diff --git a/src/test/utils.ts b/src/test/utils.ts new file mode 100644 index 000000000..00dee05b2 --- /dev/null +++ b/src/test/utils.ts @@ -0,0 +1,20 @@ +import { defer } from "rxjs"; + +// Utils referenced in v17 angular docs, copied from +// https://stackblitz.com/edit/spec-has-no-expectations?file=src%2Ftesting%2Fasync-observable-helpers.ts + +/** + * Create async observable that emits-once and completes + * after a JS engine turn + */ +export function asyncData(data: T) { + return defer(() => Promise.resolve(data)); +} + +/** + * Create async observable error that errors + * after a JS engine turn + */ +export function asyncError(errorObject: any) { + return defer(() => Promise.reject(errorObject)); +} From eb37f8dbd279e3b7bb915b174765998613a7cb1d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 10 Sep 2024 16:23:37 -0700 Subject: [PATCH 129/204] chore: code tidying --- packages/data-models/deployment.model.ts | 4 ++-- packages/scripts/src/tasks/providers/appData.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 06d083c76..bd093f1ef 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -7,7 +7,7 @@ export const DEPLOYMENT_CONFIG_VERSION = 20240910.0; /** Configuration settings available to runtime application */ export interface IDeploymentRuntimeConfig { /** version of open-app-builder used to compile */ - _core_version: string; + _app_builder_version: string; /** tag of content version provided by content git repo*/ _content_version: string; @@ -153,7 +153,7 @@ export interface IDeploymentConfigJson extends IDeploymentConfig { export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { _content_version: "", - _core_version: "", + _app_builder_version: "", name: "", api: { db_name: "plh", diff --git a/packages/scripts/src/tasks/providers/appData.ts b/packages/scripts/src/tasks/providers/appData.ts index 368f2f34f..39bb30d5d 100644 --- a/packages/scripts/src/tasks/providers/appData.ts +++ b/packages/scripts/src/tasks/providers/appData.ts @@ -68,7 +68,7 @@ function generateRuntimeConfig(deploymentConfig: IDeploymentConfigJson): IDeploy firebase, supabase, error_logging, - _core_version: packageJSON.version, + _app_builder_version: packageJSON.version, _content_version: git.content_tag_latest || "", name, }; From 597a37b162b171b100b6861cb5fae8d2fb9aabfd Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 10 Sep 2024 16:31:40 -0700 Subject: [PATCH 130/204] chore: code tidying --- packages/scripts/src/tasks/providers/appData.ts | 2 +- src/app/shared/services/deployment/deployment.service.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/scripts/src/tasks/providers/appData.ts b/packages/scripts/src/tasks/providers/appData.ts index 39bb30d5d..bb26fc59a 100644 --- a/packages/scripts/src/tasks/providers/appData.ts +++ b/packages/scripts/src/tasks/providers/appData.ts @@ -34,7 +34,7 @@ const copyDeploymentDataToApp = () => { const { app_data } = WorkflowRunner.config; // copy filtered subset of app_data - const copiedFolders = ["assets", "sheets", "translations", "config.json"]; + const copiedFolders = ["assets", "sheets", "translations"]; const filter_fn = (entry: IContentsEntry) => { const [baseDir] = entry.relativePath.split("/"); return copiedFolders.includes(baseDir); diff --git a/src/app/shared/services/deployment/deployment.service.ts b/src/app/shared/services/deployment/deployment.service.ts index 5a5e4b58c..e0b0a5110 100644 --- a/src/app/shared/services/deployment/deployment.service.ts +++ b/src/app/shared/services/deployment/deployment.service.ts @@ -37,7 +37,6 @@ export class DeploymentService extends AsyncServiceBase { console.warn("No deployment config available"); return of(null); }), - // HACK - json config converts functions to strings, not strongly typed map((v) => v as IDeploymentRuntimeConfig) ); } From 923b3fa9854953e1bf6a4b4cac477d4ea5bd1168 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 11 Sep 2024 12:46:26 +0100 Subject: [PATCH 131/204] docs: add instructions for avoiding folder path issues --- README.md | 3 +++ documentation/docs/index.md | 3 +++ documentation/requirements.txt | Bin 1026 -> 1030 bytes 3 files changed, 6 insertions(+) diff --git a/README.md b/README.md index d71c085b8..42aef700a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ ## Installation +### Choosing a location to download the repo +Before running the commands, make sure you’re in the correct folder (the "current working directory") where you want to save the repo locally. You can use the `pwd` command to print the current directory, and `cd` to change the directory. You should ensure that there are no spaces at any point in the current working directory path. For example, if your top level user folder contains a space in the name, consider downloading the repo to a folder located outside of the user folder, e.g. in a new top-level `C:/apps` folder on Windows. + ### Download the repo with binary assets ``` git lfs clone https://github.com/IDEMSInternational/open-app-builder.git diff --git a/documentation/docs/index.md b/documentation/docs/index.md index 50f7fa3b9..29f5a3ffb 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -16,6 +16,9 @@ ## Installation +### Choosing a location to download the repo +Before running the commands, make sure you’re in the correct folder (the "current working directory") where you want to save the repo locally. You can use the `pwd` command to print the current directory, and `cd` to change the directory. You should ensure that there are no spaces at any point in the current working directory path. For example, if your top level user folder contains a space in the name, consider downloading the repo to a folder located outside of the user folder, e.g. in a new top-level `C:/apps` folder on Windows. + ### Download the repo with binary assets ``` git lfs clone https://github.com/IDEMSInternational/open-app-builder.git diff --git a/documentation/requirements.txt b/documentation/requirements.txt index d4fb5e9f13619fd7714eda6bd6f5cf2e40268ed2..2aaabae9de6b6a9e9708b63c73086ed53cc6cf1b 100644 GIT binary patch delta 15 WcmZqTXye!*z|5@2U^JPZ`7;0=Tm#nt delta 11 ScmZqUXyVu)z&u%m`4a#Wt^;@g From 80604ecf5b55cfe9e168304b0d275830d4dd46e0 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 11 Sep 2024 13:10:41 +0100 Subject: [PATCH 132/204] docs: styling of folder path instructions --- README.md | 3 +++ documentation/docs/index.md | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 42aef700a..e779b071f 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,12 @@ ## Installation ### Choosing a location to download the repo + Before running the commands, make sure you’re in the correct folder (the "current working directory") where you want to save the repo locally. You can use the `pwd` command to print the current directory, and `cd` to change the directory. You should ensure that there are no spaces at any point in the current working directory path. For example, if your top level user folder contains a space in the name, consider downloading the repo to a folder located outside of the user folder, e.g. in a new top-level `C:/apps` folder on Windows. ### Download the repo with binary assets + +To download the repo into the current working directory, run: ``` git lfs clone https://github.com/IDEMSInternational/open-app-builder.git ``` diff --git a/documentation/docs/index.md b/documentation/docs/index.md index 29f5a3ffb..bc02cf966 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -16,10 +16,12 @@ ## Installation -### Choosing a location to download the repo -Before running the commands, make sure you’re in the correct folder (the "current working directory") where you want to save the repo locally. You can use the `pwd` command to print the current directory, and `cd` to change the directory. You should ensure that there are no spaces at any point in the current working directory path. For example, if your top level user folder contains a space in the name, consider downloading the repo to a folder located outside of the user folder, e.g. in a new top-level `C:/apps` folder on Windows. - ### Download the repo with binary assets +!!! tip "Choosing a location to download the repo" + + Before running the commands, make sure you’re in the correct folder (the "current working directory") where you want to save the repo locally. You can use the `pwd` command to print the current directory, and `cd` to change the directory. You should ensure that there are no spaces at any point in the current working directory path. For example, if your top level user folder contains a space in the name, consider downloading the repo to a folder located outside of the user folder, e.g. in a new top-level `C:/apps` folder on Windows. + +To download the repo into the current working directory, run: ``` git lfs clone https://github.com/IDEMSInternational/open-app-builder.git ``` From 109cad74a9e6c56e8d29593320243e9872467d23 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 11 Sep 2024 14:14:46 +0100 Subject: [PATCH 133/204] docs: update python command in running-server instructions --- documentation/docs/contributors/documentation/running-server.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/contributors/documentation/running-server.md b/documentation/docs/contributors/documentation/running-server.md index 21ae227fe..a6212ea6e 100644 --- a/documentation/docs/contributors/documentation/running-server.md +++ b/documentation/docs/contributors/documentation/running-server.md @@ -21,7 +21,7 @@ The scripts below will create a python [virtual environment](https://docs.python ```sh linenums="1" cd documentation - python -m venv .venv + python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt mkdocs serve From 427de76824856baadbb4f56b15a233bfeacd0f10 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 10:17:24 -0700 Subject: [PATCH 134/204] chore: code tidying --- packages/shared/src/utils/file-utils.ts | 2 -- src/app/shared/services/deployment/deployment.service.spec.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/shared/src/utils/file-utils.ts b/packages/shared/src/utils/file-utils.ts index 6cc01cd1a..3d47250f4 100644 --- a/packages/shared/src/utils/file-utils.ts +++ b/packages/shared/src/utils/file-utils.ts @@ -497,8 +497,6 @@ export function replicateFile(src: string, target: string) { return; } } - // copy from src to target - fs.ensureDir(path.dirname(target)); fs.ensureDirSync(path.dirname(target)); fs.copyFileSync(src, target); const mtime = new Date(srcStats.modifiedTime); diff --git a/src/app/shared/services/deployment/deployment.service.spec.ts b/src/app/shared/services/deployment/deployment.service.spec.ts index 05638f13d..693b2ef1f 100644 --- a/src/app/shared/services/deployment/deployment.service.spec.ts +++ b/src/app/shared/services/deployment/deployment.service.spec.ts @@ -29,7 +29,7 @@ describe("Deployment Service", () => { expect(service.config().name).toEqual(mockConfig.name); }); - it("Handles handles missing deployment json", async () => { + it("Handles missing deployment json", async () => { const errorResponse = new HttpErrorResponse({ error: "test 404 error", status: 404, From b262128acc11efc9b22a113c252852ac7754f755 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 12:02:45 -0700 Subject: [PATCH 135/204] refactor: runtime deployment services --- src/app/app.component.html | 21 +++++++++++-------- src/app/app.component.ts | 20 +++++++++++------- src/app/feature/feedback/feedback.service.ts | 10 +++++---- .../services/app-config/app-config.service.ts | 14 ++++++------- src/app/shared/services/auth/auth.service.ts | 7 ++++--- .../crashlytics/crashlytics.service.ts | 6 +++--- src/app/shared/services/db/db-sync.service.ts | 7 +++++-- .../dynamic-data/adapters/persistedMemory.ts | 7 ++++--- .../dynamic-data/adapters/reactiveMemory.ts | 6 ++++-- .../file-manager/file-manager.service.ts | 7 ++++--- .../services/firebase/firebase.service.ts | 8 +++---- .../remote-asset/remote-asset.service.ts | 7 ++++--- .../shared/services/server/server.service.ts | 7 +++++-- src/environments/environment.prod.ts | 1 - src/environments/environment.ts | 1 - 15 files changed, 74 insertions(+), 55 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index f6931026e..50f45c6c0 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -16,16 +16,19 @@ {{ sideMenuDefaults.title }}
- - - {{ CONTENT_VERSION }} - - - {{ APP_VERSION }} - + @if (sideMenuDefaults.should_show_version) { + @if (deploymentService.config()._content_version; as CONTENT_VERSION) { + + + {{ CONTENT_VERSION }} + + + } @else { + {{ this.deploymentService.config()._app_builder_version }} + } + } ({{ DEPLOYMENT_NAME }})({{ this.deploymentService.config().name }})
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index bcbf94702..27e8a8a98 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -41,6 +41,7 @@ import { SeoService } from "./shared/services/seo/seo.service"; import { FeedbackService } from "./feature/feedback/feedback.service"; 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"; @Component({ selector: "app-root", @@ -48,15 +49,13 @@ import { LocalStorageService } from "./shared/services/local-storage/local-stora styleUrls: ["app.component.scss"], }) export class AppComponent { - APP_VERSION = environment.version; - CONTENT_VERSION = environment.deploymentConfig.git.content_tag_latest; - DEPLOYMENT_NAME = environment.deploymentName; appConfig: IAppConfig; appAuthenticationDefaults: IAppConfig["APP_AUTHENTICATION_DEFAULTS"]; sideMenuDefaults: IAppConfig["APP_SIDEMENU_DEFAULTS"]; footerDefaults: IAppConfig["APP_FOOTER_DEFAULTS"]; /** Track when app ready to render sidebar and route templates */ public renderAppTemplates = false; + /** * A space-separated list of values, hierarchically representing the current platform, * e.g. on iPhone the value would be "mobile ios iphone". @@ -66,6 +65,9 @@ export class AppComponent { platforms: string; constructor( + // Public services (for UI) + public deploymentService: DeploymentService, + // 3rd Party Services private platform: Platform, private cdr: ChangeDetectorRef, @@ -111,6 +113,9 @@ export class AppComponent { } private async initializeApp() { + // eagerly load deployment config to make available throughout app + await this.deploymentService.ready(); + this.platform.ready().then(async () => { this.platforms = this.platform.platforms().join(" "); this.subscribeToAppConfigChanges(); @@ -148,9 +153,10 @@ export class AppComponent { } /** Populate contact fields that may be used by other services during initialisation */ private async populateAppInitFields() { - this.localStorageService.setProtected("DEPLOYMENT_NAME", this.DEPLOYMENT_NAME); - this.localStorageService.setProtected("APP_VERSION", this.APP_VERSION); - this.localStorageService.setProtected("CONTENT_VERSION", this.CONTENT_VERSION); + const { _content_version, _app_builder_version, name } = this.deploymentService.config(); + this.localStorageService.setProtected("DEPLOYMENT_NAME", name); + this.localStorageService.setProtected("APP_VERSION", _app_builder_version); + this.localStorageService.setProtected("CONTENT_VERSION", _content_version); // HACK - ensure first_app_launch migrated from event service if (!this.localStorageService.getProtected("APP_FIRST_LAUNCH")) { await this.appEventService.ready(); @@ -164,7 +170,7 @@ export class AppComponent { * Currently only run on native where specified (but can comment out for testing locally) */ private async loadAuthConfig() { - const { firebase } = environment.deploymentConfig; + const { firebase } = this.deploymentService.config(); const { enforceLogin } = this.appAuthenticationDefaults; const ensureLogin = firebase.config && enforceLogin && Capacitor.isNativePlatform(); if (ensureLogin) { diff --git a/src/app/feature/feedback/feedback.service.ts b/src/app/feature/feedback/feedback.service.ts index afcb4b49b..693629798 100644 --- a/src/app/feature/feedback/feedback.service.ts +++ b/src/app/feature/feedback/feedback.service.ts @@ -16,7 +16,6 @@ import { import { UserMetaService } from "src/app/shared/services/userMeta/userMeta.service"; import { TemplateService } from "src/app/shared/components/template/services/template.service"; import { generateTimestamp } from "src/app/shared/utils"; -import { environment } from "src/environments/environment"; import { DbService } from "src/app/shared/services/db/db.service"; import { DBSyncService } from "src/app/shared/services/db/db-sync.service"; import { @@ -38,6 +37,7 @@ import { } from "src/app/shared/components/template/services/instance/template-action.registry"; import { SyncServiceBase } from "src/app/shared/services/syncService.base"; import { LocalStorageService } from "src/app/shared/services/local-storage/local-storage.service"; +import { DeploymentService } from "src/app/shared/services/deployment/deployment.service"; @Injectable({ providedIn: "root", @@ -80,7 +80,8 @@ export class FeedbackService extends SyncServiceBase { private router: Router, private themeService: ThemeService, private skinService: SkinService, - private templateActionRegistry: TemplateActionRegistry + private templateActionRegistry: TemplateActionRegistry, + private deploymentService: DeploymentService ) { super("Feedback"); this.subscribeToAppConfigChanges(); @@ -367,13 +368,14 @@ export class FeedbackService extends SyncServiceBase { * device info and user uuid */ public generateFeedbackMetadata() { + const { _app_builder_version, name } = this.deploymentService.config(); const metadata: IFeedbackMetadata = { deviceInfo: this.deviceInfo, pathname: location.pathname, uuid: this.userMetaService.getUserMeta("uuid"), timestamp: generateTimestamp(), - app_version: environment.version, - app_deployment_name: environment.deploymentName, + app_version: _app_builder_version, + app_deployment_name: name, app_theme: this.themeService.getCurrentTheme(), app_skin: this.skinService.getActiveSkinName(), }; diff --git a/src/app/shared/services/app-config/app-config.service.ts b/src/app/shared/services/app-config/app-config.service.ts index 25a8a3ea6..25052fe36 100644 --- a/src/app/shared/services/app-config/app-config.service.ts +++ b/src/app/shared/services/app-config/app-config.service.ts @@ -1,18 +1,17 @@ import { Injectable } from "@angular/core"; import { getDefaultAppConfig, IAppConfig, IAppConfigOverride } from "data-models"; import { BehaviorSubject } from "rxjs"; -import { environment } from "src/environments/environment"; import { deepMergeObjects, RecursivePartial, trackObservableObjectChanges } from "../../utils"; import clone from "clone"; import { SyncServiceBase } from "../syncService.base"; import { startWith } from "rxjs/operators"; import { Observable } from "rxjs"; +import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root", }) export class AppConfigService extends SyncServiceBase { - deploymentOverrides: IAppConfigOverride = (environment.deploymentConfig as any).app_config || {}; /** List of constants provided by data-models combined with deployment-specific overrides and skin-specific overrides */ appConfig$ = new BehaviorSubject(undefined as any); @@ -42,19 +41,18 @@ export class AppConfigService extends SyncServiceBase { return this.changes$.pipe(startWith(this.value)); } - constructor() { + constructor(private deploymentService: DeploymentService) { super("AppConfig"); this.initialise(); } private initialise() { + const deploymentOverrides: IAppConfigOverride = + this.deploymentService.config().app_config || {}; this.APP_CONFIG = getDefaultAppConfig(); // Store app config with deployment overrides applied, to be merged with additional overrides when applied - this.deploymentAppConfig = this.applyAppConfigOverrides( - this.APP_CONFIG, - this.deploymentOverrides - ); - this.updateAppConfig(this.deploymentOverrides); + this.deploymentAppConfig = this.applyAppConfigOverrides(this.APP_CONFIG, deploymentOverrides); + this.updateAppConfig(deploymentOverrides); } public updateAppConfig(overrides: IAppConfigOverride) { diff --git a/src/app/shared/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts index 2f85b1ae3..3b65d589d 100644 --- a/src/app/shared/services/auth/auth.service.ts +++ b/src/app/shared/services/auth/auth.service.ts @@ -2,11 +2,11 @@ import { Injectable } from "@angular/core"; import { FirebaseAuthentication, User } from "@capacitor-firebase/authentication"; import { BehaviorSubject, firstValueFrom } from "rxjs"; import { filter } from "rxjs/operators"; -import { environment } from "src/environments/environment"; import { SyncServiceBase } from "../syncService.base"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; import { FirebaseService } from "../firebase/firebase.service"; import { LocalStorageService } from "../local-storage/local-storage.service"; +import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root", @@ -18,13 +18,14 @@ export class AuthService extends SyncServiceBase { constructor( private templateActionRegistry: TemplateActionRegistry, private firebaseService: FirebaseService, - private localStorageService: LocalStorageService + private localStorageService: LocalStorageService, + private deploymentService: DeploymentService ) { super("Auth"); this.initialise(); } private initialise() { - const { firebase } = environment.deploymentConfig; + const { firebase } = this.deploymentService.config(); if (firebase?.auth?.enabled && this.firebaseService.app) { this.addAuthListeners(); this.registerTemplateActionHandlers(); diff --git a/src/app/shared/services/crashlytics/crashlytics.service.ts b/src/app/shared/services/crashlytics/crashlytics.service.ts index 9e6b2f0ad..ac5616d97 100644 --- a/src/app/shared/services/crashlytics/crashlytics.service.ts +++ b/src/app/shared/services/crashlytics/crashlytics.service.ts @@ -3,7 +3,7 @@ import { FirebaseCrashlytics } from "@capacitor-firebase/crashlytics"; import { Capacitor } from "@capacitor/core"; import { Device } from "@capacitor/device"; import { AsyncServiceBase } from "../asyncService.base"; -import { environment } from "src/environments/environment"; +import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root", @@ -14,13 +14,13 @@ import { environment } from "src/environments/environment"; * https://github.com/capawesome-team/capacitor-firebase/tree/main/packages/crashlytics */ export class CrashlyticsService extends AsyncServiceBase { - constructor() { + constructor(private deploymentService: DeploymentService) { super("Crashlytics"); this.registerInitFunction(this.initialise); } private async initialise() { if (Capacitor.isNativePlatform()) { - const { firebase } = environment.deploymentConfig; + const { firebase } = this.deploymentService.config(); // Crashlytics is still supported on native device without firebase config (uses google-services.json) // so use config property to toggle enabled instead await this.setEnabled({ enabled: firebase?.crashlytics?.enabled }); diff --git a/src/app/shared/services/db/db-sync.service.ts b/src/app/shared/services/db/db-sync.service.ts index dd6f3e8e2..e5110e185 100644 --- a/src/app/shared/services/db/db-sync.service.ts +++ b/src/app/shared/services/db/db-sync.service.ts @@ -14,6 +14,7 @@ import { AppConfigService } from "../app-config/app-config.service"; import { AsyncServiceBase } from "../asyncService.base"; import { UserMetaService } from "../userMeta/userMeta.service"; import { DbService } from "./db.service"; +import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root" }) /** @@ -29,7 +30,8 @@ export class DBSyncService extends AsyncServiceBase { private dbService: DbService, private http: HttpClient, private userMetaService: UserMetaService, - private appConfigService: AppConfigService + private appConfigService: AppConfigService, + private deploymentService: DeploymentService ) { super("DB Sync"); this.registerInitFunction(this.inititialise); @@ -81,12 +83,13 @@ export class DBSyncService extends AsyncServiceBase { /** Populate common app_meta to local record */ private generateServerRecord(record: any, mapping: IDBServerMapping) { + const { name } = this.deploymentService.config(); const { is_user_record, user_record_id_field } = mapping; if (is_user_record && user_record_id_field) { const serverRecord: IDBServerUserRecord = { app_user_id: this.userMetaService.getUserMeta("uuid"), app_user_record_id: record[user_record_id_field], - app_deployment_name: environment.deploymentName, + app_deployment_name: name, app_version: environment.version, data: record, }; diff --git a/src/app/shared/services/dynamic-data/adapters/persistedMemory.ts b/src/app/shared/services/dynamic-data/adapters/persistedMemory.ts index 73d78a4c2..bdd93774b 100644 --- a/src/app/shared/services/dynamic-data/adapters/persistedMemory.ts +++ b/src/app/shared/services/dynamic-data/adapters/persistedMemory.ts @@ -17,8 +17,8 @@ addRxPlugin(RxDBUpdatePlugin); import { debounceTime, filter, firstValueFrom, Subject } from "rxjs"; import { FlowTypes } from "data-models"; -import { environment } from "src/environments/environment"; import { deepMergeObjects, compareObjectKeys } from "../../../utils"; +import { DeploymentService } from "../../deployment/deployment.service"; /** * All persisted docs are stored in the same format with a standard set of meta fields and doc data @@ -78,7 +78,7 @@ export class PersistedMemoryAdapter { [key: string]: RxCollection; }>; - constructor() { + constructor(private deploymentService: DeploymentService) { this.subscribeToStatePersist(); } @@ -98,8 +98,9 @@ export class PersistedMemoryAdapter { } public async create() { + const { name } = this.deploymentService.config(); this.db = await createRxDatabase({ - name: `${environment.deploymentName}_user`, + name: `${name}_user`, storage: getRxStorageDexie({ autoOpen: true }), ignoreDuplicate: true, }); diff --git a/src/app/shared/services/dynamic-data/adapters/reactiveMemory.ts b/src/app/shared/services/dynamic-data/adapters/reactiveMemory.ts index 1d788dd47..4085f0912 100644 --- a/src/app/shared/services/dynamic-data/adapters/reactiveMemory.ts +++ b/src/app/shared/services/dynamic-data/adapters/reactiveMemory.ts @@ -22,7 +22,7 @@ import { RxDBUpdatePlugin } from "rxdb/plugins/update"; addRxPlugin(RxDBUpdatePlugin); import { BehaviorSubject } from "rxjs"; -import { environment } from "src/environments/environment"; +import { DeploymentService } from "../../deployment/deployment.service"; /** * Create a base schema for data @@ -67,10 +67,12 @@ export class ReactiveMemoryAdapater { MemoryStorageInternals, RxStorageMemoryInstanceCreationOptions >; + constructor(private deploymentService: DeploymentService) {} public async createDB() { + const { name } = this.deploymentService.config(); this.db = await createRxDatabase({ - name: `${environment.deploymentName}`, + name, storage: getRxStorageMemory(), ignoreDuplicate: true, }); diff --git a/src/app/shared/services/file-manager/file-manager.service.ts b/src/app/shared/services/file-manager/file-manager.service.ts index 6e59d18e0..d9582ae73 100644 --- a/src/app/shared/services/file-manager/file-manager.service.ts +++ b/src/app/shared/services/file-manager/file-manager.service.ts @@ -5,10 +5,10 @@ import { Capacitor } from "@capacitor/core"; import write_blob from "capacitor-blob-writer"; import { saveAs } from "file-saver"; import { SyncServiceBase } from "../syncService.base"; -import { environment } from "src/environments/environment"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; import { TemplateAssetService } from "../../components/template/services/template-asset.service"; import { ErrorHandlerService } from "../error-handler/error-handler.service"; +import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root", @@ -19,14 +19,15 @@ export class FileManagerService extends SyncServiceBase { constructor( private errorHandler: ErrorHandlerService, private templateActionRegistry: TemplateActionRegistry, - private templateAssetService: TemplateAssetService + private templateAssetService: TemplateAssetService, + private deploymentService: DeploymentService ) { super("FileManager"); this.initialise(); } private initialise() { - this.cacheName = environment.deploymentConfig.name; + this.cacheName = this.deploymentService.config().name; this.registerTemplateActionHandlers(); } diff --git a/src/app/shared/services/firebase/firebase.service.ts b/src/app/shared/services/firebase/firebase.service.ts index e17f6097c..39fddb5a5 100644 --- a/src/app/shared/services/firebase/firebase.service.ts +++ b/src/app/shared/services/firebase/firebase.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { initializeApp, FirebaseApp } from "firebase/app"; -import { environment } from "src/environments/environment"; import { SyncServiceBase } from "../syncService.base"; +import { DeploymentService } from "../deployment/deployment.service"; /** Service used to configure initialize firebase app core configuration */ @Injectable({ providedIn: "root" }) @@ -9,7 +9,7 @@ export class FirebaseService extends SyncServiceBase { /** Initialised firebase app. Will be undefined if firebase config unavailable */ app: FirebaseApp | undefined; - constructor() { + constructor(private deploymentService: DeploymentService) { super("Firebase"); this.initialise(); } @@ -18,7 +18,7 @@ export class FirebaseService extends SyncServiceBase { * Configure app module imports dependent on what firebase features should be enabled */ private initialise() { - const { firebase } = environment.deploymentConfig; + const { firebase } = this.deploymentService.config(); // Check if any services are enabled, simply return if not const enabledServices = Object.entries(firebase) @@ -32,6 +32,6 @@ export class FirebaseService extends SyncServiceBase { return; } - this.app = initializeApp(environment.deploymentConfig.firebase.config); + this.app = initializeApp(firebase.config); } } diff --git a/src/app/shared/services/remote-asset/remote-asset.service.ts b/src/app/shared/services/remote-asset/remote-asset.service.ts index 1bd645f0f..fddff64d3 100644 --- a/src/app/shared/services/remote-asset/remote-asset.service.ts +++ b/src/app/shared/services/remote-asset/remote-asset.service.ts @@ -3,7 +3,6 @@ import { HttpClient, HttpEventType } from "@angular/common/http"; import { Capacitor } from "@capacitor/core"; import { createClient, SupabaseClient } from "@supabase/supabase-js"; import { FileObject } from "@supabase/storage-js"; -import { environment } from "src/environments/environment"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; import { FlowTypes, IAppConfig } from "../../model"; import { AppConfigService } from "../app-config/app-config.service"; @@ -16,6 +15,7 @@ import { AsyncServiceBase } from "../asyncService.base"; import { IAssetEntry } from "packages/data-models/deployment.model"; import { DynamicDataService } from "../dynamic-data/dynamic-data.service"; import { arrayToHashmap, convertBlobToBase64 } from "../../utils"; +import { DeploymentService } from "../deployment/deployment.service"; const CORE_ASSET_PACK_NAME = "core_assets"; @@ -39,7 +39,8 @@ export class RemoteAssetService extends AsyncServiceBase { private fileManagerService: FileManagerService, private templateAssetService: TemplateAssetService, private templateActionRegistry: TemplateActionRegistry, - private http: HttpClient + private http: HttpClient, + private deploymentService: DeploymentService ) { super("RemoteAsset"); this.registerInitFunction(this.initialise); @@ -48,7 +49,7 @@ export class RemoteAssetService extends AsyncServiceBase { private async initialise() { this.registerTemplateActionHandlers(); // require supabase to be configured to use remote asset service - const { enabled, publicApiKey, url } = environment.deploymentConfig.supabase; + const { enabled, publicApiKey, url } = this.deploymentService.config().supabase; this.supabaseEnabled = enabled; if (this.supabaseEnabled) { await this.ensureAsyncServicesReady([this.templateAssetService, this.dynamicDataService]); diff --git a/src/app/shared/services/server/server.service.ts b/src/app/shared/services/server/server.service.ts index 0edd99802..42df07250 100644 --- a/src/app/shared/services/server/server.service.ts +++ b/src/app/shared/services/server/server.service.ts @@ -10,6 +10,7 @@ import { AppConfigService } from "../app-config/app-config.service"; import { SyncServiceBase } from "../syncService.base"; import { LocalStorageService } from "../local-storage/local-storage.service"; import { DynamicDataService } from "../dynamic-data/dynamic-data.service"; +import { DeploymentService } from "../deployment/deployment.service"; /** * Backend API @@ -31,7 +32,8 @@ export class ServerService extends SyncServiceBase { private http: HttpClient, private appConfigService: AppConfigService, private localStorageService: LocalStorageService, - private dynamicDataService: DynamicDataService + private dynamicDataService: DynamicDataService, + private deploymentService: DeploymentService ) { super("Server"); this.initialise(); @@ -57,6 +59,7 @@ export class ServerService extends SyncServiceBase { } public async syncUserData() { + const { name } = this.deploymentService.config(); await this.dynamicDataService.ready(); if (!this.device_info) { this.device_info = await Device.getInfo(); @@ -78,7 +81,7 @@ export class ServerService extends SyncServiceBase { contact_fields, app_version: environment.version, device_info: this.device_info, - app_deployment_name: environment.deploymentName, + app_deployment_name: name, dynamic_data, }; console.log("[SERVER] sync data", data); diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 161077181..b7e290ad6 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -4,7 +4,6 @@ import type { IDeploymentConfig } from "data-models"; export const environment = { version: packageJson.version, - deploymentName: deploymentJson.name, // HACK - json config converts functions to strings, not strongly typed deploymentConfig: deploymentJson as any as IDeploymentConfig, production: true, diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 27059abb4..9ac4ab5a0 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -5,7 +5,6 @@ import type { IDeploymentConfig } from "data-models"; export const environment = { /** App version, as provided by package.json */ version: packageJson.version, - deploymentName: deploymentJson.name, // HACK - json config converts functions to strings, not strongly typed deploymentConfig: deploymentJson as any as IDeploymentConfig, production: false, From 5b017f6aec3ffa11951097fb3179f2a2137e675e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 12:04:53 -0700 Subject: [PATCH 136/204] chore: update deployment runtime web config --- packages/data-models/deployment.model.ts | 10 +++++----- src/app/shared/services/seo/seo.service.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index bd093f1ef..165763c02 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -59,6 +59,10 @@ export interface IDeploymentRuntimeConfig { url?: string; publicApiKey?: string; }; + web: { + /** Relative path of custom favicon asset to load from app_data assets */ + favicon_asset?: string; + }; } /** Deployment settings not available at runtime */ @@ -124,10 +128,6 @@ interface IDeploymentCoreConfig { /** translated string for import. Default `./app_data/translations_source/translated_strings */ translated_strings_path?: string; }; - web: { - /** Relative path of custom favicon asset to load from app_data assets */ - favicon_asset?: string; - }; workflows: { /** path to custom workflow files to include */ custom_ts_files: string[]; @@ -169,6 +169,7 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { supabase: { enabled: false, }, + web: {}, }; /** Full example of just all config once merged with defaults */ @@ -198,7 +199,6 @@ export const DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS: IDeploymentConfig = { source_strings_path: "./app_data/translations_source/source_strings", translated_strings_path: "./app_data/translations_source/translated_strings", }, - web: {}, workflows: { custom_ts_files: [], task_cache_path: "./tasks", diff --git a/src/app/shared/services/seo/seo.service.ts b/src/app/shared/services/seo/seo.service.ts index 61a20b907..95e120414 100644 --- a/src/app/shared/services/seo/seo.service.ts +++ b/src/app/shared/services/seo/seo.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; -import { environment } from "src/environments/environment"; import { SyncServiceBase } from "../syncService.base"; +import { DeploymentService } from "../deployment/deployment.service"; interface ISEOMeta { title: string; @@ -21,7 +21,7 @@ type IMetaName = providedIn: "root", }) export class SeoService extends SyncServiceBase { - constructor() { + constructor(private deploymentService: DeploymentService) { super("SEO Service"); // call after init to apply defaults this.updateMeta({}); @@ -65,7 +65,7 @@ export class SeoService extends SyncServiceBase { private getDefaultSEOTags(): ISEOMeta { const PUBLIC_URL = location.origin; let faviconUrl = `${PUBLIC_URL}/assets/icon/favicon.svg`; - const { web, app_config } = environment.deploymentConfig; + const { web, app_config } = this.deploymentService.config(); if (web?.favicon_asset) { faviconUrl = `${PUBLIC_URL}/assets/app_data/assets/${web.favicon_asset}`; } From 06746ad8556841791854c49cad7b6c0fa0ee75ca Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 12:30:04 -0700 Subject: [PATCH 137/204] refactor: environment version references --- packages/data-models/deployment.model.ts | 2 +- src/app/shared/services/db/db-sync.service.ts | 4 ++-- .../error-handler/error-handler.service.ts | 21 +++++++++++-------- .../shared/services/server/server.service.ts | 4 ++-- .../services/task/task-action.service.ts | 10 ++++++--- src/environments/environment.prod.ts | 2 -- src/environments/environment.ts | 3 --- 7 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 165763c02..473d4b601 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -6,7 +6,7 @@ export const DEPLOYMENT_CONFIG_VERSION = 20240910.0; /** Configuration settings available to runtime application */ export interface IDeploymentRuntimeConfig { - /** version of open-app-builder used to compile */ + /** version of open-app-builder used to compile, read from builder repo package.json */ _app_builder_version: string; /** tag of content version provided by content git repo*/ _content_version: string; diff --git a/src/app/shared/services/db/db-sync.service.ts b/src/app/shared/services/db/db-sync.service.ts index e5110e185..964a8868f 100644 --- a/src/app/shared/services/db/db-sync.service.ts +++ b/src/app/shared/services/db/db-sync.service.ts @@ -83,14 +83,14 @@ export class DBSyncService extends AsyncServiceBase { /** Populate common app_meta to local record */ private generateServerRecord(record: any, mapping: IDBServerMapping) { - const { name } = this.deploymentService.config(); + const { name, _app_builder_version } = this.deploymentService.config(); const { is_user_record, user_record_id_field } = mapping; if (is_user_record && user_record_id_field) { const serverRecord: IDBServerUserRecord = { app_user_id: this.userMetaService.getUserMeta("uuid"), app_user_record_id: record[user_record_id_field], app_deployment_name: name, - app_version: environment.version, + app_version: _app_builder_version, data: record, }; return serverRecord; diff --git a/src/app/shared/services/error-handler/error-handler.service.ts b/src/app/shared/services/error-handler/error-handler.service.ts index 5c7b55adc..0e6f2f229 100644 --- a/src/app/shared/services/error-handler/error-handler.service.ts +++ b/src/app/shared/services/error-handler/error-handler.service.ts @@ -7,6 +7,7 @@ import { GIT_SHA } from "src/environments/sha"; import { fromError as getStacktraceFromError } from "stacktrace-js"; import { CrashlyticsService } from "../crashlytics/crashlytics.service"; import { FirebaseService } from "../firebase/firebase.service"; +import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root", @@ -18,7 +19,11 @@ export class ErrorHandlerService extends ErrorHandler { // Error handling is important and needs to be loaded first. // Because of this we should manually inject the services with Injector. - constructor(private injector: Injector, private firebaseService: FirebaseService) { + constructor( + private injector: Injector, + private firebaseService: FirebaseService, + private deploymentService: DeploymentService + ) { super(); } @@ -30,13 +35,12 @@ export class ErrorHandlerService extends ErrorHandler { * (although workaround required as cannot extend multiple services) */ private async initialise() { - const { production, deploymentConfig } = environment; - const { error_logging, firebase } = deploymentConfig; - if (production && error_logging?.dsn) { + const { error_logging, firebase } = this.deploymentService.config(); + if (environment.production && error_logging?.dsn) { await this.initialiseSentry(); this.sentryEnabled = true; } - if (production && this.firebaseService.app && Capacitor.isNativePlatform()) { + if (environment.production && this.firebaseService.app && Capacitor.isNativePlatform()) { // crashlytics initialised in app component so omitted here this.crashlyticsEnabled = firebase.crashlytics.enabled; } @@ -74,12 +78,11 @@ export class ErrorHandlerService extends ErrorHandler { * https://docs.sentry.io/platforms/javascript/guides/capacitor/ */ private async initialiseSentry() { - const { deploymentConfig, version, production } = environment; - const { error_logging, name } = deploymentConfig; + const { error_logging, name, _app_builder_version } = this.deploymentService.config(); Sentry.init({ dsn: error_logging?.dsn, - environment: production ? "production" : "development", - release: `${name}-${version}-${GIT_SHA}`, + environment: environment.production ? "production" : "development", + release: `${name}-${_app_builder_version}-${GIT_SHA}`, autoSessionTracking: false, attachStacktrace: true, enabled: true, diff --git a/src/app/shared/services/server/server.service.ts b/src/app/shared/services/server/server.service.ts index 42df07250..076034ca5 100644 --- a/src/app/shared/services/server/server.service.ts +++ b/src/app/shared/services/server/server.service.ts @@ -59,7 +59,7 @@ export class ServerService extends SyncServiceBase { } public async syncUserData() { - const { name } = this.deploymentService.config(); + const { name, _app_builder_version } = this.deploymentService.config(); await this.dynamicDataService.ready(); if (!this.device_info) { this.device_info = await Device.getInfo(); @@ -79,7 +79,7 @@ export class ServerService extends SyncServiceBase { // TODO - get DTO from api (?) const data = { contact_fields, - app_version: environment.version, + app_version: _app_builder_version, device_info: this.device_info, app_deployment_name: name, dynamic_data, diff --git a/src/app/shared/services/task/task-action.service.ts b/src/app/shared/services/task/task-action.service.ts index a80da3771..a52ad252b 100644 --- a/src/app/shared/services/task/task-action.service.ts +++ b/src/app/shared/services/task/task-action.service.ts @@ -1,10 +1,10 @@ import { Injectable } from "@angular/core"; import { Subject } from "rxjs"; -import { environment } from "src/environments/environment"; import { generateTimestamp } from "../../utils"; import { AsyncServiceBase } from "../asyncService.base"; import { DbService } from "../db/db.service"; +import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root" }) /** @@ -25,7 +25,10 @@ export class TaskActionService extends AsyncServiceBase { private appInactiveStartTime = new Date().getTime(); /** Don't log inactivity periods lower than this number (30000ms = 30s) */ private readonly INACTIVITY_THRESHOLD = 30000; - constructor(private db: DbService) { + constructor( + private db: DbService, + private deploymentService: DeploymentService + ) { super("TaskActions"); this.registerInitFunction(this.initialise); } @@ -130,13 +133,14 @@ export class TaskActionService extends AsyncServiceBase { } private createNewEntry(task_id: string) { + const { _app_builder_version } = this.deploymentService.config(); const timestamp = generateTimestamp(); const entry: ITaskEntry = { id: `${task_id}_${timestamp}`, task_id, actions: [], _created: timestamp, - _appVersion: environment.version, + _appVersion: _app_builder_version, _completed: false, _duration: 0, }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index b7e290ad6..9b5eec46a 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,9 +1,7 @@ -import packageJson from "../../package.json"; import deploymentJson from "../../.idems_app/deployments/activeDeployment.json"; import type { IDeploymentConfig } from "data-models"; export const environment = { - version: packageJson.version, // HACK - json config converts functions to strings, not strongly typed deploymentConfig: deploymentJson as any as IDeploymentConfig, production: true, diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 9ac4ab5a0..6825f94d3 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,10 +1,7 @@ -import packageJson from "../../package.json"; import deploymentJson from "../../.idems_app/deployments/activeDeployment.json"; import type { IDeploymentConfig } from "data-models"; export const environment = { - /** App version, as provided by package.json */ - version: packageJson.version, // HACK - json config converts functions to strings, not strongly typed deploymentConfig: deploymentJson as any as IDeploymentConfig, production: false, From 298a1b2e4890c771db893dc03544903932082651 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 12:40:08 -0700 Subject: [PATCH 138/204] fix: dynamic data adapter db names --- .../dynamic-data/adapters/persistedMemory.ts | 6 ++---- .../dynamic-data/adapters/reactiveMemory.ts | 9 +++------ .../services/dynamic-data/dynamic-data.service.ts | 13 ++++++++----- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/app/shared/services/dynamic-data/adapters/persistedMemory.ts b/src/app/shared/services/dynamic-data/adapters/persistedMemory.ts index bdd93774b..4378dd7a8 100644 --- a/src/app/shared/services/dynamic-data/adapters/persistedMemory.ts +++ b/src/app/shared/services/dynamic-data/adapters/persistedMemory.ts @@ -18,7 +18,6 @@ import { debounceTime, filter, firstValueFrom, Subject } from "rxjs"; import { FlowTypes } from "data-models"; import { deepMergeObjects, compareObjectKeys } from "../../../utils"; -import { DeploymentService } from "../../deployment/deployment.service"; /** * All persisted docs are stored in the same format with a standard set of meta fields and doc data @@ -78,7 +77,7 @@ export class PersistedMemoryAdapter { [key: string]: RxCollection; }>; - constructor(private deploymentService: DeploymentService) { + constructor(private dbName: string) { this.subscribeToStatePersist(); } @@ -98,9 +97,8 @@ export class PersistedMemoryAdapter { } public async create() { - const { name } = this.deploymentService.config(); this.db = await createRxDatabase({ - name: `${name}_user`, + name: `${this.dbName}_user`, storage: getRxStorageDexie({ autoOpen: true }), ignoreDuplicate: true, }); diff --git a/src/app/shared/services/dynamic-data/adapters/reactiveMemory.ts b/src/app/shared/services/dynamic-data/adapters/reactiveMemory.ts index 4085f0912..59d8f6288 100644 --- a/src/app/shared/services/dynamic-data/adapters/reactiveMemory.ts +++ b/src/app/shared/services/dynamic-data/adapters/reactiveMemory.ts @@ -22,8 +22,6 @@ import { RxDBUpdatePlugin } from "rxdb/plugins/update"; addRxPlugin(RxDBUpdatePlugin); import { BehaviorSubject } from "rxjs"; -import { DeploymentService } from "../../deployment/deployment.service"; - /** * Create a base schema for data * NOTE - by default assumes data has an id field which will be used as primary key @@ -59,7 +57,7 @@ interface IDataUpdate { data?: Record; } -export class ReactiveMemoryAdapater { +export class ReactiveMemoryAdapter { private db: RxDatabase< { [key: string]: RxCollection; @@ -67,12 +65,11 @@ export class ReactiveMemoryAdapater { MemoryStorageInternals, RxStorageMemoryInstanceCreationOptions >; - constructor(private deploymentService: DeploymentService) {} + constructor(private dbName: string) {} public async createDB() { - const { name } = this.deploymentService.config(); this.db = await createRxDatabase({ - name, + name: this.dbName, storage: getRxStorageMemory(), ignoreDuplicate: true, }); diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 5fd7c6138..e49574cbb 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -8,9 +8,10 @@ import { AppDataService } from "../data/app-data.service"; import { AsyncServiceBase } from "../asyncService.base"; import { arrayToHashmap, deepMergeObjects } from "../../utils"; import { PersistedMemoryAdapter } from "./adapters/persistedMemory"; -import { ReactiveMemoryAdapater, REACTIVE_SCHEMA_BASE } from "./adapters/reactiveMemory"; +import { ReactiveMemoryAdapter, REACTIVE_SCHEMA_BASE } from "./adapters/reactiveMemory"; import { TemplateActionRegistry } from "../../components/template/services/instance/template-action.registry"; import { TopLevelProperty } from "rxdb/dist/types/types"; +import { DeploymentService } from "../deployment/deployment.service"; type IDocWithMeta = { id: string; APP_META?: Record }; @@ -27,7 +28,7 @@ export class DynamicDataService extends AsyncServiceBase { * Each flow is represented in its own collection, and populated as requested. * This allows users to query and subscribe to data changes in an efficient way */ - private db: ReactiveMemoryAdapater; + private db: ReactiveMemoryAdapter; /** * A separate cache stores user edits flow data, initially in memory @@ -46,7 +47,8 @@ export class DynamicDataService extends AsyncServiceBase { constructor( private appDataService: AppDataService, - private templateActionRegistry: TemplateActionRegistry + private templateActionRegistry: TemplateActionRegistry, + private deploymentService: DeploymentService ) { super("Dynamic Data"); this.registerInitFunction(this.initialise); @@ -54,6 +56,7 @@ export class DynamicDataService extends AsyncServiceBase { } private async initialise() { + const { name } = this.deploymentService.config(); // Enable dev mode when not in production // NOTE - calls 'global' so requires polyfill if (!environment.production) { @@ -61,8 +64,8 @@ export class DynamicDataService extends AsyncServiceBase { addRxPlugin(module.RxDBDevModePlugin); }); } - this.writeCache = await new PersistedMemoryAdapter().create(); - this.db = await new ReactiveMemoryAdapater().createDB(); + this.writeCache = await new PersistedMemoryAdapter(name).create(); + this.db = await new ReactiveMemoryAdapter(name).createDB(); } private registerTemplateActionHandlers() { this.templateActionRegistry.register({ From fca4107f690ff1ac9ec5b6539e40d1b6dbc563e5 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 13:16:09 -0700 Subject: [PATCH 139/204] fix: deployment init --- src/app/app.component.html | 8 ++++---- src/app/app.component.ts | 11 ++++++----- src/app/app.module.ts | 12 +++++++++++- .../shared/services/deployment/deployment.service.ts | 6 +++--- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 50f45c6c0..13b532e1f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -17,18 +17,18 @@ {{ sideMenuDefaults.title }}
@if (sideMenuDefaults.should_show_version) { - @if (deploymentService.config()._content_version; as CONTENT_VERSION) { + @if (deploymentConfig()._content_version; as CONTENT_VERSION) { - + {{ CONTENT_VERSION }} } @else { - {{ this.deploymentService.config()._app_builder_version }} + {{ deploymentConfig()._app_builder_version }} } } ({{ this.deploymentService.config().name }})({{ deploymentConfig().name }})
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 27e8a8a98..81273b264 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -56,6 +56,10 @@ export class AppComponent { /** Track when app ready to render sidebar and route templates */ public renderAppTemplates = false; + public get deploymentConfig() { + return this.deploymentService.config; + } + /** * A space-separated list of values, hierarchically representing the current platform, * e.g. on iPhone the value would be "mobile ios iphone". @@ -65,8 +69,8 @@ export class AppComponent { platforms: string; constructor( - // Public services (for UI) - public deploymentService: DeploymentService, + // Component UI + private deploymentService: DeploymentService, // 3rd Party Services private platform: Platform, @@ -113,9 +117,6 @@ export class AppComponent { } private async initializeApp() { - // eagerly load deployment config to make available throughout app - await this.deploymentService.ready(); - this.platform.ready().then(async () => { this.platforms = this.platform.platforms().join(" "); this.subscribeToAppConfigChanges(); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 02bfbf6ed..0adbeb589 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,4 +1,4 @@ -import { ErrorHandler, NgModule } from "@angular/core"; +import { APP_INITIALIZER, ErrorHandler, NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; import { FormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @@ -25,6 +25,7 @@ import { TemplateComponentsModule } from "./shared/components/template/template. import { ContextMenuModule } from "./shared/modules/context-menu/context-menu.module"; import { TourModule } from "./feature/tour/tour.module"; import { ErrorHandlerService } from "./shared/services/error-handler/error-handler.service"; +import { DeploymentService } from "./shared/services/deployment/deployment.service"; // Note we need a separate function as it's required // by the AOT compiler. @@ -55,6 +56,15 @@ export function lottiePlayerFactory() { ContextMenuModule, ], providers: [ + // ensure deployment service initialized before app component load + { + provide: APP_INITIALIZER, + multi: true, + useFactory: (deploymentService: DeploymentService) => { + return () => deploymentService.ready(); + }, + deps: [DeploymentService], + }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, HTTP, Device, diff --git a/src/app/shared/services/deployment/deployment.service.ts b/src/app/shared/services/deployment/deployment.service.ts index e0b0a5110..87adccaf7 100644 --- a/src/app/shared/services/deployment/deployment.service.ts +++ b/src/app/shared/services/deployment/deployment.service.ts @@ -8,8 +8,8 @@ import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "pa /** * Deployment runtime config settings * - * NOTE - this is populated as a blocking service during init, - * so no need to await service initialisation before access + * NOTE - this is intialized using an `APP_INITIALIZER` token within + * the main app.module.ts */ export class DeploymentService extends AsyncServiceBase { constructor(private http: HttpClient) { @@ -32,7 +32,7 @@ export class DeploymentService extends AsyncServiceBase { } private loadDeployment() { - return this.http.get("assets/deployment.json").pipe( + return this.http.get("assets/app_data/deployment.json").pipe( catchError(() => { console.warn("No deployment config available"); return of(null); From 0741fabb61498aacf58ae4dff1129e4292e01eb4 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 13:36:57 -0700 Subject: [PATCH 140/204] refactor: revert deployment config signals --- src/app/app.component.html | 8 ++++---- src/app/app.component.ts | 4 ++-- src/app/feature/feedback/feedback.service.ts | 2 +- .../shared/services/app-config/app-config.service.ts | 3 +-- src/app/shared/services/auth/auth.service.ts | 2 +- .../shared/services/crashlytics/crashlytics.service.ts | 2 +- src/app/shared/services/db/db-sync.service.ts | 2 +- .../shared/services/deployment/deployment.service.ts | 10 ++++++---- .../services/dynamic-data/dynamic-data.service.ts | 2 +- .../services/error-handler/error-handler.service.ts | 4 ++-- .../services/file-manager/file-manager.service.ts | 2 +- src/app/shared/services/firebase/firebase.service.ts | 2 +- .../services/remote-asset/remote-asset.service.ts | 2 +- src/app/shared/services/seo/seo.service.ts | 2 +- src/app/shared/services/server/server.service.ts | 2 +- src/app/shared/services/task/task-action.service.ts | 2 +- 16 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 13b532e1f..27bacab8d 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -17,18 +17,18 @@ {{ sideMenuDefaults.title }}
@if (sideMenuDefaults.should_show_version) { - @if (deploymentConfig()._content_version; as CONTENT_VERSION) { + @if (deploymentConfig._content_version; as CONTENT_VERSION) { - + {{ CONTENT_VERSION }} } @else { - {{ deploymentConfig()._app_builder_version }} + {{ deploymentConfig._app_builder_version }} } } ({{ deploymentConfig().name }})({{ deploymentConfig.name }})
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 81273b264..b16b1adf0 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -154,7 +154,7 @@ export class AppComponent { } /** Populate contact fields that may be used by other services during initialisation */ private async populateAppInitFields() { - const { _content_version, _app_builder_version, name } = this.deploymentService.config(); + const { _content_version, _app_builder_version, name } = this.deploymentService.config; this.localStorageService.setProtected("DEPLOYMENT_NAME", name); this.localStorageService.setProtected("APP_VERSION", _app_builder_version); this.localStorageService.setProtected("CONTENT_VERSION", _content_version); @@ -171,7 +171,7 @@ export class AppComponent { * Currently only run on native where specified (but can comment out for testing locally) */ private async loadAuthConfig() { - const { firebase } = this.deploymentService.config(); + const { firebase } = this.deploymentService.config; const { enforceLogin } = this.appAuthenticationDefaults; const ensureLogin = firebase.config && enforceLogin && Capacitor.isNativePlatform(); if (ensureLogin) { diff --git a/src/app/feature/feedback/feedback.service.ts b/src/app/feature/feedback/feedback.service.ts index 693629798..3058b28d2 100644 --- a/src/app/feature/feedback/feedback.service.ts +++ b/src/app/feature/feedback/feedback.service.ts @@ -368,7 +368,7 @@ export class FeedbackService extends SyncServiceBase { * device info and user uuid */ public generateFeedbackMetadata() { - const { _app_builder_version, name } = this.deploymentService.config(); + const { _app_builder_version, name } = this.deploymentService.config; const metadata: IFeedbackMetadata = { deviceInfo: this.deviceInfo, pathname: location.pathname, diff --git a/src/app/shared/services/app-config/app-config.service.ts b/src/app/shared/services/app-config/app-config.service.ts index 25052fe36..5c9bde8d8 100644 --- a/src/app/shared/services/app-config/app-config.service.ts +++ b/src/app/shared/services/app-config/app-config.service.ts @@ -47,8 +47,7 @@ export class AppConfigService extends SyncServiceBase { } private initialise() { - const deploymentOverrides: IAppConfigOverride = - this.deploymentService.config().app_config || {}; + const deploymentOverrides: IAppConfigOverride = this.deploymentService.config.app_config || {}; this.APP_CONFIG = getDefaultAppConfig(); // Store app config with deployment overrides applied, to be merged with additional overrides when applied this.deploymentAppConfig = this.applyAppConfigOverrides(this.APP_CONFIG, deploymentOverrides); diff --git a/src/app/shared/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts index 3b65d589d..affb1e16f 100644 --- a/src/app/shared/services/auth/auth.service.ts +++ b/src/app/shared/services/auth/auth.service.ts @@ -25,7 +25,7 @@ export class AuthService extends SyncServiceBase { this.initialise(); } private initialise() { - const { firebase } = this.deploymentService.config(); + const { firebase } = this.deploymentService.config; if (firebase?.auth?.enabled && this.firebaseService.app) { this.addAuthListeners(); this.registerTemplateActionHandlers(); diff --git a/src/app/shared/services/crashlytics/crashlytics.service.ts b/src/app/shared/services/crashlytics/crashlytics.service.ts index ac5616d97..a6f7010f7 100644 --- a/src/app/shared/services/crashlytics/crashlytics.service.ts +++ b/src/app/shared/services/crashlytics/crashlytics.service.ts @@ -20,7 +20,7 @@ export class CrashlyticsService extends AsyncServiceBase { } private async initialise() { if (Capacitor.isNativePlatform()) { - const { firebase } = this.deploymentService.config(); + const { firebase } = this.deploymentService.config; // Crashlytics is still supported on native device without firebase config (uses google-services.json) // so use config property to toggle enabled instead await this.setEnabled({ enabled: firebase?.crashlytics?.enabled }); diff --git a/src/app/shared/services/db/db-sync.service.ts b/src/app/shared/services/db/db-sync.service.ts index 964a8868f..ea8ac1b99 100644 --- a/src/app/shared/services/db/db-sync.service.ts +++ b/src/app/shared/services/db/db-sync.service.ts @@ -83,7 +83,7 @@ export class DBSyncService extends AsyncServiceBase { /** Populate common app_meta to local record */ private generateServerRecord(record: any, mapping: IDBServerMapping) { - const { name, _app_builder_version } = this.deploymentService.config(); + const { name, _app_builder_version } = this.deploymentService.config; const { is_user_record, user_record_id_field } = mapping; if (is_user_record && user_record_id_field) { const serverRecord: IDBServerUserRecord = { diff --git a/src/app/shared/services/deployment/deployment.service.ts b/src/app/shared/services/deployment/deployment.service.ts index 87adccaf7..eb82ae9ac 100644 --- a/src/app/shared/services/deployment/deployment.service.ts +++ b/src/app/shared/services/deployment/deployment.service.ts @@ -1,4 +1,4 @@ -import { Injectable, signal } from "@angular/core"; +import { Injectable } from "@angular/core"; import { AsyncServiceBase } from "../asyncService.base"; import { HttpClient } from "@angular/common/http"; import { catchError, map, of, firstValueFrom } from "rxjs"; @@ -18,16 +18,18 @@ export class DeploymentService extends AsyncServiceBase { } /** Private writeable config to allow population from JSON */ - private _config = signal(DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS); + private _config = DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS; /** Read-only access to deployment runtime config */ - public config = this._config.asReadonly(); + public get config() { + return this._config; + } /** Load active deployment configuration from JSON file */ private async initialise() { const deployment = await firstValueFrom(this.loadDeployment()); if (deployment) { - this._config.set(deployment); + this._config = deployment; } } diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index e49574cbb..0785bf10b 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -56,7 +56,7 @@ export class DynamicDataService extends AsyncServiceBase { } private async initialise() { - const { name } = this.deploymentService.config(); + const { name } = this.deploymentService.config; // Enable dev mode when not in production // NOTE - calls 'global' so requires polyfill if (!environment.production) { diff --git a/src/app/shared/services/error-handler/error-handler.service.ts b/src/app/shared/services/error-handler/error-handler.service.ts index 0e6f2f229..45e557fe0 100644 --- a/src/app/shared/services/error-handler/error-handler.service.ts +++ b/src/app/shared/services/error-handler/error-handler.service.ts @@ -35,7 +35,7 @@ export class ErrorHandlerService extends ErrorHandler { * (although workaround required as cannot extend multiple services) */ private async initialise() { - const { error_logging, firebase } = this.deploymentService.config(); + const { error_logging, firebase } = this.deploymentService.config; if (environment.production && error_logging?.dsn) { await this.initialiseSentry(); this.sentryEnabled = true; @@ -78,7 +78,7 @@ export class ErrorHandlerService extends ErrorHandler { * https://docs.sentry.io/platforms/javascript/guides/capacitor/ */ private async initialiseSentry() { - const { error_logging, name, _app_builder_version } = this.deploymentService.config(); + const { error_logging, name, _app_builder_version } = this.deploymentService.config; Sentry.init({ dsn: error_logging?.dsn, environment: environment.production ? "production" : "development", diff --git a/src/app/shared/services/file-manager/file-manager.service.ts b/src/app/shared/services/file-manager/file-manager.service.ts index d9582ae73..be0bf3c48 100644 --- a/src/app/shared/services/file-manager/file-manager.service.ts +++ b/src/app/shared/services/file-manager/file-manager.service.ts @@ -27,7 +27,7 @@ export class FileManagerService extends SyncServiceBase { } private initialise() { - this.cacheName = this.deploymentService.config().name; + this.cacheName = this.deploymentService.config.name; this.registerTemplateActionHandlers(); } diff --git a/src/app/shared/services/firebase/firebase.service.ts b/src/app/shared/services/firebase/firebase.service.ts index 39fddb5a5..e8ca57e73 100644 --- a/src/app/shared/services/firebase/firebase.service.ts +++ b/src/app/shared/services/firebase/firebase.service.ts @@ -18,7 +18,7 @@ export class FirebaseService extends SyncServiceBase { * Configure app module imports dependent on what firebase features should be enabled */ private initialise() { - const { firebase } = this.deploymentService.config(); + const { firebase } = this.deploymentService.config; // Check if any services are enabled, simply return if not const enabledServices = Object.entries(firebase) diff --git a/src/app/shared/services/remote-asset/remote-asset.service.ts b/src/app/shared/services/remote-asset/remote-asset.service.ts index fddff64d3..f097fb85f 100644 --- a/src/app/shared/services/remote-asset/remote-asset.service.ts +++ b/src/app/shared/services/remote-asset/remote-asset.service.ts @@ -49,7 +49,7 @@ export class RemoteAssetService extends AsyncServiceBase { private async initialise() { this.registerTemplateActionHandlers(); // require supabase to be configured to use remote asset service - const { enabled, publicApiKey, url } = this.deploymentService.config().supabase; + const { enabled, publicApiKey, url } = this.deploymentService.config.supabase; this.supabaseEnabled = enabled; if (this.supabaseEnabled) { await this.ensureAsyncServicesReady([this.templateAssetService, this.dynamicDataService]); diff --git a/src/app/shared/services/seo/seo.service.ts b/src/app/shared/services/seo/seo.service.ts index 95e120414..6e9b254d1 100644 --- a/src/app/shared/services/seo/seo.service.ts +++ b/src/app/shared/services/seo/seo.service.ts @@ -65,7 +65,7 @@ export class SeoService extends SyncServiceBase { private getDefaultSEOTags(): ISEOMeta { const PUBLIC_URL = location.origin; let faviconUrl = `${PUBLIC_URL}/assets/icon/favicon.svg`; - const { web, app_config } = this.deploymentService.config(); + const { web, app_config } = this.deploymentService.config; if (web?.favicon_asset) { faviconUrl = `${PUBLIC_URL}/assets/app_data/assets/${web.favicon_asset}`; } diff --git a/src/app/shared/services/server/server.service.ts b/src/app/shared/services/server/server.service.ts index 076034ca5..fc0c04a6d 100644 --- a/src/app/shared/services/server/server.service.ts +++ b/src/app/shared/services/server/server.service.ts @@ -59,7 +59,7 @@ export class ServerService extends SyncServiceBase { } public async syncUserData() { - const { name, _app_builder_version } = this.deploymentService.config(); + const { name, _app_builder_version } = this.deploymentService.config; await this.dynamicDataService.ready(); if (!this.device_info) { this.device_info = await Device.getInfo(); diff --git a/src/app/shared/services/task/task-action.service.ts b/src/app/shared/services/task/task-action.service.ts index a52ad252b..5cec8a66e 100644 --- a/src/app/shared/services/task/task-action.service.ts +++ b/src/app/shared/services/task/task-action.service.ts @@ -133,7 +133,7 @@ export class TaskActionService extends AsyncServiceBase { } private createNewEntry(task_id: string) { - const { _app_builder_version } = this.deploymentService.config(); + const { _app_builder_version } = this.deploymentService.config; const timestamp = generateTimestamp(); const entry: ITaskEntry = { id: `${task_id}_${timestamp}`, From adca32bb09a9468aa5e55cc47eea31a40af5cfd7 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 13:39:19 -0700 Subject: [PATCH 141/204] chore: code tidying --- src/app/app.module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0adbeb589..322d87506 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -56,6 +56,9 @@ export function lottiePlayerFactory() { ContextMenuModule, ], providers: [ + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + HTTP, + Device, // ensure deployment service initialized before app component load { provide: APP_INITIALIZER, @@ -65,9 +68,6 @@ export function lottiePlayerFactory() { }, deps: [DeploymentService], }, - { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, - HTTP, - Device, httpInterceptorProviders, { provide: ErrorHandler, useClass: ErrorHandlerService }, ], From ccab3a23a08c7ed3f3823f1ff27b4ba7af8c8006 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 15:38:31 -0700 Subject: [PATCH 142/204] chore: bump action artifact versions --- .github/workflows/deprecated/deployment-hosting.yml | 6 +++--- .github/workflows/deprecated/sourcemaps-upload.yml | 2 +- .github/workflows/reusable-android-build.yml | 6 +++--- .github/workflows/reusable-android-release.yml | 2 +- .github/workflows/reusable-appetize.yml | 2 +- .github/workflows/reusable-deploy-pr-preview.yml | 2 +- .github/workflows/reusable-deploy-web-preview.yml | 6 +++++- .github/workflows/test-preview.yml | 2 +- .github/workflows/test-visual.yml | 8 ++++---- .../docs/authors/advanced/looping-data-dynamic.md | 0 .../actions/templates/deploy-firebase/template.yml | 2 +- .../templates/pr-preview-firebase/template.yml | 2 +- .../data-items/data-items.component.spec.ts | 12 ++++++++++++ 13 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 documentation/docs/authors/advanced/looping-data-dynamic.md diff --git a/.github/workflows/deprecated/deployment-hosting.yml b/.github/workflows/deprecated/deployment-hosting.yml index eeb3b20be..89317c308 100644 --- a/.github/workflows/deprecated/deployment-hosting.yml +++ b/.github/workflows/deprecated/deployment-hosting.yml @@ -34,7 +34,7 @@ jobs: echo "DEPLOYMENT_NAME=${{needs.build.outputs.DEPLOYMENT_NAME}}" >> $GITHUB_ENV echo "GIT_SHA=${{needs.build.outputs.GIT_SHA}}" >> $GITHUB_ENV - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: www - name: Extract Build folder @@ -52,7 +52,7 @@ jobs: SENTRY_PROJECT: ${{env.DEPLOYMENT_NAME}} continue-on-error: true - name: Store sourcemaps artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: sourcemaps-$GIT_SHA path: www/*.map @@ -65,7 +65,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: www - name: Extract Build folder diff --git a/.github/workflows/deprecated/sourcemaps-upload.yml b/.github/workflows/deprecated/sourcemaps-upload.yml index 5410a65c8..d37456836 100644 --- a/.github/workflows/deprecated/sourcemaps-upload.yml +++ b/.github/workflows/deprecated/sourcemaps-upload.yml @@ -60,7 +60,7 @@ jobs: SENTRY_PROJECT: ${{env.DEPLOYMENT_NAME}} continue-on-error: true - name: Store sourcemaps artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: sourcemaps-$SHA_SHORT path: www/*.map diff --git a/.github/workflows/reusable-android-build.yml b/.github/workflows/reusable-android-build.yml index 8ef127275..493e0acc7 100644 --- a/.github/workflows/reusable-android-build.yml +++ b/.github/workflows/reusable-android-build.yml @@ -104,7 +104,7 @@ jobs: run: yarn workflow android - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: www @@ -134,7 +134,7 @@ jobs: run: ./gradlew :app:assembleDebug - name: Upload debug apk - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: debug_apk path: android/app/build/outputs/apk/debug/app-debug.apk @@ -155,7 +155,7 @@ jobs: keyPassword: ${{ env.KEY_PASSWORD }} - name: Upload release bundle - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: release_bundle path: ${{steps.sign_aab.outputs.signedReleaseFile}} diff --git a/.github/workflows/reusable-android-release.yml b/.github/workflows/reusable-android-release.yml index c4a4271e1..8f60a46ed 100644 --- a/.github/workflows/reusable-android-release.yml +++ b/.github/workflows/reusable-android-release.yml @@ -33,7 +33,7 @@ jobs: - name: Download Build Artifact id: download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: release_bundle path: ./ diff --git a/.github/workflows/reusable-appetize.yml b/.github/workflows/reusable-appetize.yml index b2dceae7d..3919b8730 100644 --- a/.github/workflows/reusable-appetize.yml +++ b/.github/workflows/reusable-appetize.yml @@ -42,7 +42,7 @@ jobs: - uses: actions/checkout@v3 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: debug_apk path: ./ diff --git a/.github/workflows/reusable-deploy-pr-preview.yml b/.github/workflows/reusable-deploy-pr-preview.yml index 614dfb436..dad03cc68 100644 --- a/.github/workflows/reusable-deploy-pr-preview.yml +++ b/.github/workflows/reusable-deploy-pr-preview.yml @@ -45,7 +45,7 @@ jobs: - uses: actions/checkout@v3 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: www diff --git a/.github/workflows/reusable-deploy-web-preview.yml b/.github/workflows/reusable-deploy-web-preview.yml index bd6dabb6c..c526b2a89 100644 --- a/.github/workflows/reusable-deploy-web-preview.yml +++ b/.github/workflows/reusable-deploy-web-preview.yml @@ -40,11 +40,14 @@ on: FIREBASE_SERVICE_ACCOUNT: required: true +# TODO - tidy up, bump versions, settup testing, rename, use templates + jobs: build_action: uses: ./.github/workflows/reusable-app-build.yml secrets: inherit + # TODO - split post_build and deploy deploy: needs: build_action runs-on: ubuntu-latest @@ -55,7 +58,8 @@ jobs: - uses: actions/checkout@v3 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 + # TODO - get id from build step ? with: name: www diff --git a/.github/workflows/test-preview.yml b/.github/workflows/test-preview.yml index 61e1552aa..ab24f91fc 100644 --- a/.github/workflows/test-preview.yml +++ b/.github/workflows/test-preview.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: www - name: Extract Build folder diff --git a/.github/workflows/test-visual.yml b/.github/workflows/test-visual.yml index 35b4f688d..277a7ebc4 100644 --- a/.github/workflows/test-visual.yml +++ b/.github/workflows/test-visual.yml @@ -55,7 +55,7 @@ jobs: # Download build ############################################################################# - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: www @@ -79,7 +79,7 @@ jobs: - name: Upload screenshots if: ${{inputs.generate}} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: screenshots-artifact # NOTE - must match SCREENSHOT_ARTIFACT_NAME in code path: packages/test-visual/output/screenshots @@ -95,7 +95,7 @@ jobs: - name: Upload artifact if: ${{inputs.compare}} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-visual-diffs-artifact path: packages/test-visual/output/diffs @@ -104,7 +104,7 @@ jobs: - name: Upload Text Outputs if: ${{inputs.compare}} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: text_output path: packages/test-visual/output/*.txt diff --git a/documentation/docs/authors/advanced/looping-data-dynamic.md b/documentation/docs/authors/advanced/looping-data-dynamic.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/actions/templates/deploy-firebase/template.yml b/packages/actions/templates/deploy-firebase/template.yml index 63a7e7b79..936f14656 100644 --- a/packages/actions/templates/deploy-firebase/template.yml +++ b/packages/actions/templates/deploy-firebase/template.yml @@ -49,7 +49,7 @@ jobs: # Extract build artifact - uses: actions/checkout@v3 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: www - name: Extract Build folder diff --git a/packages/actions/templates/pr-preview-firebase/template.yml b/packages/actions/templates/pr-preview-firebase/template.yml index d99fca980..4a0b4e3e9 100644 --- a/packages/actions/templates/pr-preview-firebase/template.yml +++ b/packages/actions/templates/pr-preview-firebase/template.yml @@ -50,7 +50,7 @@ jobs: # Extract build artifact - uses: actions/checkout@v3 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: www - name: Extract Build folder diff --git a/src/app/shared/components/template/components/data-items/data-items.component.spec.ts b/src/app/shared/components/template/components/data-items/data-items.component.spec.ts index cc0c93fd6..2066e1452 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.spec.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.spec.ts @@ -236,4 +236,16 @@ describe("DataItemsComponent", () => { ).args[0]; expect(setItemContext.currentItemId).toBe(rowItemId); }); + // TODO + // it("sets an item correctly for a given _index", async () => { + // await actions.set_item({ + // context: SET_ITEM_CONTEXT, + // _index: 1, + // writeableProps: { string: "sets an item correctly for a given _index" }, + // }); + // const obs = await service.query$("data_list", "test_flow"); + // const data = await firstValueFrom(obs); + // expect(data[0].string).toEqual("hello"); + // expect(data[1].string).toEqual("sets an item correctly for a given _index"); + // }); }); From 93b1505c9e9fd72a93823a4d2d033262e7a66fea Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 15:39:57 -0700 Subject: [PATCH 143/204] chore: bump upload-pages-artifact --- .github/workflows/web-build.yml | 2 +- packages/actions/templates/app-build/template.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/web-build.yml b/.github/workflows/web-build.yml index e317a1535..e5fe0e80c 100644 --- a/.github/workflows/web-build.yml +++ b/.github/workflows/web-build.yml @@ -175,7 +175,7 @@ jobs: # Use github pages upload artifact action to compress and upload - name: Upload artifact if: ${{!inputs.skip-upload}} - uses: actions/upload-pages-artifact@v1.0.8 + uses: actions/upload-pages-artifact@v3 with: path: "www/" name: www diff --git a/packages/actions/templates/app-build/template.yml b/packages/actions/templates/app-build/template.yml index 9f0d24a55..2c2cc6870 100644 --- a/packages/actions/templates/app-build/template.yml +++ b/packages/actions/templates/app-build/template.yml @@ -69,7 +69,7 @@ jobs: # Use github pages upload artifact action to compress and upload - name: Upload artifact - uses: actions/upload-pages-artifact@v1.0.8 + uses: actions/upload-pages-artifact@v3 with: path: "www/" name: ${{inputs.artifact-name}} From 2974a024149c208838430b7a8694b4e7c6545b1b Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 15:42:15 -0700 Subject: [PATCH 144/204] chore: delete wip docs --- documentation/docs/authors/advanced/looping-data-dynamic.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 documentation/docs/authors/advanced/looping-data-dynamic.md diff --git a/documentation/docs/authors/advanced/looping-data-dynamic.md b/documentation/docs/authors/advanced/looping-data-dynamic.md deleted file mode 100644 index e69de29bb..000000000 From 9a7b5668e8caedf979597b3fde4d5f221bd0f27e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 15:43:13 -0700 Subject: [PATCH 145/204] chore: revert temp spec tests --- .../data-items/data-items.component.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/app/shared/components/template/components/data-items/data-items.component.spec.ts b/src/app/shared/components/template/components/data-items/data-items.component.spec.ts index 2066e1452..cc0c93fd6 100644 --- a/src/app/shared/components/template/components/data-items/data-items.component.spec.ts +++ b/src/app/shared/components/template/components/data-items/data-items.component.spec.ts @@ -236,16 +236,4 @@ describe("DataItemsComponent", () => { ).args[0]; expect(setItemContext.currentItemId).toBe(rowItemId); }); - // TODO - // it("sets an item correctly for a given _index", async () => { - // await actions.set_item({ - // context: SET_ITEM_CONTEXT, - // _index: 1, - // writeableProps: { string: "sets an item correctly for a given _index" }, - // }); - // const obs = await service.query$("data_list", "test_flow"); - // const data = await firstValueFrom(obs); - // expect(data[0].string).toEqual("hello"); - // expect(data[1].string).toEqual("sets an item correctly for a given _index"); - // }); }); From e355a65f4638fe10c01ea74ddcad1fc0fb13f29d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 11 Sep 2024 15:54:11 -0700 Subject: [PATCH 146/204] chore: code tidying --- .github/workflows/reusable-deploy-web-preview.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/reusable-deploy-web-preview.yml b/.github/workflows/reusable-deploy-web-preview.yml index c526b2a89..1eff632cb 100644 --- a/.github/workflows/reusable-deploy-web-preview.yml +++ b/.github/workflows/reusable-deploy-web-preview.yml @@ -40,8 +40,6 @@ on: FIREBASE_SERVICE_ACCOUNT: required: true -# TODO - tidy up, bump versions, settup testing, rename, use templates - jobs: build_action: uses: ./.github/workflows/reusable-app-build.yml @@ -59,7 +57,6 @@ jobs: - name: Download Build Artifact uses: actions/download-artifact@v4 - # TODO - get id from build step ? with: name: www @@ -68,6 +65,7 @@ jobs: mkdir www tar -xf artifact.tar --directory www + # TODO - use templated files # Create a .firebaserc file mapping any firebase deployment host targets (required if multi-host projects) # e.g. {"projects": {"default": "my_app"},"targets": {"my_app": {"hosting": {"my_app_dev":["my_app_dev"]} } } - name: Populate Firebase Targets From fbf68b6c31b33dcd09be2e0a9c6732fb40d68c75 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Thu, 12 Sep 2024 10:24:11 +0100 Subject: [PATCH 147/204] docs: tidy path issue documentation --- README.md | 4 ---- .../docs/contributors/documentation/running-server.md | 2 +- documentation/docs/index.md | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e779b071f..a76f3333f 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,6 @@ ## Installation -### Choosing a location to download the repo - -Before running the commands, make sure you’re in the correct folder (the "current working directory") where you want to save the repo locally. You can use the `pwd` command to print the current directory, and `cd` to change the directory. You should ensure that there are no spaces at any point in the current working directory path. For example, if your top level user folder contains a space in the name, consider downloading the repo to a folder located outside of the user folder, e.g. in a new top-level `C:/apps` folder on Windows. - ### Download the repo with binary assets To download the repo into the current working directory, run: diff --git a/documentation/docs/contributors/documentation/running-server.md b/documentation/docs/contributors/documentation/running-server.md index a6212ea6e..ab941f15e 100644 --- a/documentation/docs/contributors/documentation/running-server.md +++ b/documentation/docs/contributors/documentation/running-server.md @@ -21,7 +21,7 @@ The scripts below will create a python [virtual environment](https://docs.python ```sh linenums="1" cd documentation - python3 -m venv .venv + python -m venv .venv # Or `python3`, depending on python installation source .venv/bin/activate pip install -r requirements.txt mkdocs serve diff --git a/documentation/docs/index.md b/documentation/docs/index.md index bc02cf966..9fa4b579d 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -19,7 +19,7 @@ ### Download the repo with binary assets !!! tip "Choosing a location to download the repo" - Before running the commands, make sure you’re in the correct folder (the "current working directory") where you want to save the repo locally. You can use the `pwd` command to print the current directory, and `cd` to change the directory. You should ensure that there are no spaces at any point in the current working directory path. For example, if your top level user folder contains a space in the name, consider downloading the repo to a folder located outside of the user folder, e.g. in a new top-level `C:/apps` folder on Windows. + It is best to download the repo to a folder path that does not include any spaces. If using a user documents directory that includes spaces, e.g. `/user/my name`, you may need to re-open the terminal in a different folder instead. To download the repo into the current working directory, run: ``` From 731a1fc90a19b8d9aed4d597a0125943ce72aae7 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 11:50:48 -0700 Subject: [PATCH 148/204] feat: wip manifest generator --- .../src/commands/app-data/convert/index.ts | 5 ++ .../flowParser/parsers/template.parser.ts | 4 +- .../convert/utils/app-data-manifest.spec.ts | 0 .../convert/utils/app-data-manifest.utils.ts | 64 +++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.spec.ts create mode 100644 packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.utils.ts diff --git a/packages/scripts/src/commands/app-data/convert/index.ts b/packages/scripts/src/commands/app-data/convert/index.ts index 6852fd2e5..39f785233 100644 --- a/packages/scripts/src/commands/app-data/convert/index.ts +++ b/packages/scripts/src/commands/app-data/convert/index.ts @@ -19,6 +19,7 @@ import { standardiseNewlines, } from "./utils"; import { FlowParserProcessor } from "./processors/flowParser/flowParser"; +import { generateManifest } from "./utils/app-data-manifest.utils"; /*************************************************************************************** * CLI @@ -130,6 +131,10 @@ export class AppDataConverter { processor.logger = this.logger; const jsonFlows = Object.values(combinedOutputsHashmap); const result = (await processor.process(jsonFlows)) as IParsedWorkbookData; + const manifest = generateManifest(result); + console.table(manifest.actions); + console.table(manifest.components); + // TODO - write to disk and log const { errors, warnings } = this.logOutputs(result); return { result, errors, warnings }; } diff --git a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts index ec363dff6..fb28b296d 100644 --- a/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts +++ b/packages/scripts/src/commands/app-data/convert/processors/flowParser/parsers/template.parser.ts @@ -9,7 +9,7 @@ import { } from "../../../utils"; export class TemplateParser extends DefaultParser { - postProcessRow(row: FlowTypes.TemplateRow, rowNumber = 1, nestedPath?: string) { + public override postProcessRow(row: FlowTypes.TemplateRow, rowNumber = 1, nestedPath?: string) { // remove empty rows if (Object.keys(row).length === 0) { return; @@ -72,7 +72,7 @@ export class TemplateParser extends DefaultParser { return row; } - public postProcessFlows(flows: FlowTypes.FlowTypeWithData[]) { + public override postProcessFlows(flows: FlowTypes.FlowTypeWithData[]) { const flowsWithOverrides = assignFlowOverrides(flows); return flowsWithOverrides; } diff --git a/packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.spec.ts b/packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.utils.ts b/packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.utils.ts new file mode 100644 index 000000000..e35da79f6 --- /dev/null +++ b/packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.utils.ts @@ -0,0 +1,64 @@ +/** + * TODO + * - migrate manifest to standalone util + tests + * - Organise structures for individual sub-reports (e.g. assets, sheets, components) + * - Include sheet types/subtypes in manifest + * - short summary text that links to breakdown + * - possible recommendations/optimisations from manifest + * - move manifest generator here? + * - how to handle implicit deps (one component uses another) + * - possibly will require runtime error/warning/prompt + * - handle dynamic + * - handle multiple emit types + * - QA components/actions that don't exist + * - possibly export list of COMPONENTS_AVAILABLE (or similar... or just use main list lookup) + * - also consider asset manifest (but would need to ensure dynamic assets included, plus param list + template value) + * - handle implicit components (check imports (?)) + */ + +// Testing notes +// yarn workflow sync_sheets --skip-download + +import { FlowTypes } from "data-models"; +import { IParsedWorkbookData } from "../types"; + +interface IFlowManifest { + components: Record; + actions: Record; + // TODO - also may need to consider imported from data_list or other sources +} + +/** + * Create a manifest of components and action handlers referenced by templates + * NOTE - this will not explicitly identify any variables injected dynamically + * TODO - add tests and example to catch? + * */ +export function generateManifest(data: IParsedWorkbookData): IFlowManifest { + const manifest: IFlowManifest = { actions: {}, components: {} }; + // TODO - consider extracting dynamic data_list actions if exist + for (const flow of data.template || []) { + for (const row of flow.rows) { + const { action_list = [], type } = row as FlowTypes.TemplateRow; + for (const action of action_list) { + manifest.actions[action.action_id] ??= 0; + manifest.actions[action.action_id]++; + } + manifest.components[type] ??= 0; + manifest.components[type]++; + } + } + // sort data alphabetically + for (const key of Object.keys(manifest)) { + manifest[key] = sortJsonByKey(manifest[key]); + } + return manifest; +} + +// TODO - move to generic location +export function sortJsonByKey>(json: T) { + const sorted = {}; + for (const [key, value] of Object.entries(json).sort((a, b) => (a[0] > b[0] ? 1 : -1))) { + sorted[key] = value; + } + return sorted as T; +} From 0cb211b0ed9f0ff765ec27647a141527e7fa1505 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 11:58:44 -0700 Subject: [PATCH 149/204] chore: update comment --- src/app/shared/services/deployment/deployment.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/shared/services/deployment/deployment.service.ts b/src/app/shared/services/deployment/deployment.service.ts index eb82ae9ac..eb897ab8b 100644 --- a/src/app/shared/services/deployment/deployment.service.ts +++ b/src/app/shared/services/deployment/deployment.service.ts @@ -9,7 +9,11 @@ import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "pa * Deployment runtime config settings * * NOTE - this is intialized using an `APP_INITIALIZER` token within - * the main app.module.ts + * the main app.module.ts and will block all other services from loading until + * it is fully initialised + * + * Services that access the deployment config therefore do not need to await + * DeploymentService init/ready methods. */ export class DeploymentService extends AsyncServiceBase { constructor(private http: HttpClient) { From 1a47cb3950b59ad08fc240b42c68d29eb6e99f50 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 12:05:01 -0700 Subject: [PATCH 150/204] chore: update comments --- src/app/shared/services/dynamic-data/dynamic-data.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/shared/services/dynamic-data/dynamic-data.service.ts b/src/app/shared/services/dynamic-data/dynamic-data.service.ts index 0785bf10b..356ccfd64 100644 --- a/src/app/shared/services/dynamic-data/dynamic-data.service.ts +++ b/src/app/shared/services/dynamic-data/dynamic-data.service.ts @@ -56,6 +56,9 @@ export class DynamicDataService extends AsyncServiceBase { } private async initialise() { + // Use the deployment name as unique database identifier + // This will allow multiple databases to be used on the same origin + // for different deployments (e.g. dev sites running on localhost) const { name } = this.deploymentService.config; // Enable dev mode when not in production // NOTE - calls 'global' so requires polyfill From 9ba340f6cbe1f16e87d63a668eb0885ce8015f6d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 14:11:13 -0700 Subject: [PATCH 151/204] feat: wip deployment config injection token --- src/main.ts | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index e6c5f3302..9dc3321e3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,16 +5,36 @@ import { defineCustomElements } from "@ionic/pwa-elements/loader"; import { AppModule } from "./app/app.module"; import { environment } from "./environments/environment"; +import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "packages/data-models"; +import { DEPLOYMENT_CONFIG } from "./app/shared/services/deployment/deployment.service"; if (environment.production) { enableProdMode(); } -platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch((err) => console.log(err)); +/** Load deployment config from asset json, returning default config if not available*/ +const loadConfig = async () => { + const res = await fetch("/assets/app_data/deployment.json"); + if (res.status === 200) { + const deploymentConfig = await res.json(); + console.log("[DEPLOYMENT] config loaded", deploymentConfig); + return deploymentConfig; + } else { + console.warn("[DEPLOYMENT] config not found, using defaults"); + return DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS; + } +}; -if (!Capacitor.isNative) { - // Call PWA custom element loader after the platform has been bootstrapped - defineCustomElements(window); -} +// Initialise platform once deployment config has loaded, setting the value of the +// global DEPLOYMENT_CONFIG injection token from the loaded json +// https://stackoverflow.com/a/62151011 +loadConfig().then((deploymentConfig) => { + platformBrowserDynamic([{ provide: DEPLOYMENT_CONFIG, useValue: deploymentConfig }]) + .bootstrapModule(AppModule) + .catch((err) => console.log(err)); + + if (!Capacitor.isNativePlatform()) { + // Call PWA custom element loader after the platform has been bootstrapped + defineCustomElements(window); + } +}); From 8c3f7e3234762eeba09ecc84b8d408fbaf13456f Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 17:19:09 -0700 Subject: [PATCH 152/204] chore: code tidying --- packages/data-models/deployment.model.ts | 6 +-- .../services/deployment/deployment.service.ts | 49 ++++++------------- src/main.ts | 15 ++++-- 3 files changed, 28 insertions(+), 42 deletions(-) diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 473d4b601..752a02a41 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -1,5 +1,5 @@ import type { IGdriveEntry } from "../@idemsInternational/gdrive-tools"; -import type { IAppConfig } from "./appConfig"; +import type { IAppConfigOverride } from "./appConfig"; /** Update version to force recompile next time deployment set (e.g. after default config update) */ export const DEPLOYMENT_CONFIG_VERSION = 20240910.0; @@ -21,7 +21,7 @@ export interface IDeploymentRuntimeConfig { endpoint?: string; }; /** Optional override of any provided constants from data-models/constants */ - app_config: IAppConfig; + app_config: IAppConfigOverride; /** 3rd party integration for logging services */ error_logging?: { /** sentry/glitchtip logging dsn */ @@ -159,7 +159,7 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { db_name: "plh", endpoint: "https://apps-server.idems.international/api", }, - app_config: {} as any, // populated by `getDefaultAppConstants()`, + app_config: {}, firebase: { config: null, diff --git a/src/app/shared/services/deployment/deployment.service.ts b/src/app/shared/services/deployment/deployment.service.ts index eb897ab8b..c50287a8b 100644 --- a/src/app/shared/services/deployment/deployment.service.ts +++ b/src/app/shared/services/deployment/deployment.service.ts @@ -1,8 +1,16 @@ -import { Injectable } from "@angular/core"; -import { AsyncServiceBase } from "../asyncService.base"; -import { HttpClient } from "@angular/common/http"; -import { catchError, map, of, firstValueFrom } from "rxjs"; -import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "packages/data-models"; +import { Inject, Injectable, InjectionToken } from "@angular/core"; +import { IDeploymentRuntimeConfig } from "packages/data-models"; +import { SyncServiceBase } from "../syncService.base"; + +/** + * Token to inject deployment config value into any service. + * This is populated from json file before platform load, as part of src\main.ts + * + * Can be used directly in the constructor via `@Inject(DEPLOYMENT_CONFIG)`, + * or values accessed from the DeploymentService + */ +export const DEPLOYMENT_CONFIG: InjectionToken = + new InjectionToken("Application Configuration"); @Injectable({ providedIn: "root" }) /** @@ -15,35 +23,8 @@ import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "pa * Services that access the deployment config therefore do not need to await * DeploymentService init/ready methods. */ -export class DeploymentService extends AsyncServiceBase { - constructor(private http: HttpClient) { +export class DeploymentService extends SyncServiceBase { + constructor(@Inject(DEPLOYMENT_CONFIG) public readonly config: IDeploymentRuntimeConfig) { super("Deployment Service"); - this.registerInitFunction(this.initialise); - } - - /** Private writeable config to allow population from JSON */ - private _config = DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS; - - /** Read-only access to deployment runtime config */ - public get config() { - return this._config; - } - - /** Load active deployment configuration from JSON file */ - private async initialise() { - const deployment = await firstValueFrom(this.loadDeployment()); - if (deployment) { - this._config = deployment; - } - } - - private loadDeployment() { - return this.http.get("assets/app_data/deployment.json").pipe( - catchError(() => { - console.warn("No deployment config available"); - return of(null); - }), - map((v) => v as IDeploymentRuntimeConfig) - ); } } diff --git a/src/main.ts b/src/main.ts index 9dc3321e3..c01efccbf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,7 +13,7 @@ if (environment.production) { } /** Load deployment config from asset json, returning default config if not available*/ -const loadConfig = async () => { +const loadConfig = async (): Promise => { const res = await fetch("/assets/app_data/deployment.json"); if (res.status === 200) { const deploymentConfig = await res.json(); @@ -21,13 +21,18 @@ const loadConfig = async () => { return deploymentConfig; } else { console.warn("[DEPLOYMENT] config not found, using defaults"); - return DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS; + return { ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, app_config: {} as any }; } }; -// Initialise platform once deployment config has loaded, setting the value of the -// global DEPLOYMENT_CONFIG injection token from the loaded json -// https://stackoverflow.com/a/62151011 +/** + * Initialise platform once deployment config has loaded, setting the value of the + * global DEPLOYMENT_CONFIG injection token from the loaded json + * https://stackoverflow.com/a/62151011 + * + * The configuration is loaded before the rest of the platform so that config values + * can be used to configure modules imported in app.module.ts + */ loadConfig().then((deploymentConfig) => { platformBrowserDynamic([{ provide: DEPLOYMENT_CONFIG, useValue: deploymentConfig }]) .bootstrapModule(AppModule) From 42410db29500c9a452afa6f41e36b95b0789ac64 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 23:16:35 -0700 Subject: [PATCH 153/204] refactor: analytics module --- packages/data-models/deployment.model.ts | 17 ++++++- .../scripts/src/tasks/providers/appData.ts | 13 +++--- src/app/app.module.ts | 29 ++++-------- src/app/deployment-features.module.ts | 17 +++++++ .../services/analytics/analytics.module.ts | 45 +++++++++++++++++++ src/app/shared/services/analytics/index.ts | 2 + src/environments/environment.prod.ts | 1 - src/environments/environment.ts | 4 -- 8 files changed, 96 insertions(+), 32 deletions(-) create mode 100644 src/app/deployment-features.module.ts create mode 100644 src/app/shared/services/analytics/analytics.module.ts create mode 100644 src/app/shared/services/analytics/index.ts diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 752a02a41..46ac67f3f 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -2,7 +2,7 @@ import type { IGdriveEntry } from "../@idemsInternational/gdrive-tools"; import type { IAppConfigOverride } from "./appConfig"; /** Update version to force recompile next time deployment set (e.g. after default config update) */ -export const DEPLOYMENT_CONFIG_VERSION = 20240910.0; +export const DEPLOYMENT_CONFIG_VERSION = 20240912.0; /** Configuration settings available to runtime application */ export interface IDeploymentRuntimeConfig { @@ -12,6 +12,8 @@ export interface IDeploymentRuntimeConfig { _content_version: string; api: { + /** Specify whether to enable communication with backend API (default true)*/ + enabled: boolean; /** Name of target db for api operations. Default `plh` */ db_name?: string; /** @@ -20,6 +22,12 @@ export interface IDeploymentRuntimeConfig { * */ endpoint?: string; }; + analytics: { + enabled: boolean; + provider: "matomo"; + endpoint: string; + siteId: number; + }; /** Optional override of any provided constants from data-models/constants */ app_config: IAppConfigOverride; /** 3rd party integration for logging services */ @@ -156,9 +164,16 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { _app_builder_version: "", name: "", api: { + enabled: true, db_name: "plh", endpoint: "https://apps-server.idems.international/api", }, + analytics: { + enabled: true, + provider: "matomo", + siteId: 1, + endpoint: "https://apps-server.idems.international/analytics", + }, app_config: {}, firebase: { diff --git a/packages/scripts/src/tasks/providers/appData.ts b/packages/scripts/src/tasks/providers/appData.ts index bb26fc59a..a51d3da94 100644 --- a/packages/scripts/src/tasks/providers/appData.ts +++ b/packages/scripts/src/tasks/providers/appData.ts @@ -60,17 +60,20 @@ const copyDeploymentDataToApp = () => { }; function generateRuntimeConfig(deploymentConfig: IDeploymentConfigJson): IDeploymentRuntimeConfig { - const { api, app_config, firebase, supabase, error_logging, git, name } = deploymentConfig; + const { analytics, api, app_config, error_logging, firebase, git, name, supabase, web } = + deploymentConfig; return { + _app_builder_version: packageJSON.version, + _content_version: git.content_tag_latest || "", + analytics, api, app_config, - firebase, - supabase, error_logging, - _app_builder_version: packageJSON.version, - _content_version: git.content_tag_latest || "", + firebase, name, + supabase, + web, }; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 322d87506..591b12936 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,15 +1,14 @@ -import { APP_INITIALIZER, ErrorHandler, NgModule } from "@angular/core"; +import { ErrorHandler, NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; import { FormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { RouteReuseStrategy } from "@angular/router"; -import { HttpClientModule } from "@angular/common/http"; +import { HTTP_INTERCEPTORS, HttpClientModule } from "@angular/common/http"; import { IonicModule, IonicRouteStrategy } from "@ionic/angular"; // Libs import { LottieModule } from "ngx-lottie"; import player from "lottie-web"; -import { MatomoModule, MatomoRouterModule } from "ngx-matomo-client"; // Native import { HTTP } from "@ionic-native/http/ngx"; @@ -19,13 +18,12 @@ import { Device } from "@ionic-native/device/ngx"; import { AppComponent } from "./app.component"; import { AppRoutingModule } from "./app-routing.module"; import { SharedModule } from "./shared/shared.module"; -import { environment } from "src/environments/environment"; -import { httpInterceptorProviders } from "./shared/services/server/interceptors"; import { TemplateComponentsModule } from "./shared/components/template/template.module"; import { ContextMenuModule } from "./shared/modules/context-menu/context-menu.module"; import { TourModule } from "./feature/tour/tour.module"; import { ErrorHandlerService } from "./shared/services/error-handler/error-handler.service"; -import { DeploymentService } from "./shared/services/deployment/deployment.service"; +import { ServerAPIInterceptor } from "./shared/services/server/interceptors"; +import { DeploymentFeaturesModule } from "./deployment-features.module"; // Note we need a separate function as it's required // by the AOT compiler. @@ -48,27 +46,16 @@ export function lottiePlayerFactory() { // LottieCacheModule.forRoot(), TemplateComponentsModule, TourModule, - MatomoModule.forRoot({ - siteId: environment.analytics.siteId, - trackerUrl: environment.analytics.endpoint, - }), - MatomoRouterModule, ContextMenuModule, + DeploymentFeaturesModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, HTTP, Device, - // ensure deployment service initialized before app component load - { - provide: APP_INITIALIZER, - multi: true, - useFactory: (deploymentService: DeploymentService) => { - return () => deploymentService.ready(); - }, - deps: [DeploymentService], - }, - httpInterceptorProviders, + // Use custom api interceptor to handle interaction with server backend + { provide: HTTP_INTERCEPTORS, useClass: ServerAPIInterceptor, multi: true }, + // Use custom error handler { provide: ErrorHandler, useClass: ErrorHandlerService }, ], bootstrap: [AppComponent], diff --git a/src/app/deployment-features.module.ts b/src/app/deployment-features.module.ts new file mode 100644 index 000000000..463e4fcbc --- /dev/null +++ b/src/app/deployment-features.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; + +import { AnalyticsModule } from "./shared/services/analytics"; + +/** + * Module imports required for specific deployment features + * + * NOTE - as angular needs all modules to be statically defined during compilation + * it is not possible to conditionally load modules at runtime. + * + * Therefore all modules are defined and loaded as part of the core build process, + * but it is still possible to override this file to create specific feature-optimised builds + * + * This is a feature marked for future implementation + */ +@NgModule({ imports: [AnalyticsModule] }) +export class DeploymentFeaturesModule {} diff --git a/src/app/shared/services/analytics/analytics.module.ts b/src/app/shared/services/analytics/analytics.module.ts new file mode 100644 index 000000000..24f0e01b1 --- /dev/null +++ b/src/app/shared/services/analytics/analytics.module.ts @@ -0,0 +1,45 @@ +import { NgModule } from "@angular/core"; + +import { + MATOMO_CONFIGURATION, + MatomoConfiguration, + provideMatomo, + withRouter, +} from "ngx-matomo-client"; + +import { IDeploymentRuntimeConfig } from "packages/data-models"; +import { DEPLOYMENT_CONFIG } from "../deployment/deployment.service"; +import { environment } from "src/environments/environment"; + +/** When running locally can configure to target local running containing (if required) */ +const devConfig: MatomoConfiguration = { + disabled: true, + trackerUrl: "http://localhost/analytics", + siteId: 1, +}; + +/** + * When configuring the analytics module + * This should be imported into the main app.module.ts + */ +@NgModule({ + imports: [], + providers: [ + provideMatomo(null, withRouter()), + // Dynamically provide the configuration used by the matomo provider so that it can + // access deployment config (injected from token) + { + provide: MATOMO_CONFIGURATION, + useFactory: (deploymentConfig: IDeploymentRuntimeConfig): MatomoConfiguration => { + if (environment.production) { + const { enabled, endpoint, siteId } = deploymentConfig.analytics; + return { disabled: !enabled, siteId, trackerUrl: endpoint }; + } else { + return devConfig; + } + }, + deps: [DEPLOYMENT_CONFIG], + }, + ], +}) +export class AnalyticsModule {} diff --git a/src/app/shared/services/analytics/index.ts b/src/app/shared/services/analytics/index.ts new file mode 100644 index 000000000..4e8738bfb --- /dev/null +++ b/src/app/shared/services/analytics/index.ts @@ -0,0 +1,2 @@ +export * from "./analytics.module"; +export * from "./analytics.service"; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 9b5eec46a..340c5e26e 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -14,5 +14,4 @@ export const environment = { domains: ["plh-demo1.idems.international", "plh-demo.idems.international"], chatNonNavigatePaths: ["/chat/action", "/chat/msg-info"], variableNameFlows: ["character_names"], - analytics: { endpoint: "https://apps-server.idems.international/analytics", siteId: 1 }, }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 6825f94d3..d0bf9509f 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -14,10 +14,6 @@ export const environment = { domains: ["plh-demo1.idems.international", "plh-demo.idems.international"], chatNonNavigatePaths: ["/chat/action", "/chat/msg-info"], variableNameFlows: ["character_names"], - /** Local Settings */ - analytics: { endpoint: "http://localhost/analytics", siteId: 1 }, - /** Production Settings **/ - // analytics: { endpoint: "https://apps-server.idems.international/analytics", siteId: 1 }, }; // This file can be replaced during build by using the `fileReplacements` array. From a2bd9ae9d5d7957579a0dfcdc680bed5cb5aedc4 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 23:17:43 -0700 Subject: [PATCH 154/204] refactor: http interceptors --- .../shared/services/server/interceptors.ts | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/app/shared/services/server/interceptors.ts b/src/app/shared/services/server/interceptors.ts index 85e5119a7..954d89cbf 100644 --- a/src/app/shared/services/server/interceptors.ts +++ b/src/app/shared/services/server/interceptors.ts @@ -1,31 +1,38 @@ -import { Injectable } from "@angular/core"; +import { Inject, Injectable } from "@angular/core"; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, - HTTP_INTERCEPTORS, HttpHeaders, } from "@angular/common/http"; -import { environment } from "src/environments/environment"; import { Observable } from "rxjs"; - -let { db_name, endpoint: API_ENDPOINT } = environment.deploymentConfig.api; - -// Override development credentials when running locally -if (!environment.production) { - // Docker endpoint. Replace :3000 with /api if running standalone api - API_ENDPOINT = "http://localhost:3000"; - db_name = "dev"; -} +import { DEPLOYMENT_CONFIG } from "../deployment/deployment.service"; +import { IDeploymentRuntimeConfig } from "packages/data-models"; /** Handle updating urls intended for api server */ @Injectable() export class ServerAPIInterceptor implements HttpInterceptor { + // Inject the global deployment config to use with requests + constructor(@Inject(DEPLOYMENT_CONFIG) private deploymentConfig: IDeploymentRuntimeConfig) {} + + /** + * Intercept all http requests to rewrite including database api endpoint and + * deployment-db-name headers, as read from deployment config + */ intercept(req: HttpRequest, next: HttpHandler): Observable> { // assume requests targetting / (e.g. /app_users) is directed to api endpoint if (req.url.startsWith("/")) { - const replacedUrl = `${API_ENDPOINT}${req.url}`; + const { db_name, endpoint, enabled } = this.deploymentConfig.api; + // If not using api silently cancel any requests to the api + // TODO - better to disable in service (could also replace interceptor with service more generally) + if (!enabled) return; + if (!db_name || !endpoint) { + console.warn("api endpoint not configured, ignoring request", req.url); + return; + } + + const replacedUrl = `${endpoint}${req.url}`; // append deployment-specific values (header set/append methods inconsistent so create new) const headerValues = { "x-deployment-db-name": db_name }; for (const key of req.headers.keys()) { @@ -37,8 +44,3 @@ export class ServerAPIInterceptor implements HttpInterceptor { return next.handle(req); } } - -/** Http interceptor providers in outside-in order */ -export const httpInterceptorProviders = [ - { provide: HTTP_INTERCEPTORS, useClass: ServerAPIInterceptor, multi: true }, -]; From 63972aed1a65fe5c68b10f8f20bdccdc6507ba90 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 23:45:18 -0700 Subject: [PATCH 155/204] chore: code tidying --- .../services/deployment/deployment.service.ts | 25 +++++++++++-------- src/main.ts | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/app/shared/services/deployment/deployment.service.ts b/src/app/shared/services/deployment/deployment.service.ts index c50287a8b..e49f9c534 100644 --- a/src/app/shared/services/deployment/deployment.service.ts +++ b/src/app/shared/services/deployment/deployment.service.ts @@ -6,23 +6,26 @@ import { SyncServiceBase } from "../syncService.base"; * Token to inject deployment config value into any service. * This is populated from json file before platform load, as part of src\main.ts * - * Can be used directly in the constructor via `@Inject(DEPLOYMENT_CONFIG)`, - * or values accessed from the DeploymentService + * Can be used directly by any service or module initialised at any time + * (including app.module.ts). + * + * @example Inject into service + * ```ts + * constructor(@Inject(DEPLOYMENT_CONFIG)) + * ``` + * @example Inject into module + * ``` + * {provide: MyModule, useFactory:(config)=>{...}, deps: [DEPLOYMENT_CONFIG]`} + * ``` */ export const DEPLOYMENT_CONFIG: InjectionToken = new InjectionToken("Application Configuration"); -@Injectable({ providedIn: "root" }) /** - * Deployment runtime config settings - * - * NOTE - this is intialized using an `APP_INITIALIZER` token within - * the main app.module.ts and will block all other services from loading until - * it is fully initialised - * - * Services that access the deployment config therefore do not need to await - * DeploymentService init/ready methods. + * The deployment service provides access to values loaded from the deployment json file + * It is an alternative to injecting directly via `@Inject(DEPLOYMENT_CONFIG)` */ +@Injectable({ providedIn: "root" }) export class DeploymentService extends SyncServiceBase { constructor(@Inject(DEPLOYMENT_CONFIG) public readonly config: IDeploymentRuntimeConfig) { super("Deployment Service"); diff --git a/src/main.ts b/src/main.ts index c01efccbf..de4c3e402 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,7 +21,7 @@ const loadConfig = async (): Promise => { return deploymentConfig; } else { console.warn("[DEPLOYMENT] config not found, using defaults"); - return { ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, app_config: {} as any }; + return DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS; } }; From c668641b1611e40bab47ca3d59e9ff4cb24f9b6d Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 13 Sep 2024 10:46:09 +0100 Subject: [PATCH 156/204] chore (docs): revert encoding on requirements.txt to utf-8 --- documentation/requirements.txt | Bin 1030 -> 487 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/documentation/requirements.txt b/documentation/requirements.txt index 2aaabae9de6b6a9e9708b63c73086ed53cc6cf1b..154629f483d9dddd928fd50f96d732712f26c017 100644 GIT binary patch literal 487 zcmZ8eyK;jt5bX6YXh1N;E>xt=#50~rmCAF03n)g92oW29eO6E<8}GKm94%0HrNOuX4?$R**TJ$&*dux) zOA%I^o&7mI$|LZAy0*idp#_je?+S$EB^g*yThR0R{(O4Xy4|V?>q>UIu=}|r!kZ;5 zL>~AK`t}iMBO~7n&$|hA0A)5Hz%E3@5+@W|=UKI~s>O{`H}*a!z;CtUVFFr0jUfL5 DpAD3& literal 1030 zcmaJ=%TB{U4D=a^PYDVeROo>NS41TwPMlCk)3PN=LrCc3%ekCMA#aic`3Zb4i8QjszZ!QH@{Y=4+05gyEJh1? zSLPesr#M-NZIMfsTzQzy=#?I3uTir_EsuDs$UZ1E9;ZBBmQa&xB%5+ibEx4JJhf`i zfzQ!{Ww07`8P`3KjcgEe6JJ!kzUd349`v1*)V z3wF*jhj#k3R*X|^Hq%Er;FkS0;4==}Bx+C9qZXKY^6>B4>|j1+ur{|Z)}>eM(3~f0 zkA0!`Buu!J0e3z)LLG6^RQfD;vXBSuViwrUU$Pxgu}1ETcJqaA?u_*uXW1djN$!Ie z@!c`^ZQ?|IKuw7bPJMBz$S~~9Sj=%##P?u8=M^4qaW>7xp>uAOIlqIpUf)t~=QtUA R7tNs~^4qX$wz1 Date: Fri, 13 Sep 2024 10:57:35 +0100 Subject: [PATCH 157/204] wip: revert documentation/requirements.txt to master --- documentation/requirements.txt | Bin 487 -> 1026 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/documentation/requirements.txt b/documentation/requirements.txt index 154629f483d9dddd928fd50f96d732712f26c017..d4fb5e9f13619fd7714eda6bd6f5cf2e40268ed2 100644 GIT binary patch literal 1026 zcmaJ=%TB{U4D=a^PYDVeROo>NS41TwPMlCk)3PN=LrCc3%ekCMA#aic`3Zb4i8QjszZ!QH@{Y=4+05gyEJh1? zSLPesr#M-NZIMfsTzQzy=#?I3uTir_EsuDs$UZ1E9;ZBBmQa&xB%5+ibEx4JJhf`i zfzQ!{Ww07`8P`3KjcgEe6JJ!kzUd349`v1*)V z3wF*jhj#k3R*X|^Hq%Er;FkS0;4==}Bx+C9qZXKY^6>B4>|j1+ur{|Z)}>eM(3~f0 zkA0!`Buu!J0e3z)LLG6^RQfD;vXBSuVy0}|Ix-zltw!F9cJhU9?u+#s=hz|2Dei+0 z@x3wUZQ4YAKuHPpr=B=fV;J^jEarApGob4V4>vfQX5!E_x5-@JL0hkHC^vJQioJ*C O&Jp=-*fZPKaT>o>UYDl; literal 487 zcmZ8eyK;jt5bX6YXh1N;E>xt=#50~rmCAF03n)g92oW29eO6E<8}GKm94%0HrNOuX4?$R**TJ$&*dux) zOA%I^o&7mI$|LZAy0*idp#_je?+S$EB^g*yThR0R{(O4Xy4|V?>q>UIu=}|r!kZ;5 zL>~AK`t}iMBO~7n&$|hA0A)5Hz%E3@5+@W|=UKI~s>O{`H}*a!z;CtUVFFr0jUfL5 DpAD3& From d819d510ff805204d2a2b3d6d6ce0ba1967656e3 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 13 Sep 2024 10:58:55 +0100 Subject: [PATCH 158/204] chore: updated documentation/requirements.txt --- documentation/requirements.txt | Bin 1026 -> 1030 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/documentation/requirements.txt b/documentation/requirements.txt index d4fb5e9f13619fd7714eda6bd6f5cf2e40268ed2..2aaabae9de6b6a9e9708b63c73086ed53cc6cf1b 100644 GIT binary patch delta 15 WcmZqTXye!*z|5@2U^JPZ`7;0=Tm#nt delta 11 ScmZqUXyVu)z&u%m`4a#Wt^;@g From c142b7359409e00341a3e7f090d8ad1fbe9d3772 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 13 Sep 2024 11:02:21 +0100 Subject: [PATCH 159/204] chore: updated .gitattributes to treat .txt files as text --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 98dea90aa..3034f90b6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,3 +16,4 @@ *.sh text eol=lf *.conf text eol=lf +*.txt text From 36ae2afcf36e69d15d7d24c6cfd8798a7eb9f20f Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 13 Sep 2024 11:05:19 +0100 Subject: [PATCH 160/204] chore: reverted .gitattributes --- .gitattributes | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 3034f90b6..98dea90aa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,4 +16,3 @@ *.sh text eol=lf *.conf text eol=lf -*.txt text From 736213212d7f737f827e0d54eb6775905dd153b1 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Fri, 13 Sep 2024 11:08:07 +0100 Subject: [PATCH 161/204] chore: changed encodeing of documentation/requirements.txt from utf-16 to utf-8 --- documentation/requirements.txt | Bin 1030 -> 490 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/documentation/requirements.txt b/documentation/requirements.txt index 2aaabae9de6b6a9e9708b63c73086ed53cc6cf1b..e270312b756b3b2a35f4170a32689f7239bc4307 100644 GIT binary patch literal 490 zcmZ8eyHdk25bX6Y*4VPgB!voEhRkG!iYjx+7h6OxWSPYIH3~k7U*IHHP~P6&YIpVX z^{p@_eb!QnS_s|<{=n1(amG|RA1xSt$4Fb=@P;X))kVr3uX)RqH$H*|DQn*Gj*U~O zv<*I{Os~0MdT_LDX}94MSF+P#CwtgLx}cp3cF<+DURTL7Jq{)IPL67U}NS41TwPMlCk)3PN=LrCc3%ekCMA#aic`3Zb4i8QjszZ!QH@{Y=4+05gyEJh1? zSLPesr#M-NZIMfsTzQzy=#?I3uTir_EsuDs$UZ1E9;ZBBmQa&xB%5+ibEx4JJhf`i zfzQ!{Ww07`8P`3KjcgEe6JJ!kzUd349`v1*)V z3wF*jhj#k3R*X|^Hq%Er;FkS0;4==}Bx+C9qZXKY^6>B4>|j1+ur{|Z)}>eM(3~f0 zkA0!`Buu!J0e3z)LLG6^RQfD;vXBSuViwrUU$Pxgu}1ETcJqaA?u_*uXW1djN$!Ie z@!c`^ZQ?|IKuw7bPJMBz$S~~9Sj=%##P?u8=M^4qaW>7xp>uAOIlqIpUf)t~=QtUA R7tNs~^4qX$wz1 Date: Fri, 13 Sep 2024 14:47:35 +0100 Subject: [PATCH 162/204] chore: update ios Podfile lock file --- ios/App/Podfile.lock | 121 +++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 57 deletions(-) diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index f5dfbe739..28b9c62be 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -8,26 +8,26 @@ PODS: - GCDWebServer (~> 3.0) - CapacitorClipboard (6.0.1): - Capacitor - - CapacitorCommunityFileOpener (1.0.5): + - CapacitorCommunityFileOpener (6.0.0): - Capacitor - CapacitorCordova (6.1.2) - CapacitorDevice (6.0.1): - Capacitor - CapacitorFilesystem (6.0.1): - Capacitor - - CapacitorFirebaseAuthentication (5.4.0): + - CapacitorFirebaseAuthentication (6.1.0): - Capacitor - - CapacitorFirebaseAuthentication/Lite (= 5.4.0) - - FirebaseAuth (~> 10.8) - - CapacitorFirebaseAuthentication/Lite (5.4.0): + - CapacitorFirebaseAuthentication/Lite (= 6.1.0) + - FirebaseAuth (~> 10.25) + - CapacitorFirebaseAuthentication/Lite (6.1.0): - Capacitor - - FirebaseAuth (~> 10.8) - - CapacitorFirebaseCrashlytics (5.4.1): + - FirebaseAuth (~> 10.25) + - CapacitorFirebaseCrashlytics (6.1.0): - Capacitor - - FirebaseCrashlytics (~> 10.8) - - CapacitorFirebasePerformance (5.4.0): + - FirebaseCrashlytics (~> 10.25) + - CapacitorFirebasePerformance (6.1.0): - Capacitor - - FirebasePerformance (~> 10.8) + - FirebasePerformance (~> 10.25) - CapacitorLocalNotifications (6.1.0): - Capacitor - CapacitorPushNotifications (6.0.2): @@ -36,65 +36,70 @@ PODS: - Capacitor - CapacitorSplashScreen (6.0.2): - Capacitor - - CapawesomeCapacitorAppUpdate (5.1.0): + - CapawesomeCapacitorAppUpdate (6.0.0): - Capacitor - - FirebaseABTesting (10.22.0): + - FirebaseABTesting (10.29.0): - FirebaseCore (~> 10.0) - - FirebaseAppCheckInterop (10.22.0) - - FirebaseAuth (10.22.0): + - FirebaseAppCheckInterop (10.29.0) + - FirebaseAuth (10.29.0): - FirebaseAppCheckInterop (~> 10.17) - FirebaseCore (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - GoogleUtilities/Environment (~> 7.8) - GTMSessionFetcher/Core (< 4.0, >= 2.1) - RecaptchaInterop (~> 100.0) - - FirebaseCore (10.22.0): + - FirebaseCore (10.29.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreExtension (10.22.0): + - FirebaseCoreExtension (10.29.0): - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.22.0): + - FirebaseCoreInternal (10.29.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseCrashlytics (10.22.0): + - FirebaseCrashlytics (10.29.0): - FirebaseCore (~> 10.5) - FirebaseInstallations (~> 10.0) + - FirebaseRemoteConfigInterop (~> 10.23) - FirebaseSessions (~> 10.5) - GoogleDataTransport (~> 9.2) - GoogleUtilities/Environment (~> 7.8) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (~> 2.1) - - FirebaseInstallations (10.22.0): + - FirebaseInstallations (10.29.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebasePerformance (10.22.0): + - FirebasePerformance (10.29.0): - FirebaseCore (~> 10.5) - FirebaseInstallations (~> 10.0) - FirebaseRemoteConfig (~> 10.0) - FirebaseSessions (~> 10.5) - GoogleDataTransport (~> 9.2) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/ISASwizzler (~> 7.8) - - GoogleUtilities/MethodSwizzler (~> 7.8) + - GoogleUtilities/Environment (~> 7.13) + - GoogleUtilities/ISASwizzler (~> 7.13) + - GoogleUtilities/MethodSwizzler (~> 7.13) + - GoogleUtilities/UserDefaults (~> 7.13) - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseRemoteConfig (10.22.0): + - FirebaseRemoteConfig (10.29.0): - FirebaseABTesting (~> 10.0) - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) + - FirebaseRemoteConfigInterop (~> 10.23) - FirebaseSharedSwift (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseSessions (10.22.0): + - FirebaseRemoteConfigInterop (10.29.0) + - FirebaseSessions (10.29.0): - FirebaseCore (~> 10.5) - FirebaseCoreExtension (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleDataTransport (~> 9.2) - - GoogleUtilities/Environment (~> 7.10) + - GoogleUtilities/Environment (~> 7.13) + - GoogleUtilities/UserDefaults (~> 7.13) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesSwift (~> 2.1) - - FirebaseSharedSwift (10.22.0) + - FirebaseSharedSwift (10.29.0) - GCDWebServer (3.5.4): - GCDWebServer/Core (= 3.5.4) - GCDWebServer/Core (3.5.4) @@ -102,37 +107,37 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.13.0): + - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/Environment (7.13.3): - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/ISASwizzler (7.13.0): + - GoogleUtilities/ISASwizzler (7.13.3): - GoogleUtilities/Privacy - - GoogleUtilities/Logger (7.13.0): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/MethodSwizzler (7.13.0): + - GoogleUtilities/MethodSwizzler (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/Network (7.13.0): + - GoogleUtilities/Network (7.13.3): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.13.0)": + - "GoogleUtilities/NSData+zlib (7.13.3)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.0) - - GoogleUtilities/Reachability (7.13.0): + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (7.13.0): + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GTMSessionFetcher/Core (3.3.1) + - GTMSessionFetcher/Core (3.5.0) - nanopb (2.30910.0): - nanopb/decode (= 2.30910.0) - nanopb/encode (= 2.30910.0) @@ -174,6 +179,7 @@ SPEC REPOS: - FirebaseInstallations - FirebasePerformance - FirebaseRemoteConfig + - FirebaseRemoteConfigInterop - FirebaseSessions - FirebaseSharedSwift - GCDWebServer @@ -224,34 +230,35 @@ SPEC CHECKSUMS: CapacitorApp: 0bc633b4eae40a1f32cd2834788fad3bc42da6a1 CapacitorBlobWriter: 110eeaf80611f19bf01a8a05ff3672149ed0baad CapacitorClipboard: 756cd7e83e8d5d19b0c74f40b57517c287bd5fe2 - CapacitorCommunityFileOpener: 8ae87db61961a6166cb929cc16e3cc719ea58da1 + CapacitorCommunityFileOpener: 4e0086fac78b4ccaaf64956abe34d3443db5767a CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd CapacitorDevice: 7097a1deb4224b77fd13a6e60a355d0062a5d772 CapacitorFilesystem: 37fb3aa5c945b4539ab11c74a5c57925a302bf24 - CapacitorFirebaseAuthentication: 64eaf9727b8f29f84069595ddc239c8e5365f49a - CapacitorFirebaseCrashlytics: 643d7e63836ae9608e10b9c3e40dfbae8679589e - CapacitorFirebasePerformance: 964b215796522c83767a78ad629b9581558b391a + CapacitorFirebaseAuthentication: 731483d98bf879a1c1b071c1efedd4bd5b0da6f2 + CapacitorFirebaseCrashlytics: a4c495104fbe5cb28967ae24ba1364a35d3c1173 + CapacitorFirebasePerformance: c806ce7f8270295465c050210d8e8a4ae2dc282e CapacitorLocalNotifications: 6bac9e948b2b8852506c6d74abb2cde140250f86 CapacitorPushNotifications: ccd797926c030acad3d5498ef452c735c90a2c89 CapacitorShare: 591ae4693d85686ceb590db8e8b44aa014ec6490 CapacitorSplashScreen: 250df9ef8014fac5c7c1fd231f0f8b1d8f0b5624 - CapawesomeCapacitorAppUpdate: 04f2c4b7942ccc72a86bcd553ab3c546df422838 - FirebaseABTesting: 66d2594b36d4ff6e7d3c8719802100990de05857 - FirebaseAppCheckInterop: 58db3e9494751399cf3e7b7e3e705cff71099153 - FirebaseAuth: bbe4c68f958504ba9e54aee181adbdf5b664fbc6 - FirebaseCore: 0326ec9b05fbed8f8716cddbf0e36894a13837f7 - FirebaseCoreExtension: 6394c00b887d0bebadbc7049c464aa0cbddc5d41 - FirebaseCoreInternal: bca337352024b18424a61e478460547d46c4c753 - FirebaseCrashlytics: e568d68ce89117c80cddb04073ab9018725fbb8c - FirebaseInstallations: 763814908793c0da14c18b3dcffdec71e29ed55e - FirebasePerformance: 095debad1fc8d7d73148a835fcaec9e528946166 - FirebaseRemoteConfig: e1b992a94d3674dddbcaf5d0d31a0312156ceb1c - FirebaseSessions: cd97fb07674f3906619c871eefbd260a1546c9d3 - FirebaseSharedSwift: 48076404e6e52372290d15a07d2ed1d2f1754023 + CapawesomeCapacitorAppUpdate: 3c05b5c8e42f9c6a88d666093406e9336d9bfdb1 + FirebaseABTesting: d87f56707159bae64e269757a6e963d490f2eebe + FirebaseAppCheckInterop: 6a1757cfd4067d8e00fccd14fcc1b8fd78cfac07 + FirebaseAuth: e2ebfaf9fb4638a1c9a3b0efd17d1b90943987cd + FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16 + FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f + FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 + FirebaseCrashlytics: 34647b41e18de773717fdd348a22206f2f9bc774 + FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd + FirebasePerformance: d0ac4aa90f8c1aedeb8d0329a56e2d77d8d9e004 + FirebaseRemoteConfig: 48ef3f243742a8d72422ccfc9f986e19d7de53fd + FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d + FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc + FirebaseSharedSwift: 20530f495084b8d840f78a100d8c5ee613375f6e GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 - GTMSessionFetcher: 8a1b34ad97ebe6f909fb8b9b77fba99943007556 + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 nanopb: 438bc412db1928dac798aa6fd75726007be04262 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 From 4d6f6a7b335e2de92befa14f8bcbbb67f197a1e1 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 13 Sep 2024 10:45:00 -0700 Subject: [PATCH 163/204] refactor: pr labeller actions --- .github/workflows/gh-pr-label-paths.yml | 22 +++++++++++ .github/workflows/gh-pr-label-title.yml | 51 +++++++++++++++++++++++++ .github/workflows/gh-pr-labeler.yml | 44 --------------------- 3 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/gh-pr-label-paths.yml create mode 100644 .github/workflows/gh-pr-label-title.yml delete mode 100644 .github/workflows/gh-pr-labeler.yml diff --git a/.github/workflows/gh-pr-label-paths.yml b/.github/workflows/gh-pr-label-paths.yml new file mode 100644 index 000000000..89aef59c2 --- /dev/null +++ b/.github/workflows/gh-pr-label-paths.yml @@ -0,0 +1,22 @@ +# Automatically apply labels to PR based on filepaths of modified files +name: Github PR Label Paths +on: + pull_request: +jobs: + label: + if: ${{ !contains(github.event.pull_request.labels.*.name , 'no-autolabel')}} + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + # Checkout repo to access local templates (if updated) + - name: checkout + uses: actions/checkout@main + # Assign labels based on files modified + # https://github.com/actions/labeler + - name: Assign PR path Labels + uses: actions/labeler@v5 + with: + configuration-path: .github/pr-labeler.config.yml + sync-labels: false diff --git a/.github/workflows/gh-pr-label-title.yml b/.github/workflows/gh-pr-label-title.yml new file mode 100644 index 000000000..5e60e63f2 --- /dev/null +++ b/.github/workflows/gh-pr-label-title.yml @@ -0,0 +1,51 @@ +# Automatically apply labels to PR based on title (if written in semantic format) +name: Github PR Label Title +on: + pull_request: + # https://frontside.com/blog/2020-05-26-github-actions-pull_request/ + types: + - opened + - edited +jobs: + label: + # Avoid re-label if title not changed or using custom label to prevent + # https://github.com/orgs/community/discussions/101695 + if: | + ${{ !contains(github.event.pull_request.labels.*.name , 'no-autolabel')}} && + (event.type == 'opened' || event.changes.title.from) + runs-on: ubuntu-latest + steps: + # Checkout repo to access local templates (if updated) + - name: checkout + uses: actions/checkout@main + # Check if PR title matches conventional commit standard + # Not strictly enforced so allow continue on error + # https://github.com/marketplace/actions/semantic-pull-request + - name: Validate PR title + uses: amannn/action-semantic-pull-request@v5 + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Assign labels based on conventional PR title + # https://github.com/marketplace/actions/conventional-release-labels + - name: Assign PR Name Labels + uses: bcoe/conventional-release-labels@v1.3.1 + with: + # Labels assigned based on pr name prefix + type_labels: | + { + "breaking": "breaking", + "chore": "maintenance", + "docs": "documentation", + "feat": "feature", + "fix": "fix", + "refactor": "maintenance", + "Breaking": "breaking", + "Chore": "maintenance", + "Docs": "documentation", + "Feat": "feature", + "Fix": "fix", + "Refactor": "maintenance" + } + # Do not ignore any labels (default ignores chore:) + ignored_types: '[]' diff --git a/.github/workflows/gh-pr-labeler.yml b/.github/workflows/gh-pr-labeler.yml deleted file mode 100644 index 6ecd6b7ec..000000000 --- a/.github/workflows/gh-pr-labeler.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Github PR Labeler -on: - # TODO - replace with pull_request_target when working in master - pull_request: - types: - - opened - - edited - - synchronize -jobs: - label: - runs-on: ubuntu-latest - steps: - # Check if PR title matches conventional commit standard - # Not strictly enforced so allow continue on error - # https://github.com/marketplace/actions/semantic-pull-request - - name: Validate PR title - uses: amannn/action-semantic-pull-request@v5 - continue-on-error: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Assign labels based on conventional PR title - # https://github.com/marketplace/actions/conventional-release-labels - - name: Assign PR Name Labels - uses: bcoe/conventional-release-labels@v1.3.1 - with: - # Labels assigned based on pr name prefix - type_labels: | - { - "breaking": "breaking", - "chore": "maintenance", - "docs": "documentation", - "feat": "feature", - "fix": "fix", - "refactor": "maintenance" - } - # Do not ignore any labels (default ignores chore:) - ignored_types: '[]' - # Assign labels based on files modified - - name: Assign PR path Labels - uses: actions/labeler@v4 - with: - configuration-path: .github/pr-labeler.config.yml - - \ No newline at end of file From faab8a320af225160d8f2d215ad57fd7fd394428 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 13 Sep 2024 11:03:20 -0700 Subject: [PATCH 164/204] chore: update labeller config --- .github/pr-labeler.config.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/pr-labeler.config.yml b/.github/pr-labeler.config.yml index 98b7d9b45..5396f0eb5 100644 --- a/.github/pr-labeler.config.yml +++ b/.github/pr-labeler.config.yml @@ -1,3 +1,12 @@ -# Additional labels to assign based on file changes -"documentation": - - documentation/**/* +# Patterns to match to auto-assign labels based on path changes +# https://github.com/actions/labeler/tree/v5/?tab=readme-ov-file#match-object + +# Documentation +'documentation': + - changed-files: + - any-glob-to-any-file: 'documentation/**/*' + +# Scripts +'scripts': + - changed-files: + - any-glob-to-any-file: 'packages/scripts/**/*' \ No newline at end of file From 48c93e62ae281fa02d846b2477cf4df130483e86 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 13 Sep 2024 11:22:24 -0700 Subject: [PATCH 165/204] chore: update pr label title trigger --- .github/workflows/gh-pr-label-title.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gh-pr-label-title.yml b/.github/workflows/gh-pr-label-title.yml index 5e60e63f2..8dee66e10 100644 --- a/.github/workflows/gh-pr-label-title.yml +++ b/.github/workflows/gh-pr-label-title.yml @@ -5,7 +5,7 @@ on: # https://frontside.com/blog/2020-05-26-github-actions-pull_request/ types: - opened - - edited + - ready_for_review jobs: label: # Avoid re-label if title not changed or using custom label to prevent From e7ab562b951928b9707540507552b69e19da867d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 13 Sep 2024 11:27:44 -0700 Subject: [PATCH 166/204] chore: update pr path label triggers --- .github/workflows/gh-pr-label-paths.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/gh-pr-label-paths.yml b/.github/workflows/gh-pr-label-paths.yml index 89aef59c2..55f6f46c1 100644 --- a/.github/workflows/gh-pr-label-paths.yml +++ b/.github/workflows/gh-pr-label-paths.yml @@ -2,6 +2,10 @@ name: Github PR Label Paths on: pull_request: + # https://frontside.com/blog/2020-05-26-github-actions-pull_request/ + types: + - opened + - synchronize jobs: label: if: ${{ !contains(github.event.pull_request.labels.*.name , 'no-autolabel')}} From 781c84862a4ff394cf7c3ae78c76e43eb99908b2 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 14 Sep 2024 12:54:24 -0700 Subject: [PATCH 167/204] feat: add object-utils and spec tests --- .../shared/src/utils/object-utils.spec.ts | 55 ++++++++++++++++ packages/shared/src/utils/object-utils.ts | 64 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 packages/shared/src/utils/object-utils.spec.ts create mode 100644 packages/shared/src/utils/object-utils.ts diff --git a/packages/shared/src/utils/object-utils.spec.ts b/packages/shared/src/utils/object-utils.spec.ts new file mode 100644 index 000000000..9f6509cfa --- /dev/null +++ b/packages/shared/src/utils/object-utils.spec.ts @@ -0,0 +1,55 @@ +import { + cleanEmptyObject, + isEmptyObjectDeep, + isObjectLiteral, + toEmptyObject, +} from "./object-utils"; + +const MOCK_NESTED_OBJECT = { + obj_1: { + obj_1_1: { + number: 2, + obj_1_1_1: {}, + }, + obj_1_2: { + obj_1_2_1: {}, + }, + }, + string: "hi", + number: 1, +}; + +describe("Object Utils", () => { + it("isObjectLiteral", () => { + expect(isObjectLiteral({})).toEqual(true); + expect(isObjectLiteral({ string: "hello" })).toEqual(true); + expect(isObjectLiteral(undefined)).toEqual(false); + expect(isObjectLiteral([])).toEqual(false); + expect(isObjectLiteral(new Date())).toEqual(false); + }); + + it("isEmptyObjectDeep", () => { + expect(isEmptyObjectDeep({})).toEqual(true); + expect(isEmptyObjectDeep(undefined)).toEqual(false); + expect(isEmptyObjectDeep({ key: { key: { key: {} } } })).toEqual(true); + expect(isEmptyObjectDeep({ key: { key: { key: undefined } } })).toEqual(false); + }); + + it("toEmptyObject", () => { + const res = toEmptyObject(MOCK_NESTED_OBJECT); + expect(res).toEqual({ obj_1: { obj_1_1: { obj_1_1_1: {} }, obj_1_2: { obj_1_2_1: {} } } }); + }); + + it("cleanEmptyObject", () => { + const res = cleanEmptyObject(MOCK_NESTED_OBJECT); + expect(res).toEqual({ + obj_1: { + obj_1_1: { + number: 2, + }, + }, + string: "hi", + number: 1, + }); + }); +}); diff --git a/packages/shared/src/utils/object-utils.ts b/packages/shared/src/utils/object-utils.ts new file mode 100644 index 000000000..6add2ddeb --- /dev/null +++ b/packages/shared/src/utils/object-utils.ts @@ -0,0 +1,64 @@ +/** + * Determine whether a value is a literal object type (`{}`) + * Adapted from discussion https://stackoverflow.com/q/1173549 + */ +export function isObjectLiteral(v: any) { + return v ? v.constructor === {}.constructor : false; +} + +/** Check if an object is either empty or contains only empty child */ +export function isEmptyObjectDeep(v: any) { + return isObjectLiteral(v) && Object.values(v).every((x) => isEmptyObjectDeep(x)); +} + +/** + * Takes a json object and empties all data inside, just leaving nested entry nodes + * This is used to create placeholder objects for deeply nested partial configurations + * @example + * ```ts + * const obj = {parent:{text:'hello',obj:{number:1}}} + * toEmptyObject(obj) + * // output + * {parent:{obj:{}}} + * ``` + ***/ +export function toEmptyObject>(obj: T) { + const emptied = {} as any; + if (isObjectLiteral(obj)) { + for (const [key, value] of Object.entries(obj)) { + if (isObjectLiteral(value)) { + emptied[key] = toEmptyObject(value); + } + } + } else { + console.error("[toEmptyObject] invalid input: " + obj); + return obj; + } + return emptied as T; +} + +/** + * Takes an input object with deeply nested keys and removes all child entries + * that are either empty `{}` or contain only empty child entries `{nested:{}}` + * @example + * ```ts + * + * ``` + */ +export function cleanEmptyObject(obj: Record) { + const cleaned = {} as any; + if (obj.constructor === {}.constructor) { + for (const [key, value] of Object.entries(obj)) { + if (value.constructor === {}.constructor) { + if (!isEmptyObjectDeep(value)) { + cleaned[key] = cleanEmptyObject(value); + } + } else { + cleaned[key] = value; + } + } + } else { + return cleaned; + } + return cleaned; +} From 612dc447d91e9f5bc5531848ed751758595b921a Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 14 Sep 2024 13:08:34 -0700 Subject: [PATCH 168/204] refactor: deployment generated app_config Change how deployments populate default app_config, preferring to generate an empty config skeleton that can be overwritten instead of a full app_config. This makes it easier to then store only the changed config values when writing to disk --- packages/data-models/appConfig.ts | 17 +++++++++-------- packages/data-models/deployment.model.ts | 13 ++++++++++--- packages/data-models/flowTypes.ts | 2 +- packages/data-models/functions.ts | 3 ++- .../scripts/src/commands/deployment/common.ts | 16 ++++++++++++---- .../scripts/src/commands/deployment/compile.ts | 4 ++++ packages/scripts/types/index.ts | 2 +- packages/shared/src/types/index.ts | 7 +++++++ src/app/shared/model/index.ts | 5 ++++- 9 files changed, 50 insertions(+), 19 deletions(-) diff --git a/packages/data-models/appConfig.ts b/packages/data-models/appConfig.ts index 8555bf476..6b9b6781f 100644 --- a/packages/data-models/appConfig.ts +++ b/packages/data-models/appConfig.ts @@ -1,6 +1,7 @@ /// import APP_CONFIG_GLOBALS from "./app-config/globals"; import clone from "clone"; +import { type RecursivePartial } from "shared/src/types"; import { IAppSkin } from "./skin.model"; /********************************************************************************************* @@ -222,14 +223,14 @@ const APP_CONFIG = { SERVER_SYNC_FREQUENCY_MS, TASKS, }; -// Export as a clone to avoid risk one import could alter another -export const getDefaultAppConfig = () => clone(APP_CONFIG); -export type IAppConfig = typeof APP_CONFIG; -/** A recursive version of Partial, making all properties, included nested ones, optional. - * Copied from https://stackoverflow.com/a/47914631 +/** + * Get full app config populated with default values + * Returned as an editable clone so that changes will not impact original */ -export type RecursivePartial = { - [P in keyof T]?: RecursivePartial; -}; +export const getDefaultAppConfig = (): IAppConfig => clone(APP_CONFIG); + +export type IAppConfig = typeof APP_CONFIG; + +/** Config overrides support deep-nested partials, merged with defaults at runtime */ export type IAppConfigOverride = RecursivePartial; diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 46ac67f3f..bb5271e85 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -1,8 +1,8 @@ import type { IGdriveEntry } from "../@idemsInternational/gdrive-tools"; -import type { IAppConfigOverride } from "./appConfig"; +import type { IAppConfig, IAppConfigOverride } from "./appConfig"; /** Update version to force recompile next time deployment set (e.g. after default config update) */ -export const DEPLOYMENT_CONFIG_VERSION = 20240912.0; +export const DEPLOYMENT_CONFIG_VERSION = 20240914.0; /** Configuration settings available to runtime application */ export interface IDeploymentRuntimeConfig { @@ -152,6 +152,12 @@ interface IDeploymentCoreConfig { export type IDeploymentConfig = IDeploymentCoreConfig & IDeploymentRuntimeConfig; +/** + * Generated config includes placeholders for all app_config entries to allow specific + * overrides for deeply nested properties, e.g. `app_config.NOTIFICATION_DEFAULTS.time.hour` + */ +export type IDeploymentConfigGenerated = IDeploymentConfig & { app_config: IAppConfig }; + /** Deployment with additional metadata when set as active deployment */ export interface IDeploymentConfigJson extends IDeploymentConfig { _workspace_path: string; @@ -175,7 +181,6 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { endpoint: "https://apps-server.idems.international/analytics", }, app_config: {}, - firebase: { config: null, auth: { enabled: false }, @@ -190,6 +195,8 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { /** Full example of just all config once merged with defaults */ export const DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS: IDeploymentConfig = { ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, + // NOTE - app_config will be populated during config generation + app_config: {} as any, name: "Full Config Example", google_drive: { assets_folder_id: "", diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 5a2905151..dd6f2411d 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -2,7 +2,7 @@ import type { IDataPipeOperation } from "shared"; import type { IAppConfig } from "./appConfig"; -import { IAssetEntry } from "./deployment.model"; +import type { IAssetEntry } from "./deployment.model"; /********************************************************************************************* * Base flow types diff --git a/packages/data-models/functions.ts b/packages/data-models/functions.ts index 1d6940e02..9131065e8 100644 --- a/packages/data-models/functions.ts +++ b/packages/data-models/functions.ts @@ -71,6 +71,7 @@ export function extractDynamicFields(data: any): FlowTypes.IDynamicField | undef export function extractDynamicEvaluators( fullExpression: string ): FlowTypes.TemplateRowDynamicEvaluator[] | null { + const appConfigDefault = getDefaultAppConfig(); // match fields such as @local.someField // deeper nesting will be need to be handled after evaluation as part of JSEvaluation // (e.g. @local.somefield.nestedProperty or even !@local.@local.dynamicNested) @@ -89,7 +90,7 @@ export function extractDynamicEvaluators( type = "raw"; } // cross-check to ensure lookup matches one of the pre-defined dynamic field types (e.g. not email@domain.com) - if (!getDefaultAppConfig().DYNAMIC_PREFIXES.includes(type)) { + if (!appConfigDefault.DYNAMIC_PREFIXES.includes(type)) { return undefined; } return { fullExpression, matchedExpression, type, fieldName }; diff --git a/packages/scripts/src/commands/deployment/common.ts b/packages/scripts/src/commands/deployment/common.ts index efc584fbe..1e39febaa 100644 --- a/packages/scripts/src/commands/deployment/common.ts +++ b/packages/scripts/src/commands/deployment/common.ts @@ -4,19 +4,27 @@ import { readJSONSync } from "fs-extra"; import path from "path"; import { DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS, getDefaultAppConfig } from "data-models"; -import type { IDeploymentConfig, IDeploymentConfigJson } from "data-models"; +import type { + IDeploymentConfig, + IDeploymentConfigGenerated, + IDeploymentConfigJson, +} from "data-models"; import { DEPLOYMENTS_PATH } from "../../paths"; import { getStackFileNames, loadDeploymentJson } from "./utils"; +import { toEmptyObject } from "shared/src/utils/object-utils"; // re-export of type for convenience export type { IDeploymentConfigJson }; /** Create a new deployment config with default values */ -export function generateDeploymentConfig(name: string): IDeploymentConfig { - const config = DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS; +export function generateDeploymentConfig(name: string) { + // populate placeholder properties for all nested appConfig to make it easier to + // apply overrides to single nested properties + const app_config = toEmptyObject(getDefaultAppConfig()); + // combine with deployment config defaults + const config: IDeploymentConfigGenerated = { ...DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS, app_config }; config.name = name; - config.app_config = getDefaultAppConfig(); return config; } diff --git a/packages/scripts/src/commands/deployment/compile.ts b/packages/scripts/src/commands/deployment/compile.ts index 8605778e9..c59dea192 100644 --- a/packages/scripts/src/commands/deployment/compile.ts +++ b/packages/scripts/src/commands/deployment/compile.ts @@ -8,6 +8,7 @@ import { ROOT_DIR } from "../../paths"; import { Logger } from "../../utils"; import { IDeploymentConfigJson } from "./common"; import { convertFunctionsToStrings } from "./utils"; +import { cleanEmptyObject } from "shared/src/utils/object-utils"; const program = new Command("compile"); interface IOptions { @@ -82,6 +83,9 @@ function convertDeploymentTsToJson( const converted = convertFunctionsToStrings(rewritten); + // remove empty placeholders populated by override config + converted.app_config = cleanEmptyObject(converted.app_config); + return { ...converted, _workspace_path, _config_ts_path, _config_version }; } diff --git a/packages/scripts/types/index.ts b/packages/scripts/types/index.ts index ea4003f0c..0d8c009d1 100644 --- a/packages/scripts/types/index.ts +++ b/packages/scripts/types/index.ts @@ -1,2 +1,2 @@ // Provide access to FlowTypes used in the app -export { FlowTypes, IDeploymentConfig, DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS } from "data-models"; +export { FlowTypes, IDeploymentConfig } from "data-models"; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 9ccbcebf4..f9066471e 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -2,3 +2,10 @@ export interface ITemplatedStringVariable { value?: string; variables?: { [key: string]: ITemplatedStringVariable }; } + +/** A recursive version of Partial, making all properties, included nested ones, optional. + * Copied from https://stackoverflow.com/a/47914631 + */ +export type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; diff --git a/src/app/shared/model/index.ts b/src/app/shared/model/index.ts index e84c4f486..a6da3fa74 100644 --- a/src/app/shared/model/index.ts +++ b/src/app/shared/model/index.ts @@ -1 +1,4 @@ -export * from "data-models"; +// Limited re-export of some types from data-models for local use + +export { FlowTypes } from "data-models/flowTypes"; +export type { IAppConfig } from "data-models/appConfig"; From ad97fc35480f82c8ac63cb48b9bfe16c2e41b5d7 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 14 Sep 2024 13:21:06 -0700 Subject: [PATCH 169/204] refactor: app-route config changes --- src/app/app-routing.module.ts | 23 +++----- .../services/app-config/app-config.utils.ts | 17 ++++++ src/app/shared/services/skin/skin.service.ts | 52 +------------------ 3 files changed, 27 insertions(+), 65 deletions(-) create mode 100644 src/app/shared/services/app-config/app-config.utils.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index cd0fdc76f..d754629e8 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,20 +1,13 @@ import { NgModule } from "@angular/core"; -import { PreloadAllModules, Route, RouterModule, Routes } from "@angular/router"; -import { APP_CONFIG } from "./data"; +import { PreloadAllModules, RouterModule, Routes } from "@angular/router"; import { TourComponent } from "./feature/tour/tour.component"; -// TODO: These should come from the appConfigService -const { APP_ROUTE_DEFAULTS } = APP_CONFIG; - -/** Routes specified from data-models */ -const DataRoutes: Routes = [ - { path: "", redirectTo: APP_ROUTE_DEFAULTS.home_route, pathMatch: "full" }, - ...APP_ROUTE_DEFAULTS.redirects, -]; -const fallbackRoute: Route = { path: "**", redirectTo: APP_ROUTE_DEFAULTS.fallback_route }; - -/** Routes required for main app features */ -const FeatureRoutes: Routes = [ +/** + * Routes required for main app features + * Additional home template redirects and fallback routes will be specified + * from deployment config via the AppConfigService + **/ +export const APP_FEATURE_ROUTES: Routes = [ { path: "campaigns", loadChildren: () => import("./feature/campaign/campaign.module").then((m) => m.CampaignModule), @@ -68,7 +61,7 @@ const FeatureRoutes: Routes = [ @NgModule({ imports: [ - RouterModule.forRoot([...FeatureRoutes, ...DataRoutes, fallbackRoute], { + RouterModule.forRoot(APP_FEATURE_ROUTES, { preloadingStrategy: PreloadAllModules, useHash: false, anchorScrolling: "enabled", diff --git a/src/app/shared/services/app-config/app-config.utils.ts b/src/app/shared/services/app-config/app-config.utils.ts new file mode 100644 index 000000000..1b0c178f2 --- /dev/null +++ b/src/app/shared/services/app-config/app-config.utils.ts @@ -0,0 +1,17 @@ +import { Router, Routes } from "@angular/router"; +import { IAppConfig } from "packages/data-models"; +import { APP_FEATURE_ROUTES } from "src/app/app-routing.module"; + +/** + * Update app routing to include redirects, home and fallback routes specified in config + */ +export const updateRoutingDefaults = (config: IAppConfig["APP_ROUTE_DEFAULTS"], router: Router) => { + const routes: Routes = [ + ...APP_FEATURE_ROUTES, + ...config.redirects, + { path: "", redirectTo: config.home_route, pathMatch: "full" }, + { path: "**", redirectTo: config.fallback_route }, + ]; + + return router.resetConfig(routes); +}; diff --git a/src/app/shared/services/skin/skin.service.ts b/src/app/shared/services/skin/skin.service.ts index 443bb87ee..0d910b599 100644 --- a/src/app/shared/services/skin/skin.service.ts +++ b/src/app/shared/services/skin/skin.service.ts @@ -5,8 +5,6 @@ import { IAppConfig, IAppSkin } from "data-models"; import { arrayToHashmap } from "../../utils"; import { AppConfigService } from "../app-config/app-config.service"; import { TemplateService } from "../../components/template/services/template.service"; -import { Router } from "@angular/router"; -import { APP_CONFIG } from "src/app/data"; import { ThemeService } from "src/app/feature/theme/services/theme.service"; import { SyncServiceBase } from "../syncService.base"; @@ -24,8 +22,7 @@ export class SkinService extends SyncServiceBase { private localStorageService: LocalStorageService, private appConfigService: AppConfigService, private templateService: TemplateService, - private themeService: ThemeService, - private router: Router + private themeService: ThemeService ) { super("Skin Service"); this.initialise(); @@ -57,12 +54,11 @@ export class SkinService extends SyncServiceBase { * */ public setSkin(skinName: string, isInit = false) { if (skinName in this.availableSkins) { - const oldSkin = this.activeSkin$.value; const targetSkin = this.availableSkins[skinName]; // console.log("[SET SKIN]", skinName, targetSkin); this.activeSkin$.next(targetSkin); // Update appConfig to reflect any overrides defined by the skin - this.appConfigService.updateAppConfig(targetSkin.appConfig); + this.appConfigService.setAppConfig(targetSkin.appConfig); if (!isInit) { // Update default values when skin changed to allow for skin-specific global overrides // Don't run on initialisation, since the skin and appConfig services must init before the template service and its dependencies @@ -72,7 +68,6 @@ export class SkinService extends SyncServiceBase { } // Use local storage so that the active skin persists across app launches this.localStorageService.setProtected("APP_SKIN", targetSkin.name); - this.updateRoutingDefaults(targetSkin, oldSkin); } else { console.error(`No skin found with name "${skinName}"`, { availableSkins: this.availableSkins, @@ -80,49 +75,6 @@ export class SkinService extends SyncServiceBase { } } - /** Override changes to config-dependent routing config inherited in app-routing.module */ - private updateRoutingDefaults(newSkin?: IAppSkin, oldSkin?: IAppSkin) { - const newRouteDefaults = newSkin?.appConfig?.APP_ROUTE_DEFAULTS; - let routes = this.router.config; - if (newRouteDefaults) { - const { APP_ROUTE_DEFAULTS: oldRouteDefaults } = oldSkin?.appConfig || APP_CONFIG; - // Replace default home route - // { path: "", redirectTo: APP_ROUTE_DEFAULTS.home_route, pathMatch: "full" }, - if ( - newRouteDefaults.home_route && - newRouteDefaults.home_route !== oldRouteDefaults.home_route - ) { - const homeRouteIndex = routes.findIndex((route) => route.path === ""); - if (homeRouteIndex > -1) { - routes[homeRouteIndex].redirectTo = newRouteDefaults.home_route; - } - } - // Replace fallbackRoute - // { path: "**", redirectTo: APP_ROUTE_DEFAULTS.fallback_route }; - if ( - newRouteDefaults.fallback_route && - newRouteDefaults.fallback_route !== oldRouteDefaults.fallback_route - ) { - const fallbackRouteIndex = routes.findIndex((route) => route.path === "**"); - if (fallbackRouteIndex > -1) { - routes[fallbackRouteIndex].redirectTo = newRouteDefaults.fallback_route; - } - } - if (newRouteDefaults.redirects) { - // Remove old redirects - if (oldRouteDefaults.redirects) { - const redirectedPaths = oldRouteDefaults.redirects.map((route) => route.path); - routes = routes.filter((route) => !redirectedPaths.includes(route.path)); - } - // Add new redirects - for (const { path, redirectTo } of newRouteDefaults.redirects) { - routes.push({ path, redirectTo }); - } - } - this.router.resetConfig(routes); - } - } - /** Get the name of the active skin, as saved in local storage */ public getActiveSkinName() { return this.localStorageService.getProtected("APP_SKIN"); From b08fd61e106bc1ae023c6846787bab6222b2f3be Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 14 Sep 2024 13:27:28 -0700 Subject: [PATCH 170/204] chore: remove legacy environment variables --- src/app/data/constants.ts | 8 -------- src/app/data/index.ts | 1 - src/environments/environment.prod.ts | 14 -------------- src/environments/environment.ts | 14 -------------- 4 files changed, 37 deletions(-) delete mode 100644 src/app/data/constants.ts diff --git a/src/app/data/constants.ts b/src/app/data/constants.ts deleted file mode 100644 index b3401ca6b..000000000 --- a/src/app/data/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getDefaultAppConfig, IAppConfig } from "data-models"; -import { environment } from "src/environments/environment"; -import { deepMergeObjects } from "../shared/utils"; - -const app_config_overrides = (environment.deploymentConfig as any).app_config || {}; - -/** List of constants provided by data-models combined with deployment-specific overrides */ -export const APP_CONFIG: IAppConfig = deepMergeObjects(getDefaultAppConfig(), app_config_overrides); diff --git a/src/app/data/index.ts b/src/app/data/index.ts index 9c2355a44..a22008418 100644 --- a/src/app/data/index.ts +++ b/src/app/data/index.ts @@ -1,4 +1,3 @@ -export * from "./constants"; export * from "./app-data"; // Not used but forces angular to reload when asset jsons changed diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 340c5e26e..c9669790b 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,17 +1,3 @@ -import deploymentJson from "../../.idems_app/deployments/activeDeployment.json"; -import type { IDeploymentConfig } from "data-models"; - export const environment = { - // HACK - json config converts functions to strings, not strongly typed - deploymentConfig: deploymentJson as any as IDeploymentConfig, production: true, - rapidPro: { - receiveUrl: - "https://rapidpro.idems.international/c/fcm/a459e9bf-6462-41fe-9bde-98dbed64e687/receive", - contactRegisterUrl: - "https://rapidpro.idems.international/c/fcm/a459e9bf-6462-41fe-9bde-98dbed64e687/register", - }, - domains: ["plh-demo1.idems.international", "plh-demo.idems.international"], - chatNonNavigatePaths: ["/chat/action", "/chat/msg-info"], - variableNameFlows: ["character_names"], }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index d0bf9509f..7262906f0 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,19 +1,5 @@ -import deploymentJson from "../../.idems_app/deployments/activeDeployment.json"; -import type { IDeploymentConfig } from "data-models"; - export const environment = { - // HACK - json config converts functions to strings, not strongly typed - deploymentConfig: deploymentJson as any as IDeploymentConfig, production: false, - rapidPro: { - receiveUrl: - "https://rapidpro.idems.international/c/fcm/a459e9bf-6462-41fe-9bde-98dbed64e687/receive", - contactRegisterUrl: - "https://rapidpro.idems.international/c/fcm/a459e9bf-6462-41fe-9bde-98dbed64e687/register", - }, - domains: ["plh-demo1.idems.international", "plh-demo.idems.international"], - chatNonNavigatePaths: ["/chat/action", "/chat/msg-info"], - variableNameFlows: ["character_names"], }; // This file can be replaced during build by using the `fileReplacements` array. From d9f60e3e35ed7cd87867b563e3551cca45f07fb6 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 14 Sep 2024 13:36:26 -0700 Subject: [PATCH 171/204] refactor: appConfigService --- .../components/header/header.component.ts | 12 ++- .../services/app-config/app-config.service.ts | 77 ++++++++++++------- 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/app/shared/components/header/header.component.ts b/src/app/shared/components/header/header.component.ts index 5beb297a8..755835bd1 100644 --- a/src/app/shared/components/header/header.component.ts +++ b/src/app/shared/components/header/header.component.ts @@ -3,8 +3,12 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { NavigationEnd, NavigationStart, Router } from "@angular/router"; import { App } from "@capacitor/app"; import { Capacitor, PluginListenerHandle } from "@capacitor/core"; -import { Subscription, fromEvent, debounceTime, map } from "rxjs"; -import { IAppConfig, IHeaderColourOptions, IHeaderVariantOptions } from "../../model"; +import { Subscription, fromEvent, map } from "rxjs"; +import type { + IAppConfig, + IHeaderColourOptions, + IHeaderVariantOptions, +} from "data-models/appConfig"; import { AppConfigService } from "../../services/app-config/app-config.service"; import { IonHeader, ScrollBaseCustomEvent, ScrollDetail } from "@ionic/angular"; import { _wait } from "packages/shared/src/utils/async-utils"; @@ -70,14 +74,14 @@ export class headerComponent implements OnInit, OnDestroy { }); } // HACK - uncomment to test collapse - // this.appConfigService.updateAppConfig({ APP_HEADER_DEFAULTS: { collapse: true } }); + // this.appConfigService.setAppConfig({ APP_HEADER_DEFAULTS: { collapse: true } }); } subscribeToAppConfigChanges() { this.appConfigChanges$ = this.appConfigService.changesWithInitialValue$.subscribe( (changes: IAppConfig) => { if (changes.APP_HEADER_DEFAULTS) { - const headerConfig = this.appConfigService.APP_CONFIG.APP_HEADER_DEFAULTS; + const headerConfig = this.appConfigService.appConfig().APP_HEADER_DEFAULTS; this.headerConfig = headerConfig; this.updateHeaderConfig(); // handle collapse config changes diff --git a/src/app/shared/services/app-config/app-config.service.ts b/src/app/shared/services/app-config/app-config.service.ts index 5c9bde8d8..7d273eb7a 100644 --- a/src/app/shared/services/app-config/app-config.service.ts +++ b/src/app/shared/services/app-config/app-config.service.ts @@ -1,31 +1,33 @@ -import { Injectable } from "@angular/core"; +import { Injectable, signal } from "@angular/core"; import { getDefaultAppConfig, IAppConfig, IAppConfigOverride } from "data-models"; import { BehaviorSubject } from "rxjs"; import { deepMergeObjects, RecursivePartial, trackObservableObjectChanges } from "../../utils"; -import clone from "clone"; import { SyncServiceBase } from "../syncService.base"; import { startWith } from "rxjs/operators"; import { Observable } from "rxjs"; import { DeploymentService } from "../deployment/deployment.service"; +import { updateRoutingDefaults } from "./app-config.utils"; +import { Router } from "@angular/router"; @Injectable({ providedIn: "root", }) export class AppConfigService extends SyncServiceBase { - /** List of constants provided by data-models combined with deployment-specific overrides and skin-specific overrides */ - appConfig$ = new BehaviorSubject(undefined as any); + /** Signal representation of current appConfig value */ + public appConfig = signal(getDefaultAppConfig()); + + /** + * @deprecated - prefer use of config signal and computed/effect bindings + * List of constants provided by data-models combined with deployment-specific overrides and skin-specific overrides + **/ + public appConfig$ = new BehaviorSubject(getDefaultAppConfig()); /** Tracking observable of deep changes to app config, exposed in `changes` public method */ private appConfigChanges$: Observable>; - APP_CONFIG: IAppConfig; - deploymentAppConfig: IAppConfig; - - public get value() { - return this.appConfig$.value; - } - /** + * @deprecated - prefer use of config signal and computed/effect bindings + * * Track deep object diff of app config changes. * Creates subject on demand, so that multiple listeners can efficiently subscribe to changes */ @@ -36,35 +38,52 @@ export class AppConfigService extends SyncServiceBase { return this.appConfigChanges$; } - /** Track deep object diff of app config changes, including full initial value */ + /** + * @deprecated - prefer use of config signal and computed/effect bindings + * Track deep object diff of app config changes, including full initial value + * */ public get changesWithInitialValue$() { - return this.changes$.pipe(startWith(this.value)); + return this.changes$.pipe(startWith(this.appConfig())); } - constructor(private deploymentService: DeploymentService) { + constructor( + private deploymentService: DeploymentService, + private router: Router + ) { super("AppConfig"); this.initialise(); } + /** When service initialises load any deployment-specific config overrides */ private initialise() { - const deploymentOverrides: IAppConfigOverride = this.deploymentService.config.app_config || {}; - this.APP_CONFIG = getDefaultAppConfig(); - // Store app config with deployment overrides applied, to be merged with additional overrides when applied - this.deploymentAppConfig = this.applyAppConfigOverrides(this.APP_CONFIG, deploymentOverrides); - this.updateAppConfig(deploymentOverrides); + // When first loading handle side-effects from default config (e.g. initial routing). + // Deployment-specific side-effects will be handled when setting the appConfig + const defaultConfig = getDefaultAppConfig(); + this.handleConfigSideEffects(defaultConfig, defaultConfig); + + // Set app config using deployment overrides + this.setAppConfig(this.deploymentService.config.app_config); } - public updateAppConfig(overrides: IAppConfigOverride) { - // Clone this.deploymentAppConfig so that the original is unaffected by deepMergeObjects() - const appConfigWithOverrides = this.applyAppConfigOverrides( - clone(this.deploymentAppConfig), - overrides - ); - this.APP_CONFIG = appConfigWithOverrides; - this.appConfig$.next(appConfigWithOverrides); + /** + * Generate a complete app config by deep-merging app config overrides + * with the default config + * @param overrides + * @returns + */ + public setAppConfig(overrides: IAppConfigOverride = {}) { + // Ignore case where no overrides provides or overrides already applied + if (Object.keys(overrides).length === 0) return; + + const mergedConfig = deepMergeObjects(getDefaultAppConfig(), overrides); + this.handleConfigSideEffects(overrides, mergedConfig); + this.appConfig$.next(mergedConfig); + this.appConfig.set(mergedConfig); } - private applyAppConfigOverrides(appConfig: IAppConfig, overrides: IAppConfigOverride) { - return deepMergeObjects(appConfig, overrides); + private handleConfigSideEffects(overrides: IAppConfigOverride = {}, config: IAppConfig) { + if (overrides.APP_ROUTE_DEFAULTS) { + updateRoutingDefaults(config.APP_ROUTE_DEFAULTS, this.router); + } } } From 0989a4c67c251af7f248abec59abc6a5a02667ad Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 14 Sep 2024 13:44:38 -0700 Subject: [PATCH 172/204] chore: fix spec test --- packages/shared/src/utils/object-utils.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/utils/object-utils.spec.ts b/packages/shared/src/utils/object-utils.spec.ts index 9f6509cfa..ff45fa657 100644 --- a/packages/shared/src/utils/object-utils.spec.ts +++ b/packages/shared/src/utils/object-utils.spec.ts @@ -37,7 +37,9 @@ describe("Object Utils", () => { it("toEmptyObject", () => { const res = toEmptyObject(MOCK_NESTED_OBJECT); - expect(res).toEqual({ obj_1: { obj_1_1: { obj_1_1_1: {} }, obj_1_2: { obj_1_2_1: {} } } }); + expect(res).toEqual({ + obj_1: { obj_1_1: { obj_1_1_1: {} }, obj_1_2: { obj_1_2_1: {} } }, + } as any); }); it("cleanEmptyObject", () => { From 3f34561d8adf21ca4acfc80afa1ba87ba291bca2 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sat, 14 Sep 2024 13:59:14 -0700 Subject: [PATCH 173/204] refactor: manifest folders --- .../scripts/src/commands/app-data/convert/index.ts | 2 +- .../app-data/convert/manifest/manifest.spec.ts | 14 ++++++++++++++ .../manifest.ts} | 4 +--- .../reporters/assets.ts} | 0 .../manifest/reporters/feature-requirements.ts | 0 .../convert/manifest/reporters/template-summary.ts | 0 6 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 packages/scripts/src/commands/app-data/convert/manifest/manifest.spec.ts rename packages/scripts/src/commands/app-data/convert/{utils/app-data-manifest.utils.ts => manifest/manifest.ts} (94%) rename packages/scripts/src/commands/app-data/convert/{utils/app-data-manifest.spec.ts => manifest/reporters/assets.ts} (100%) create mode 100644 packages/scripts/src/commands/app-data/convert/manifest/reporters/feature-requirements.ts create mode 100644 packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.ts diff --git a/packages/scripts/src/commands/app-data/convert/index.ts b/packages/scripts/src/commands/app-data/convert/index.ts index 39f785233..da46396e7 100644 --- a/packages/scripts/src/commands/app-data/convert/index.ts +++ b/packages/scripts/src/commands/app-data/convert/index.ts @@ -19,7 +19,7 @@ import { standardiseNewlines, } from "./utils"; import { FlowParserProcessor } from "./processors/flowParser/flowParser"; -import { generateManifest } from "./utils/app-data-manifest.utils"; +import { generateManifest } from "./manifest/manifest"; /*************************************************************************************** * CLI diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.spec.ts b/packages/scripts/src/commands/app-data/convert/manifest/manifest.spec.ts new file mode 100644 index 000000000..65bc4541f --- /dev/null +++ b/packages/scripts/src/commands/app-data/convert/manifest/manifest.spec.ts @@ -0,0 +1,14 @@ +import { generateManifest, sortJsonByKey } from "./manifest"; +import { IParsedWorkbookData } from "../types"; + +const MOCK_WORKBOOKS: IParsedWorkbookData = { + template: [], +}; + +/** yarn workspace scripts test -t manifest.spec.ts */ +describe("App Data Manifest", () => { + it("Generates manifest", () => { + const res = generateManifest(MOCK_WORKBOOKS); + expect(res).toEqual({}); + }); +}); diff --git a/packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.utils.ts b/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts similarity index 94% rename from packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.utils.ts rename to packages/scripts/src/commands/app-data/convert/manifest/manifest.ts index e35da79f6..e14ac7dd7 100644 --- a/packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.utils.ts +++ b/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts @@ -1,11 +1,9 @@ /** * TODO - * - migrate manifest to standalone util + tests * - Organise structures for individual sub-reports (e.g. assets, sheets, components) * - Include sheet types/subtypes in manifest * - short summary text that links to breakdown * - possible recommendations/optimisations from manifest - * - move manifest generator here? * - how to handle implicit deps (one component uses another) * - possibly will require runtime error/warning/prompt * - handle dynamic @@ -54,7 +52,7 @@ export function generateManifest(data: IParsedWorkbookData): IFlowManifest { return manifest; } -// TODO - move to generic location +// TODO - move to generic location (possibly object-utils once #2423 merged) export function sortJsonByKey>(json: T) { const sorted = {}; for (const [key, value] of Object.entries(json).sort((a, b) => (a[0] > b[0] ? 1 : -1))) { diff --git a/packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.spec.ts b/packages/scripts/src/commands/app-data/convert/manifest/reporters/assets.ts similarity index 100% rename from packages/scripts/src/commands/app-data/convert/utils/app-data-manifest.spec.ts rename to packages/scripts/src/commands/app-data/convert/manifest/reporters/assets.ts diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/feature-requirements.ts b/packages/scripts/src/commands/app-data/convert/manifest/reporters/feature-requirements.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.ts new file mode 100644 index 000000000..e69de29bb From 28aafb2979c1aed673efa52bb2400ec1014e81aa Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 08:47:20 -0700 Subject: [PATCH 174/204] chore: update todos --- .../scripts/src/commands/app-data/convert/manifest/manifest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts b/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts index e14ac7dd7..a5cf8d3b1 100644 --- a/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts +++ b/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts @@ -1,6 +1,8 @@ /** * TODO * - Organise structures for individual sub-reports (e.g. assets, sheets, components) + * - Decide whether to run report row-by-row or just allow repeated loops + * - Generate output html * - Include sheet types/subtypes in manifest * - short summary text that links to breakdown * - possible recommendations/optimisations from manifest From fcb7cf5f330eb9b0d77069c94493cbb118fa2b3f Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 10:03:35 -0700 Subject: [PATCH 175/204] chore: fix test --- .../scripts/src/commands/app-data/postProcess/assets.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts b/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts index 8a1c99862..9d0f62449 100644 --- a/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts +++ b/packages/scripts/src/commands/app-data/postProcess/assets.spec.ts @@ -2,7 +2,7 @@ import { createHash } from "crypto"; import { AssetsPostProcessor } from "./assets"; import type { IDeploymentConfigJson } from "../../deployment/common"; -import type { RecursivePartial } from "data-models/appConfig"; +import { type RecursivePartial } from "shared/src/types"; import { readJsonSync, readdirSync, statSync, existsSync } from "fs-extra"; import { vol } from "memfs"; From 45aea6fb15151d30228e8cb3b5a488fd7ee4cd66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 20:57:01 +0000 Subject: [PATCH 176/204] chore(deps): bump dompurify from 3.0.8 to 3.1.3 Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.0.8 to 3.1.3. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.0.8...3.1.3) --- updated-dependencies: - dependency-name: dompurify dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 368 +++++++++++++++++++++++++-------------------------- yarn.lock | 10 +- 2 files changed, 189 insertions(+), 189 deletions(-) diff --git a/package.json b/package.json index 826ec67f0..be75e1a16 100644 --- a/package.json +++ b/package.json @@ -1,184 +1,184 @@ -{ - "name": "frontend", - "version": "0.16.35", - "author": "IDEMS International", - "license": "See LICENSE", - "homepage": "https://idems.international/", - "description": "", - "workspaces": [ - "packages/*", - "packages/@idemsInternational/*" - ], - "scripts": { - "ng": "ng", - "start": "yarn prepare && ng serve --open", - "start:local": "yarn prepare && concurrently --kill-others --raw \"ng serve --open\" \"yarn workflow sync_local\"", - "prepare": "node .husky/prepare.js && yarn workflow populate_src_assets", - "build": "yarn prepare && ng build --configuration=production", - "compodoc:generate": "compodoc --tsconfig tsconfig.docs.json --theme material --assetsFolder 'documentation/docs/assets' --output documentation/docs/generated --disablePrivate --disableLifeCycleHooks", - "compodoc:serve": "yarn compodoc:generate --serve --watch --open", - "scripts": "yarn workspace scripts start", - "workflow": "yarn workspace scripts start workflow run", - "server": "yarn workspace server", - "translations": "yarn workspace translations start", - "test:workspaces": "yarn workspace shared test && yarn workspace scripts test", - "test:unit": "ng test", - "test:unit:ci": "ng test --no-watch --no-progress", - "test:e2e": "yarn workspace test-e2e start", - "lint": "ng lint", - "lint:docs": "cspell \"documentation/docs/**/*.md\" --config \"cspell.config.yml\"", - "analyse": "ng build --configuration=production --stats-json && npx webpack-bundle-analyzer www/stats.json", - "version": "yarn workspace scripts start version", - "format": "yarn prettier . --write --log-level silent" - }, - "private": true, - "dependencies": { - "@angular/animations": "^17.2.2", - "@angular/common": "~17.2.2", - "@angular/core": "~17.2.2", - "@angular/elements": "^17.2.2", - "@angular/forms": "~17.2.2", - "@angular/platform-browser": "~17.2.2", - "@angular/platform-browser-dynamic": "~17.2.2", - "@angular/router": "~17.2.2", - "@capacitor-community/file-opener": "^6.0.0", - "@capacitor-firebase/authentication": "^6.1.0", - "@capacitor-firebase/crashlytics": "^6.1.0", - "@capacitor-firebase/performance": "^6.1.0", - "@capacitor/android": "^6.0.0", - "@capacitor/app": "^6.0.0", - "@capacitor/clipboard": "^6.0.0", - "@capacitor/core": "^6.0.0", - "@capacitor/device": "^6.0.0", - "@capacitor/filesystem": "^6.0.0", - "@capacitor/ios": "^6.0.0", - "@capacitor/local-notifications": "^6.0.0", - "@capacitor/push-notifications": "^6.0.0", - "@capacitor/share": "^6.0.0", - "@capacitor/splash-screen": "^6.0.0", - "@capawesome/capacitor-app-update": "^6.0.0", - "@ionic-native/core": "^5.36.0", - "@ionic-native/device": "^5.36.0", - "@ionic-native/http": "^5.36.0", - "@ionic-native/media": "^5.36.0", - "@ionic-native/status-bar": "^5.36.0", - "@ionic/angular": "^7.7.3", - "@ionic/pwa-elements": "^3.2.2", - "@sentry/angular-ivy": "^7.102.1", - "@supabase/supabase-js": "^2.39.0", - "@types/file-saver": "^2.0.7", - "bootstrap-datepicker": "^1.10.0", - "capacitor-blob-writer": "^1.1.17", - "clone": "^2.1.2", - "core-js": "^3.33.3", - "data-models": "workspace:*", - "date-fns": "^2.30.0", - "deep-object-diff": "^1.1.9", - "dexie": "^3.2.4", - "dexie-observable": "3.0.0-beta.11", - "dexie-syncable": "^3.0.0-beta.10", - "document-register-element": "^1.14.10", - "dompurify": "^3.0.6", - "extract-math": "^1.2.3", - "file-saver": "^2.0.5", - "firebase": "^10.7.0", - "globalthis": "^1.0.3", - "howler": "^2.2.4", - "html5sortable": "^0.13.3", - "intro.js": "^3.4.0", - "jquery": "^3.7.1", - "jquery-touchswipe": "^1.6.19", - "katex": "^0.16.10", - "lottie-web": "^5.12.2", - "marked": "^2.1.3", - "marked-smartypants-lite": "^1.0.2", - "mergexml": "^1.2.3", - "ng2-nouislider": "^2.0.0", - "ngx-extended-pdf-viewer": "18.1.9", - "ngx-lottie": "^10.0.0", - "ngx-matomo-client": "^6.0.2", - "nouislider": "^15.7.1", - "qrcode": "^1.5.3", - "rxdb": "^14.17.1", - "rxjs": "^7.8.1", - "scripts": "workspace:*", - "shared": "workspace:*", - "signature_pad": "^4.1.7", - "stacktrace-js": "^2.0.2", - "swiper": "^8.4.7", - "trusted-types": "^2.0.0", - "zone.js": "~0.14.2" - }, - "devDependencies": { - "@angular-devkit/architect": "0.1702.1", - "@angular-devkit/build-angular": "~17.2.1", - "@angular-devkit/core": "~17.2.1", - "@angular-devkit/schematics": "~17.2.1", - "@angular-eslint/builder": "17.2.1", - "@angular-eslint/eslint-plugin": "17.2.1", - "@angular-eslint/eslint-plugin-template": "17.2.1", - "@angular-eslint/schematics": "17.2.1", - "@angular-eslint/template-parser": "17.2.1", - "@angular/cli": "~17.2.1", - "@angular/compiler": "~17.2.2", - "@angular/compiler-cli": "~17.2.2", - "@angular/language-service": "~17.2.2", - "@capacitor/cli": "^6.0.0", - "@compodoc/compodoc": "^1.1.23", - "@ionic/angular-toolkit": "^11.0.1", - "@ionic/cli": "^7.1.5", - "@schematics/angular": "~17.0.3", - "@swc/helpers": "^0.5.1", - "@types/clone": "^2.1.1", - "@types/howler": "^2.2.2", - "@types/intro.js": "^3.0.1", - "@types/jasmine": "~4.3.0", - "@types/lokijs": "^1.5.7", - "@types/marked": "^2.0.3", - "@types/node": "^18.18.13", - "@types/nouislider": "^15.0.0", - "@types/swiper": "~4.2.0", - "@typescript-eslint/eslint-plugin": "^6.13.1", - "@typescript-eslint/parser": "^6.13.1", - "codelyzer": "^6.0.2", - "concurrently": "^6.2.0", - "cspell": "^6.20.1", - "eslint": "^8.54.0", - "eslint-config-prettier": "^9.1.0", - "eslint-config-standard-with-typescript": "latest", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jasmine": "^4.1.3", - "eslint-plugin-jsdoc": "40.1.1", - "eslint-plugin-n": "^15.7.0", - "eslint-plugin-prefer-arrow": "1.2.3", - "eslint-plugin-promise": "^6.1.1", - "firebase-tools": "^13.6.0", - "husky": "^8.0.3", - "jasmine-core": "~4.5.0", - "jasmine-spec-reporter": "~7.0.0", - "karma": "~6.4.0", - "karma-chrome-launcher": "~3.1.0", - "karma-coverage": "~2.2.0", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.0.0", - "lint-staged": "^15.2.2", - "prettier": "^3.2.5", - "typescript": "5.2.2" - }, - "resolutions": { - "@supabase/gotrue-js": "2.61.0" - }, - "lint-staged": { - "*.{ts,scss,html}": [ - "prettier --write" - ] - }, - "browser": { - "__hack__": "add support for lokijs in browser by ignoring fs", - "fs": false - }, - "engines": { - "node": ">=18.x" - }, - "packageManager": "yarn@3.3.1" -} +{ + "name": "frontend", + "version": "0.16.35", + "author": "IDEMS International", + "license": "See LICENSE", + "homepage": "https://idems.international/", + "description": "", + "workspaces": [ + "packages/*", + "packages/@idemsInternational/*" + ], + "scripts": { + "ng": "ng", + "start": "yarn prepare && ng serve --open", + "start:local": "yarn prepare && concurrently --kill-others --raw \"ng serve --open\" \"yarn workflow sync_local\"", + "prepare": "node .husky/prepare.js && yarn workflow populate_src_assets", + "build": "yarn prepare && ng build --configuration=production", + "compodoc:generate": "compodoc --tsconfig tsconfig.docs.json --theme material --assetsFolder 'documentation/docs/assets' --output documentation/docs/generated --disablePrivate --disableLifeCycleHooks", + "compodoc:serve": "yarn compodoc:generate --serve --watch --open", + "scripts": "yarn workspace scripts start", + "workflow": "yarn workspace scripts start workflow run", + "server": "yarn workspace server", + "translations": "yarn workspace translations start", + "test:workspaces": "yarn workspace shared test && yarn workspace scripts test", + "test:unit": "ng test", + "test:unit:ci": "ng test --no-watch --no-progress", + "test:e2e": "yarn workspace test-e2e start", + "lint": "ng lint", + "lint:docs": "cspell \"documentation/docs/**/*.md\" --config \"cspell.config.yml\"", + "analyse": "ng build --configuration=production --stats-json && npx webpack-bundle-analyzer www/stats.json", + "version": "yarn workspace scripts start version", + "format": "yarn prettier . --write --log-level silent" + }, + "private": true, + "dependencies": { + "@angular/animations": "^17.2.2", + "@angular/common": "~17.2.2", + "@angular/core": "~17.2.2", + "@angular/elements": "^17.2.2", + "@angular/forms": "~17.2.2", + "@angular/platform-browser": "~17.2.2", + "@angular/platform-browser-dynamic": "~17.2.2", + "@angular/router": "~17.2.2", + "@capacitor-community/file-opener": "^6.0.0", + "@capacitor-firebase/authentication": "^6.1.0", + "@capacitor-firebase/crashlytics": "^6.1.0", + "@capacitor-firebase/performance": "^6.1.0", + "@capacitor/android": "^6.0.0", + "@capacitor/app": "^6.0.0", + "@capacitor/clipboard": "^6.0.0", + "@capacitor/core": "^6.0.0", + "@capacitor/device": "^6.0.0", + "@capacitor/filesystem": "^6.0.0", + "@capacitor/ios": "^6.0.0", + "@capacitor/local-notifications": "^6.0.0", + "@capacitor/push-notifications": "^6.0.0", + "@capacitor/share": "^6.0.0", + "@capacitor/splash-screen": "^6.0.0", + "@capawesome/capacitor-app-update": "^6.0.0", + "@ionic-native/core": "^5.36.0", + "@ionic-native/device": "^5.36.0", + "@ionic-native/http": "^5.36.0", + "@ionic-native/media": "^5.36.0", + "@ionic-native/status-bar": "^5.36.0", + "@ionic/angular": "^7.7.3", + "@ionic/pwa-elements": "^3.2.2", + "@sentry/angular-ivy": "^7.102.1", + "@supabase/supabase-js": "^2.39.0", + "@types/file-saver": "^2.0.7", + "bootstrap-datepicker": "^1.10.0", + "capacitor-blob-writer": "^1.1.17", + "clone": "^2.1.2", + "core-js": "^3.33.3", + "data-models": "workspace:*", + "date-fns": "^2.30.0", + "deep-object-diff": "^1.1.9", + "dexie": "^3.2.4", + "dexie-observable": "3.0.0-beta.11", + "dexie-syncable": "^3.0.0-beta.10", + "document-register-element": "^1.14.10", + "dompurify": "^3.1.3", + "extract-math": "^1.2.3", + "file-saver": "^2.0.5", + "firebase": "^10.7.0", + "globalthis": "^1.0.3", + "howler": "^2.2.4", + "html5sortable": "^0.13.3", + "intro.js": "^3.4.0", + "jquery": "^3.7.1", + "jquery-touchswipe": "^1.6.19", + "katex": "^0.16.10", + "lottie-web": "^5.12.2", + "marked": "^2.1.3", + "marked-smartypants-lite": "^1.0.2", + "mergexml": "^1.2.3", + "ng2-nouislider": "^2.0.0", + "ngx-extended-pdf-viewer": "18.1.9", + "ngx-lottie": "^10.0.0", + "ngx-matomo-client": "^6.0.2", + "nouislider": "^15.7.1", + "qrcode": "^1.5.3", + "rxdb": "^14.17.1", + "rxjs": "^7.8.1", + "scripts": "workspace:*", + "shared": "workspace:*", + "signature_pad": "^4.1.7", + "stacktrace-js": "^2.0.2", + "swiper": "^8.4.7", + "trusted-types": "^2.0.0", + "zone.js": "~0.14.2" + }, + "devDependencies": { + "@angular-devkit/architect": "0.1702.1", + "@angular-devkit/build-angular": "~17.2.1", + "@angular-devkit/core": "~17.2.1", + "@angular-devkit/schematics": "~17.2.1", + "@angular-eslint/builder": "17.2.1", + "@angular-eslint/eslint-plugin": "17.2.1", + "@angular-eslint/eslint-plugin-template": "17.2.1", + "@angular-eslint/schematics": "17.2.1", + "@angular-eslint/template-parser": "17.2.1", + "@angular/cli": "~17.2.1", + "@angular/compiler": "~17.2.2", + "@angular/compiler-cli": "~17.2.2", + "@angular/language-service": "~17.2.2", + "@capacitor/cli": "^6.0.0", + "@compodoc/compodoc": "^1.1.23", + "@ionic/angular-toolkit": "^11.0.1", + "@ionic/cli": "^7.1.5", + "@schematics/angular": "~17.0.3", + "@swc/helpers": "^0.5.1", + "@types/clone": "^2.1.1", + "@types/howler": "^2.2.2", + "@types/intro.js": "^3.0.1", + "@types/jasmine": "~4.3.0", + "@types/lokijs": "^1.5.7", + "@types/marked": "^2.0.3", + "@types/node": "^18.18.13", + "@types/nouislider": "^15.0.0", + "@types/swiper": "~4.2.0", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", + "codelyzer": "^6.0.2", + "concurrently": "^6.2.0", + "cspell": "^6.20.1", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.1.0", + "eslint-config-standard-with-typescript": "latest", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jasmine": "^4.1.3", + "eslint-plugin-jsdoc": "40.1.1", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-prefer-arrow": "1.2.3", + "eslint-plugin-promise": "^6.1.1", + "firebase-tools": "^13.6.0", + "husky": "^8.0.3", + "jasmine-core": "~4.5.0", + "jasmine-spec-reporter": "~7.0.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "lint-staged": "^15.2.2", + "prettier": "^3.2.5", + "typescript": "5.2.2" + }, + "resolutions": { + "@supabase/gotrue-js": "2.61.0" + }, + "lint-staged": { + "*.{ts,scss,html}": [ + "prettier --write" + ] + }, + "browser": { + "__hack__": "add support for lokijs in browser by ignoring fs", + "fs": false + }, + "engines": { + "node": ">=18.x" + }, + "packageManager": "yarn@3.3.1" +} diff --git a/yarn.lock b/yarn.lock index a627f1e2e..fdf1481e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12543,10 +12543,10 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:^3.0.6": - version: 3.0.8 - resolution: "dompurify@npm:3.0.8" - checksum: cac660ccae15a9603f06a85344da868a4c3732d8b57f7998de0f421eb4b9e67d916be52e9bb2a57b2f95b49e994cc50bcd06bb87f2cb2849cf058bdf15266237 +"dompurify@npm:^3.1.3": + version: 3.1.6 + resolution: "dompurify@npm:3.1.6" + checksum: cc4fc4ccd9261fbceb2a1627a985c70af231274a26ddd3f643fd0616a0a44099bd9e4480940ce3655612063be4a1fe9f5e9309967526f8c0a99f931602323866 languageName: node linkType: hard @@ -14996,7 +14996,7 @@ __metadata: dexie-observable: 3.0.0-beta.11 dexie-syncable: ^3.0.0-beta.10 document-register-element: ^1.14.10 - dompurify: ^3.0.6 + dompurify: ^3.1.3 eslint: ^8.54.0 eslint-config-prettier: ^9.1.0 eslint-config-standard-with-typescript: latest From 94e2bbf6f7b4222a2f6ec43b5379bd5334083b9c Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 15:29:31 -0700 Subject: [PATCH 177/204] feat: manifest utils and specs --- .../convert/manifest/manifest.utils.spec.ts | 26 +++++++++++++++ .../convert/manifest/manifest.utils.ts | 32 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.spec.ts create mode 100644 packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.ts diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.spec.ts b/packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.spec.ts new file mode 100644 index 000000000..9601db47e --- /dev/null +++ b/packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.spec.ts @@ -0,0 +1,26 @@ +import { generateMarkdownTable, sortJsonByKey } from "./manifest.utils"; + +/** yarn workspace scripts test -t manifest.utils.spec.ts */ +describe("manifest utils", () => { + it("sortJsonByKey", () => { + const mockJson = { b: "first entry", a: "second entry" }; + const res = sortJsonByKey(mockJson); + expect(Object.keys(res)).toStrictEqual(["a", "b"]); + }); + it("generateMarkdownTable", () => { + const res = generateMarkdownTable([ + { key_1: "value_1a", key_2: "value_1b" }, + { key_1: "value_2a", key_2: "value_2b" }, + ]); + expect(res).toEqual( + `| key_1 | key_2 |\n| --- | --- |\n| value_1a | value_1b |\n| value_2a | value_2b |` + /* When formatted output in form: + + | key_1 | key_2 | + | --- | --- | + | value_1a | value_1b | + | value_2a | value_2b | + */ + ); + }); +}); diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.ts b/packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.ts new file mode 100644 index 000000000..68e6ef55e --- /dev/null +++ b/packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.ts @@ -0,0 +1,32 @@ +// TODO - move to generic location (possibly object-utils once #2423 merged) +export function sortJsonByKey>(json: T) { + const sorted = {}; + for (const [key, value] of Object.entries(json).sort((a, b) => (a[0] > b[0] ? 1 : -1))) { + sorted[key] = value; + } + return sorted as T; +} + +/** + * Convert an array to a basic markdown-formatted table + * @example + * ``` + * generateMarkdownTable([{key_1:'value_1', key_2:'value_2'}]) + * // output + * | key_1 | key_2 | + * | --- | --- | + * | value_1 | value_2 | + * ``` + */ +export function generateMarkdownTable(data: Record[]) { + const columns = Object.keys(data[0]); + + const rows: string[][] = []; + rows.push(columns); + rows.push(columns.map(() => "---")); + for (const el of data) { + rows.push(columns.map((c) => el[c])); + } + const rowStrings: string[] = rows.map((r) => `| ${r.join(" | ")} |`); + return rowStrings.join("\n"); +} From aced1604770d5efb8e58a181d9887f766a7a9e87 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 15:48:53 -0700 Subject: [PATCH 178/204] feat: add template summary report and specs --- .../convert/manifest/reporters/assets.ts | 0 .../reporters/feature-requirements.ts | 0 .../convert/manifest/reporters/index.ts | 1 + .../reporters/template-summary.spec.ts | 68 +++++++++++++++++++ .../manifest/reporters/template-summary.ts | 64 +++++++++++++++++ 5 files changed, 133 insertions(+) delete mode 100644 packages/scripts/src/commands/app-data/convert/manifest/reporters/assets.ts delete mode 100644 packages/scripts/src/commands/app-data/convert/manifest/reporters/feature-requirements.ts create mode 100644 packages/scripts/src/commands/app-data/convert/manifest/reporters/index.ts create mode 100644 packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.spec.ts diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/assets.ts b/packages/scripts/src/commands/app-data/convert/manifest/reporters/assets.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/feature-requirements.ts b/packages/scripts/src/commands/app-data/convert/manifest/reporters/feature-requirements.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/index.ts b/packages/scripts/src/commands/app-data/convert/manifest/reporters/index.ts new file mode 100644 index 000000000..90f267d70 --- /dev/null +++ b/packages/scripts/src/commands/app-data/convert/manifest/reporters/index.ts @@ -0,0 +1 @@ +export * from "./template-summary"; diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.spec.ts b/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.spec.ts new file mode 100644 index 000000000..ede3840a7 --- /dev/null +++ b/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.spec.ts @@ -0,0 +1,68 @@ +import { FlowTypes } from "data-models"; +import { IParsedWorkbookData } from "../../types"; +import { TemplateSummaryReport } from "./template-summary"; + +const MOCK_ROWS_1: FlowTypes.TemplateRow[] = [ + { + type: "button", + _nested_name: "", + name: "", + action_list: [ + { action_id: "share", args: [], trigger: "click" }, + { action_id: "audio_play", args: [], trigger: "click" }, + ], + }, + { + type: "text", + _nested_name: "", + name: "", + action_list: [ + { action_id: "emit", args: ["force_reprocess"], trigger: "click" }, + { action_id: "emit", args: ["completed"], trigger: "click" }, + ], + }, +]; +const MOCK_ROWS_2: FlowTypes.TemplateRow[] = [ + { + type: "button", + _nested_name: "", + name: "", + action_list: [{ action_id: "share", args: [], trigger: "click" }], + }, +]; + +const MOCK_WORKBOOK_DATA: IParsedWorkbookData = { + template: [ + { + flow_type: "template", + flow_name: "mock_template_1", + rows: MOCK_ROWS_1, + }, + { + flow_type: "template", + flow_name: "mock_template_2", + rows: MOCK_ROWS_2, + }, + ], +}; + +/** yarn workspace scripts test -t template-summary.spec.ts */ +describe("Template Summary Report", () => { + it("Enumerates component references", async () => { + const { template_components } = await new TemplateSummaryReport().process(MOCK_WORKBOOK_DATA); + expect(template_components.data).toEqual([ + { type: "button", count: 2 }, + { type: "text", count: 1 }, + ]); + }); + it("Enumerates action references", async () => { + const { template_actions } = await new TemplateSummaryReport().process(MOCK_WORKBOOK_DATA); + // NOTE - emit actions should be enumerated by subtype from args + expect(template_actions.data).toEqual([ + { type: "audio_play", count: 1 }, + { type: "emit: completed", count: 1 }, + { type: "emit: force_reprocess", count: 1 }, + { type: "share", count: 2 }, + ]); + }); +}); diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.ts index e69de29bb..992523a0b 100644 --- a/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.ts +++ b/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.ts @@ -0,0 +1,64 @@ +import { FlowTypes } from "data-models"; + +import { IParsedWorkbookData } from "../../types"; +import { sortJsonByKey } from "../manifest.utils"; +import { IManifestReport } from "../manifest.types"; + +interface ITemplateSummary { + components: Record; + actions: Record; +} + +/** + * Generates a list of all components and action types used within templates + * This will later be used to develop a list of features required + * + * TODO + * - consider check for template references for unused templates (would need to include data refs) + */ +export class TemplateSummaryReport { + private summary: ITemplateSummary = { actions: {}, components: {} }; + + public async process(data: IParsedWorkbookData) { + // TODO - could also consider components or actions referenced from data_lists + for (const flow of data.template || []) { + for (const row of flow.rows) { + const { action_list = [], type } = row as FlowTypes.TemplateRow; + for (const action of action_list) { + let { action_id, args } = action; + // HACK -include emit type actions + if (action_id === "emit") { + action_id += `: ${args[0]}`; + } + this.summary.actions[action_id] ??= { count: 0 }; + this.summary.actions[action_id].count++; + } + this.summary.components[type] ??= { count: 0 }; + this.summary.components[type].count++; + } + } + + const reports: Record<"template_components" | "template_actions", IManifestReport> = { + template_components: { + type: "table", + title: "Components", + level: "info", + data: this.getReportData(this.summary.components), + }, + template_actions: { + type: "table", + title: "Actions", + level: "info", + data: this.getReportData(this.summary.actions), + }, + }; + + return reports; + } + + /** Convert type records to array for report */ + private getReportData(data: Record) { + const sorted = sortJsonByKey(data); + return Object.entries(sorted).map(([type, { count }]) => ({ type, count })); + } +} From 423040d7899a8d8c4f616d1230f8a7eaaddd7eb0 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 15:52:37 -0700 Subject: [PATCH 179/204] feat: manifest generator --- .../src/commands/app-data/convert/index.ts | 7 +- .../convert/manifest/manifest.spec.ts | 14 --- .../app-data/convert/manifest/manifest.ts | 99 +++++++++++-------- .../convert/manifest/manifest.types.ts | 19 ++++ 4 files changed, 79 insertions(+), 60 deletions(-) delete mode 100644 packages/scripts/src/commands/app-data/convert/manifest/manifest.spec.ts create mode 100644 packages/scripts/src/commands/app-data/convert/manifest/manifest.types.ts diff --git a/packages/scripts/src/commands/app-data/convert/index.ts b/packages/scripts/src/commands/app-data/convert/index.ts index da46396e7..5f13455f1 100644 --- a/packages/scripts/src/commands/app-data/convert/index.ts +++ b/packages/scripts/src/commands/app-data/convert/index.ts @@ -19,7 +19,7 @@ import { standardiseNewlines, } from "./utils"; import { FlowParserProcessor } from "./processors/flowParser/flowParser"; -import { generateManifest } from "./manifest/manifest"; +import { ManifestGenerator } from "./manifest/manifest"; /*************************************************************************************** * CLI @@ -131,9 +131,8 @@ export class AppDataConverter { processor.logger = this.logger; const jsonFlows = Object.values(combinedOutputsHashmap); const result = (await processor.process(jsonFlows)) as IParsedWorkbookData; - const manifest = generateManifest(result); - console.table(manifest.actions); - console.table(manifest.components); + await new ManifestGenerator(this.activeDeployment).process(result); + // TODO - write to disk and log const { errors, warnings } = this.logOutputs(result); return { result, errors, warnings }; diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.spec.ts b/packages/scripts/src/commands/app-data/convert/manifest/manifest.spec.ts deleted file mode 100644 index 65bc4541f..000000000 --- a/packages/scripts/src/commands/app-data/convert/manifest/manifest.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { generateManifest, sortJsonByKey } from "./manifest"; -import { IParsedWorkbookData } from "../types"; - -const MOCK_WORKBOOKS: IParsedWorkbookData = { - template: [], -}; - -/** yarn workspace scripts test -t manifest.spec.ts */ -describe("App Data Manifest", () => { - it("Generates manifest", () => { - const res = generateManifest(MOCK_WORKBOOKS); - expect(res).toEqual({}); - }); -}); diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts b/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts index a5cf8d3b1..1b8c5e8ff 100644 --- a/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts +++ b/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts @@ -1,64 +1,79 @@ /** * TODO - * - Organise structures for individual sub-reports (e.g. assets, sheets, components) - * - Decide whether to run report row-by-row or just allow repeated loops - * - Generate output html - * - Include sheet types/subtypes in manifest - * - short summary text that links to breakdown + * - Include sheet types/subtypes reporter + * - Include asset references + * - Better to name as manifest or report + * - Document authors to gitignore outputs (if not wanted) + * + * Future + * - Specific feature manifests that declare action/component namespaces * - possible recommendations/optimisations from manifest * - how to handle implicit deps (one component uses another) * - possibly will require runtime error/warning/prompt * - handle dynamic - * - handle multiple emit types * - QA components/actions that don't exist * - possibly export list of COMPONENTS_AVAILABLE (or similar... or just use main list lookup) * - also consider asset manifest (but would need to ensure dynamic assets included, plus param list + template value) * - handle implicit components (check imports (?)) */ +import { IDeploymentConfigJson } from "data-models"; +import { IParsedWorkbookData } from "../types"; +import { TemplateSummaryReport } from "./reporters"; +import { IManifestReport } from "./manifest.types"; +import { resolve, dirname } from "path"; +import { writeFile, ensureDir, emptyDir } from "fs-extra"; +import { generateMarkdownTable } from "./manifest.utils"; +import { logOutput } from "shared"; +import chalk from "chalk"; + // Testing notes // yarn workflow sync_sheets --skip-download -import { FlowTypes } from "data-models"; -import { IParsedWorkbookData } from "../types"; +/** + * + **/ +export class ManifestGenerator { + constructor(private deployment: IDeploymentConfigJson) {} -interface IFlowManifest { - components: Record; - actions: Record; - // TODO - also may need to consider imported from data_list or other sources -} + public async process(data: IParsedWorkbookData) { + const { template_actions, template_components } = await new TemplateSummaryReport().process( + data + ); -/** - * Create a manifest of components and action handlers referenced by templates - * NOTE - this will not explicitly identify any variables injected dynamically - * TODO - add tests and example to catch? - * */ -export function generateManifest(data: IParsedWorkbookData): IFlowManifest { - const manifest: IFlowManifest = { actions: {}, components: {} }; - // TODO - consider extracting dynamic data_list actions if exist - for (const flow of data.template || []) { - for (const row of flow.rows) { - const { action_list = [], type } = row as FlowTypes.TemplateRow; - for (const action of action_list) { - manifest.actions[action.action_id] ??= 0; - manifest.actions[action.action_id]++; - } - manifest.components[type] ??= 0; - manifest.components[type]++; - } + const outputReports = { template_actions, template_components }; + await this.writeOutputs(outputReports); } - // sort data alphabetically - for (const key of Object.keys(manifest)) { - manifest[key] = sortJsonByKey(manifest[key]); + + private async writeOutputs(reports: Record) { + const outputDir = resolve(this.deployment._workspace_path, "reports"); + await ensureDir(dirname(outputDir)); + await emptyDir(outputDir); + await this.writeOutputJson(reports, resolve(outputDir, "summary.json")); + await this.writeOutputMarkdown(reports, resolve(outputDir, "summary.md")); + logOutput({ msg1: "Reports Generated", msg2: outputDir }); + // repeat log in case boxed output broken + console.log(chalk.gray(outputDir)); } - return manifest; -} -// TODO - move to generic location (possibly object-utils once #2423 merged) -export function sortJsonByKey>(json: T) { - const sorted = {}; - for (const [key, value] of Object.entries(json).sort((a, b) => (a[0] > b[0] ? 1 : -1))) { - sorted[key] = value; + private async writeOutputJson(reports: Record, target: string) { + const output: Record = {}; + for (const [key, { data }] of Object.entries(reports)) { + output[key] = data; + } + await writeFile(target, JSON.stringify(output, null, 2)); + } + + private async writeOutputMarkdown(reports: Record, target: string) { + const contents = ["# Summary"]; + for (const report of Object.values(reports)) { + if (report.type === "table") { + contents.push(""); + contents.push(`## ${report.title}`); + const mdTable = generateMarkdownTable(report.data); + contents.push(mdTable); + } + } + await writeFile(target, contents.join("\n")); } - return sorted as T; } diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.types.ts b/packages/scripts/src/commands/app-data/convert/manifest/manifest.types.ts new file mode 100644 index 000000000..4b5421f14 --- /dev/null +++ b/packages/scripts/src/commands/app-data/convert/manifest/manifest.types.ts @@ -0,0 +1,19 @@ +// Use union types to allow strong typing by manifest display type +export type IManifestReport = IManifestReportTable | IManifestReportText; + +interface IManifestReportTypeBase { + /** Reporting level, default "info" (future will include warnings/recommendations). */ + level: "info"; + /** Title to display on top of report */ + title: string; +} + +interface IManifestReportTable extends IManifestReportTypeBase { + data: Record[]; + type: "table"; +} + +interface IManifestReportText extends IManifestReportTypeBase { + data: string; + type: "text"; +} From add715d2000aa2c3158e389a799d7b0ffc582058 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 15:53:48 -0700 Subject: [PATCH 180/204] chore: rename to reports --- .../commands/app-data/convert/{manifest => report}/manifest.ts | 0 .../app-data/convert/{manifest => report}/manifest.types.ts | 0 .../app-data/convert/{manifest => report}/manifest.utils.spec.ts | 0 .../app-data/convert/{manifest => report}/manifest.utils.ts | 0 .../app-data/convert/{manifest => report}/reporters/index.ts | 0 .../{manifest => report}/reporters/template-summary.spec.ts | 0 .../convert/{manifest => report}/reporters/template-summary.ts | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename packages/scripts/src/commands/app-data/convert/{manifest => report}/manifest.ts (100%) rename packages/scripts/src/commands/app-data/convert/{manifest => report}/manifest.types.ts (100%) rename packages/scripts/src/commands/app-data/convert/{manifest => report}/manifest.utils.spec.ts (100%) rename packages/scripts/src/commands/app-data/convert/{manifest => report}/manifest.utils.ts (100%) rename packages/scripts/src/commands/app-data/convert/{manifest => report}/reporters/index.ts (100%) rename packages/scripts/src/commands/app-data/convert/{manifest => report}/reporters/template-summary.spec.ts (100%) rename packages/scripts/src/commands/app-data/convert/{manifest => report}/reporters/template-summary.ts (100%) diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.ts b/packages/scripts/src/commands/app-data/convert/report/manifest.ts similarity index 100% rename from packages/scripts/src/commands/app-data/convert/manifest/manifest.ts rename to packages/scripts/src/commands/app-data/convert/report/manifest.ts diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.types.ts b/packages/scripts/src/commands/app-data/convert/report/manifest.types.ts similarity index 100% rename from packages/scripts/src/commands/app-data/convert/manifest/manifest.types.ts rename to packages/scripts/src/commands/app-data/convert/report/manifest.types.ts diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.spec.ts b/packages/scripts/src/commands/app-data/convert/report/manifest.utils.spec.ts similarity index 100% rename from packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.spec.ts rename to packages/scripts/src/commands/app-data/convert/report/manifest.utils.spec.ts diff --git a/packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.ts b/packages/scripts/src/commands/app-data/convert/report/manifest.utils.ts similarity index 100% rename from packages/scripts/src/commands/app-data/convert/manifest/manifest.utils.ts rename to packages/scripts/src/commands/app-data/convert/report/manifest.utils.ts diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/index.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/index.ts similarity index 100% rename from packages/scripts/src/commands/app-data/convert/manifest/reporters/index.ts rename to packages/scripts/src/commands/app-data/convert/report/reporters/index.ts diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.spec.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.spec.ts similarity index 100% rename from packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.spec.ts rename to packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.spec.ts diff --git a/packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts similarity index 100% rename from packages/scripts/src/commands/app-data/convert/manifest/reporters/template-summary.ts rename to packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts From ada0b61164235eb4cf5ccd2881d39328cdd633bf Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 15:55:39 -0700 Subject: [PATCH 181/204] chore: code tidying --- .../src/commands/app-data/convert/report/manifest.ts | 10 +++++----- .../commands/app-data/convert/report/manifest.types.ts | 8 ++++---- .../convert/report/reporters/template-summary.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/report/manifest.ts b/packages/scripts/src/commands/app-data/convert/report/manifest.ts index 1b8c5e8ff..4f4258221 100644 --- a/packages/scripts/src/commands/app-data/convert/report/manifest.ts +++ b/packages/scripts/src/commands/app-data/convert/report/manifest.ts @@ -20,7 +20,7 @@ import { IDeploymentConfigJson } from "data-models"; import { IParsedWorkbookData } from "../types"; import { TemplateSummaryReport } from "./reporters"; -import { IManifestReport } from "./manifest.types"; +import { IReport } from "./manifest.types"; import { resolve, dirname } from "path"; import { writeFile, ensureDir, emptyDir } from "fs-extra"; import { generateMarkdownTable } from "./manifest.utils"; @@ -33,7 +33,7 @@ import chalk from "chalk"; /** * **/ -export class ManifestGenerator { +export class ReportGenerator { constructor(private deployment: IDeploymentConfigJson) {} public async process(data: IParsedWorkbookData) { @@ -45,7 +45,7 @@ export class ManifestGenerator { await this.writeOutputs(outputReports); } - private async writeOutputs(reports: Record) { + private async writeOutputs(reports: Record) { const outputDir = resolve(this.deployment._workspace_path, "reports"); await ensureDir(dirname(outputDir)); await emptyDir(outputDir); @@ -56,7 +56,7 @@ export class ManifestGenerator { console.log(chalk.gray(outputDir)); } - private async writeOutputJson(reports: Record, target: string) { + private async writeOutputJson(reports: Record, target: string) { const output: Record = {}; for (const [key, { data }] of Object.entries(reports)) { output[key] = data; @@ -64,7 +64,7 @@ export class ManifestGenerator { await writeFile(target, JSON.stringify(output, null, 2)); } - private async writeOutputMarkdown(reports: Record, target: string) { + private async writeOutputMarkdown(reports: Record, target: string) { const contents = ["# Summary"]; for (const report of Object.values(reports)) { if (report.type === "table") { diff --git a/packages/scripts/src/commands/app-data/convert/report/manifest.types.ts b/packages/scripts/src/commands/app-data/convert/report/manifest.types.ts index 4b5421f14..182ffd642 100644 --- a/packages/scripts/src/commands/app-data/convert/report/manifest.types.ts +++ b/packages/scripts/src/commands/app-data/convert/report/manifest.types.ts @@ -1,19 +1,19 @@ // Use union types to allow strong typing by manifest display type -export type IManifestReport = IManifestReportTable | IManifestReportText; +export type IReport = IReportTable | IReportText; -interface IManifestReportTypeBase { +interface IReportBase { /** Reporting level, default "info" (future will include warnings/recommendations). */ level: "info"; /** Title to display on top of report */ title: string; } -interface IManifestReportTable extends IManifestReportTypeBase { +interface IReportTable extends IReportBase { data: Record[]; type: "table"; } -interface IManifestReportText extends IManifestReportTypeBase { +interface IReportText extends IReportBase { data: string; type: "text"; } diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts index 992523a0b..a25b0189a 100644 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts @@ -2,7 +2,7 @@ import { FlowTypes } from "data-models"; import { IParsedWorkbookData } from "../../types"; import { sortJsonByKey } from "../manifest.utils"; -import { IManifestReport } from "../manifest.types"; +import { IReport } from "../manifest.types"; interface ITemplateSummary { components: Record; @@ -38,7 +38,7 @@ export class TemplateSummaryReport { } } - const reports: Record<"template_components" | "template_actions", IManifestReport> = { + const reports: Record<"template_components" | "template_actions", IReport> = { template_components: { type: "table", title: "Components", From f01306a3384c56cf5957f19628b32541ef847695 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 15:58:39 -0700 Subject: [PATCH 182/204] chore: code tidying --- .../app-data/convert/report/{manifest.ts => report.ts} | 9 ++++----- .../report/{manifest.types.ts => report.types.ts} | 2 +- .../{manifest.utils.spec.ts => report.utils.spec.ts} | 6 +++--- .../report/{manifest.utils.ts => report.utils.ts} | 0 .../convert/report/reporters/template-summary.ts | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) rename packages/scripts/src/commands/app-data/convert/report/{manifest.ts => report.ts} (88%) rename packages/scripts/src/commands/app-data/convert/report/{manifest.types.ts => report.types.ts} (86%) rename packages/scripts/src/commands/app-data/convert/report/{manifest.utils.spec.ts => report.utils.spec.ts} (80%) rename packages/scripts/src/commands/app-data/convert/report/{manifest.utils.ts => report.utils.ts} (100%) diff --git a/packages/scripts/src/commands/app-data/convert/report/manifest.ts b/packages/scripts/src/commands/app-data/convert/report/report.ts similarity index 88% rename from packages/scripts/src/commands/app-data/convert/report/manifest.ts rename to packages/scripts/src/commands/app-data/convert/report/report.ts index 4f4258221..967532f61 100644 --- a/packages/scripts/src/commands/app-data/convert/report/manifest.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.ts @@ -2,28 +2,27 @@ * TODO * - Include sheet types/subtypes reporter * - Include asset references - * - Better to name as manifest or report * - Document authors to gitignore outputs (if not wanted) * * Future * - Specific feature manifests that declare action/component namespaces - * - possible recommendations/optimisations from manifest + * - possible recommendations/optimisations from reports * - how to handle implicit deps (one component uses another) * - possibly will require runtime error/warning/prompt * - handle dynamic * - QA components/actions that don't exist * - possibly export list of COMPONENTS_AVAILABLE (or similar... or just use main list lookup) - * - also consider asset manifest (but would need to ensure dynamic assets included, plus param list + template value) + * - also consider asset report (but would need to ensure dynamic assets included, plus param list + template value) * - handle implicit components (check imports (?)) */ import { IDeploymentConfigJson } from "data-models"; import { IParsedWorkbookData } from "../types"; import { TemplateSummaryReport } from "./reporters"; -import { IReport } from "./manifest.types"; +import { IReport } from "./report.types"; import { resolve, dirname } from "path"; import { writeFile, ensureDir, emptyDir } from "fs-extra"; -import { generateMarkdownTable } from "./manifest.utils"; +import { generateMarkdownTable } from "./report.utils"; import { logOutput } from "shared"; import chalk from "chalk"; diff --git a/packages/scripts/src/commands/app-data/convert/report/manifest.types.ts b/packages/scripts/src/commands/app-data/convert/report/report.types.ts similarity index 86% rename from packages/scripts/src/commands/app-data/convert/report/manifest.types.ts rename to packages/scripts/src/commands/app-data/convert/report/report.types.ts index 182ffd642..680bacc9f 100644 --- a/packages/scripts/src/commands/app-data/convert/report/manifest.types.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.types.ts @@ -1,4 +1,4 @@ -// Use union types to allow strong typing by manifest display type +// Use union types to allow strong typing by report display type export type IReport = IReportTable | IReportText; interface IReportBase { diff --git a/packages/scripts/src/commands/app-data/convert/report/manifest.utils.spec.ts b/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts similarity index 80% rename from packages/scripts/src/commands/app-data/convert/report/manifest.utils.spec.ts rename to packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts index 9601db47e..abcd2ddaf 100644 --- a/packages/scripts/src/commands/app-data/convert/report/manifest.utils.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts @@ -1,7 +1,7 @@ -import { generateMarkdownTable, sortJsonByKey } from "./manifest.utils"; +import { generateMarkdownTable, sortJsonByKey } from "./report.utils"; -/** yarn workspace scripts test -t manifest.utils.spec.ts */ -describe("manifest utils", () => { +/** yarn workspace scripts test -t report.utils.spec.ts */ +describe("report utils", () => { it("sortJsonByKey", () => { const mockJson = { b: "first entry", a: "second entry" }; const res = sortJsonByKey(mockJson); diff --git a/packages/scripts/src/commands/app-data/convert/report/manifest.utils.ts b/packages/scripts/src/commands/app-data/convert/report/report.utils.ts similarity index 100% rename from packages/scripts/src/commands/app-data/convert/report/manifest.utils.ts rename to packages/scripts/src/commands/app-data/convert/report/report.utils.ts diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts index a25b0189a..0d63f4419 100644 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts @@ -1,8 +1,8 @@ import { FlowTypes } from "data-models"; import { IParsedWorkbookData } from "../../types"; -import { sortJsonByKey } from "../manifest.utils"; -import { IReport } from "../manifest.types"; +import { sortJsonByKey } from "../report.utils"; +import { IReport } from "../report.types"; interface ITemplateSummary { components: Record; From a4e874193fb2b16b83a2eb7cae9306433c8b139f Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 15:58:55 -0700 Subject: [PATCH 183/204] chore: update gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index f2f6ca5db..754a22b7b 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,6 @@ src/assets/app_data scripts/config/token.json .eslintcache -report.* # Ignore yarn-v2 dependencies # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored From 5900e72344355570b59fcd0b005886d9eb1cad7f Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Mon, 16 Sep 2024 16:02:18 -0700 Subject: [PATCH 184/204] chore: code tidying --- packages/scripts/src/commands/app-data/convert/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/index.ts b/packages/scripts/src/commands/app-data/convert/index.ts index 5f13455f1..939ff2de2 100644 --- a/packages/scripts/src/commands/app-data/convert/index.ts +++ b/packages/scripts/src/commands/app-data/convert/index.ts @@ -19,7 +19,7 @@ import { standardiseNewlines, } from "./utils"; import { FlowParserProcessor } from "./processors/flowParser/flowParser"; -import { ManifestGenerator } from "./manifest/manifest"; +import { ReportGenerator } from "./report/report"; /*************************************************************************************** * CLI @@ -131,7 +131,7 @@ export class AppDataConverter { processor.logger = this.logger; const jsonFlows = Object.values(combinedOutputsHashmap); const result = (await processor.process(jsonFlows)) as IParsedWorkbookData; - await new ManifestGenerator(this.activeDeployment).process(result); + await new ReportGenerator(this.activeDeployment).process(result); // TODO - write to disk and log const { errors, warnings } = this.logOutputs(result); From 0d675b76ed80f40eba52fa3834f24c96bc5432d2 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 17 Sep 2024 12:01:02 +0100 Subject: [PATCH 185/204] chore: revert package.json line endings --- package.json | 368 +++++++++++++++++++++++++-------------------------- 1 file changed, 184 insertions(+), 184 deletions(-) diff --git a/package.json b/package.json index be75e1a16..0f9d2d39e 100644 --- a/package.json +++ b/package.json @@ -1,184 +1,184 @@ -{ - "name": "frontend", - "version": "0.16.35", - "author": "IDEMS International", - "license": "See LICENSE", - "homepage": "https://idems.international/", - "description": "", - "workspaces": [ - "packages/*", - "packages/@idemsInternational/*" - ], - "scripts": { - "ng": "ng", - "start": "yarn prepare && ng serve --open", - "start:local": "yarn prepare && concurrently --kill-others --raw \"ng serve --open\" \"yarn workflow sync_local\"", - "prepare": "node .husky/prepare.js && yarn workflow populate_src_assets", - "build": "yarn prepare && ng build --configuration=production", - "compodoc:generate": "compodoc --tsconfig tsconfig.docs.json --theme material --assetsFolder 'documentation/docs/assets' --output documentation/docs/generated --disablePrivate --disableLifeCycleHooks", - "compodoc:serve": "yarn compodoc:generate --serve --watch --open", - "scripts": "yarn workspace scripts start", - "workflow": "yarn workspace scripts start workflow run", - "server": "yarn workspace server", - "translations": "yarn workspace translations start", - "test:workspaces": "yarn workspace shared test && yarn workspace scripts test", - "test:unit": "ng test", - "test:unit:ci": "ng test --no-watch --no-progress", - "test:e2e": "yarn workspace test-e2e start", - "lint": "ng lint", - "lint:docs": "cspell \"documentation/docs/**/*.md\" --config \"cspell.config.yml\"", - "analyse": "ng build --configuration=production --stats-json && npx webpack-bundle-analyzer www/stats.json", - "version": "yarn workspace scripts start version", - "format": "yarn prettier . --write --log-level silent" - }, - "private": true, - "dependencies": { - "@angular/animations": "^17.2.2", - "@angular/common": "~17.2.2", - "@angular/core": "~17.2.2", - "@angular/elements": "^17.2.2", - "@angular/forms": "~17.2.2", - "@angular/platform-browser": "~17.2.2", - "@angular/platform-browser-dynamic": "~17.2.2", - "@angular/router": "~17.2.2", - "@capacitor-community/file-opener": "^6.0.0", - "@capacitor-firebase/authentication": "^6.1.0", - "@capacitor-firebase/crashlytics": "^6.1.0", - "@capacitor-firebase/performance": "^6.1.0", - "@capacitor/android": "^6.0.0", - "@capacitor/app": "^6.0.0", - "@capacitor/clipboard": "^6.0.0", - "@capacitor/core": "^6.0.0", - "@capacitor/device": "^6.0.0", - "@capacitor/filesystem": "^6.0.0", - "@capacitor/ios": "^6.0.0", - "@capacitor/local-notifications": "^6.0.0", - "@capacitor/push-notifications": "^6.0.0", - "@capacitor/share": "^6.0.0", - "@capacitor/splash-screen": "^6.0.0", - "@capawesome/capacitor-app-update": "^6.0.0", - "@ionic-native/core": "^5.36.0", - "@ionic-native/device": "^5.36.0", - "@ionic-native/http": "^5.36.0", - "@ionic-native/media": "^5.36.0", - "@ionic-native/status-bar": "^5.36.0", - "@ionic/angular": "^7.7.3", - "@ionic/pwa-elements": "^3.2.2", - "@sentry/angular-ivy": "^7.102.1", - "@supabase/supabase-js": "^2.39.0", - "@types/file-saver": "^2.0.7", - "bootstrap-datepicker": "^1.10.0", - "capacitor-blob-writer": "^1.1.17", - "clone": "^2.1.2", - "core-js": "^3.33.3", - "data-models": "workspace:*", - "date-fns": "^2.30.0", - "deep-object-diff": "^1.1.9", - "dexie": "^3.2.4", - "dexie-observable": "3.0.0-beta.11", - "dexie-syncable": "^3.0.0-beta.10", - "document-register-element": "^1.14.10", - "dompurify": "^3.1.3", - "extract-math": "^1.2.3", - "file-saver": "^2.0.5", - "firebase": "^10.7.0", - "globalthis": "^1.0.3", - "howler": "^2.2.4", - "html5sortable": "^0.13.3", - "intro.js": "^3.4.0", - "jquery": "^3.7.1", - "jquery-touchswipe": "^1.6.19", - "katex": "^0.16.10", - "lottie-web": "^5.12.2", - "marked": "^2.1.3", - "marked-smartypants-lite": "^1.0.2", - "mergexml": "^1.2.3", - "ng2-nouislider": "^2.0.0", - "ngx-extended-pdf-viewer": "18.1.9", - "ngx-lottie": "^10.0.0", - "ngx-matomo-client": "^6.0.2", - "nouislider": "^15.7.1", - "qrcode": "^1.5.3", - "rxdb": "^14.17.1", - "rxjs": "^7.8.1", - "scripts": "workspace:*", - "shared": "workspace:*", - "signature_pad": "^4.1.7", - "stacktrace-js": "^2.0.2", - "swiper": "^8.4.7", - "trusted-types": "^2.0.0", - "zone.js": "~0.14.2" - }, - "devDependencies": { - "@angular-devkit/architect": "0.1702.1", - "@angular-devkit/build-angular": "~17.2.1", - "@angular-devkit/core": "~17.2.1", - "@angular-devkit/schematics": "~17.2.1", - "@angular-eslint/builder": "17.2.1", - "@angular-eslint/eslint-plugin": "17.2.1", - "@angular-eslint/eslint-plugin-template": "17.2.1", - "@angular-eslint/schematics": "17.2.1", - "@angular-eslint/template-parser": "17.2.1", - "@angular/cli": "~17.2.1", - "@angular/compiler": "~17.2.2", - "@angular/compiler-cli": "~17.2.2", - "@angular/language-service": "~17.2.2", - "@capacitor/cli": "^6.0.0", - "@compodoc/compodoc": "^1.1.23", - "@ionic/angular-toolkit": "^11.0.1", - "@ionic/cli": "^7.1.5", - "@schematics/angular": "~17.0.3", - "@swc/helpers": "^0.5.1", - "@types/clone": "^2.1.1", - "@types/howler": "^2.2.2", - "@types/intro.js": "^3.0.1", - "@types/jasmine": "~4.3.0", - "@types/lokijs": "^1.5.7", - "@types/marked": "^2.0.3", - "@types/node": "^18.18.13", - "@types/nouislider": "^15.0.0", - "@types/swiper": "~4.2.0", - "@typescript-eslint/eslint-plugin": "^6.13.1", - "@typescript-eslint/parser": "^6.13.1", - "codelyzer": "^6.0.2", - "concurrently": "^6.2.0", - "cspell": "^6.20.1", - "eslint": "^8.54.0", - "eslint-config-prettier": "^9.1.0", - "eslint-config-standard-with-typescript": "latest", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jasmine": "^4.1.3", - "eslint-plugin-jsdoc": "40.1.1", - "eslint-plugin-n": "^15.7.0", - "eslint-plugin-prefer-arrow": "1.2.3", - "eslint-plugin-promise": "^6.1.1", - "firebase-tools": "^13.6.0", - "husky": "^8.0.3", - "jasmine-core": "~4.5.0", - "jasmine-spec-reporter": "~7.0.0", - "karma": "~6.4.0", - "karma-chrome-launcher": "~3.1.0", - "karma-coverage": "~2.2.0", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.0.0", - "lint-staged": "^15.2.2", - "prettier": "^3.2.5", - "typescript": "5.2.2" - }, - "resolutions": { - "@supabase/gotrue-js": "2.61.0" - }, - "lint-staged": { - "*.{ts,scss,html}": [ - "prettier --write" - ] - }, - "browser": { - "__hack__": "add support for lokijs in browser by ignoring fs", - "fs": false - }, - "engines": { - "node": ">=18.x" - }, - "packageManager": "yarn@3.3.1" -} +{ + "name": "frontend", + "version": "0.16.35", + "author": "IDEMS International", + "license": "See LICENSE", + "homepage": "https://idems.international/", + "description": "", + "workspaces": [ + "packages/*", + "packages/@idemsInternational/*" + ], + "scripts": { + "ng": "ng", + "start": "yarn prepare && ng serve --open", + "start:local": "yarn prepare && concurrently --kill-others --raw \"ng serve --open\" \"yarn workflow sync_local\"", + "prepare": "node .husky/prepare.js && yarn workflow populate_src_assets", + "build": "yarn prepare && ng build --configuration=production", + "compodoc:generate": "compodoc --tsconfig tsconfig.docs.json --theme material --assetsFolder 'documentation/docs/assets' --output documentation/docs/generated --disablePrivate --disableLifeCycleHooks", + "compodoc:serve": "yarn compodoc:generate --serve --watch --open", + "scripts": "yarn workspace scripts start", + "workflow": "yarn workspace scripts start workflow run", + "server": "yarn workspace server", + "translations": "yarn workspace translations start", + "test:workspaces": "yarn workspace shared test && yarn workspace scripts test", + "test:unit": "ng test", + "test:unit:ci": "ng test --no-watch --no-progress", + "test:e2e": "yarn workspace test-e2e start", + "lint": "ng lint", + "lint:docs": "cspell \"documentation/docs/**/*.md\" --config \"cspell.config.yml\"", + "analyse": "ng build --configuration=production --stats-json && npx webpack-bundle-analyzer www/stats.json", + "version": "yarn workspace scripts start version", + "format": "yarn prettier . --write --log-level silent" + }, + "private": true, + "dependencies": { + "@angular/animations": "^17.2.2", + "@angular/common": "~17.2.2", + "@angular/core": "~17.2.2", + "@angular/elements": "^17.2.2", + "@angular/forms": "~17.2.2", + "@angular/platform-browser": "~17.2.2", + "@angular/platform-browser-dynamic": "~17.2.2", + "@angular/router": "~17.2.2", + "@capacitor-community/file-opener": "^6.0.0", + "@capacitor-firebase/authentication": "^6.1.0", + "@capacitor-firebase/crashlytics": "^6.1.0", + "@capacitor-firebase/performance": "^6.1.0", + "@capacitor/android": "^6.0.0", + "@capacitor/app": "^6.0.0", + "@capacitor/clipboard": "^6.0.0", + "@capacitor/core": "^6.0.0", + "@capacitor/device": "^6.0.0", + "@capacitor/filesystem": "^6.0.0", + "@capacitor/ios": "^6.0.0", + "@capacitor/local-notifications": "^6.0.0", + "@capacitor/push-notifications": "^6.0.0", + "@capacitor/share": "^6.0.0", + "@capacitor/splash-screen": "^6.0.0", + "@capawesome/capacitor-app-update": "^6.0.0", + "@ionic-native/core": "^5.36.0", + "@ionic-native/device": "^5.36.0", + "@ionic-native/http": "^5.36.0", + "@ionic-native/media": "^5.36.0", + "@ionic-native/status-bar": "^5.36.0", + "@ionic/angular": "^7.7.3", + "@ionic/pwa-elements": "^3.2.2", + "@sentry/angular-ivy": "^7.102.1", + "@supabase/supabase-js": "^2.39.0", + "@types/file-saver": "^2.0.7", + "bootstrap-datepicker": "^1.10.0", + "capacitor-blob-writer": "^1.1.17", + "clone": "^2.1.2", + "core-js": "^3.33.3", + "data-models": "workspace:*", + "date-fns": "^2.30.0", + "deep-object-diff": "^1.1.9", + "dexie": "^3.2.4", + "dexie-observable": "3.0.0-beta.11", + "dexie-syncable": "^3.0.0-beta.10", + "document-register-element": "^1.14.10", + "dompurify": "^3.1.3", + "extract-math": "^1.2.3", + "file-saver": "^2.0.5", + "firebase": "^10.7.0", + "globalthis": "^1.0.3", + "howler": "^2.2.4", + "html5sortable": "^0.13.3", + "intro.js": "^3.4.0", + "jquery": "^3.7.1", + "jquery-touchswipe": "^1.6.19", + "katex": "^0.16.10", + "lottie-web": "^5.12.2", + "marked": "^2.1.3", + "marked-smartypants-lite": "^1.0.2", + "mergexml": "^1.2.3", + "ng2-nouislider": "^2.0.0", + "ngx-extended-pdf-viewer": "18.1.9", + "ngx-lottie": "^10.0.0", + "ngx-matomo-client": "^6.0.2", + "nouislider": "^15.7.1", + "qrcode": "^1.5.3", + "rxdb": "^14.17.1", + "rxjs": "^7.8.1", + "scripts": "workspace:*", + "shared": "workspace:*", + "signature_pad": "^4.1.7", + "stacktrace-js": "^2.0.2", + "swiper": "^8.4.7", + "trusted-types": "^2.0.0", + "zone.js": "~0.14.2" + }, + "devDependencies": { + "@angular-devkit/architect": "0.1702.1", + "@angular-devkit/build-angular": "~17.2.1", + "@angular-devkit/core": "~17.2.1", + "@angular-devkit/schematics": "~17.2.1", + "@angular-eslint/builder": "17.2.1", + "@angular-eslint/eslint-plugin": "17.2.1", + "@angular-eslint/eslint-plugin-template": "17.2.1", + "@angular-eslint/schematics": "17.2.1", + "@angular-eslint/template-parser": "17.2.1", + "@angular/cli": "~17.2.1", + "@angular/compiler": "~17.2.2", + "@angular/compiler-cli": "~17.2.2", + "@angular/language-service": "~17.2.2", + "@capacitor/cli": "^6.0.0", + "@compodoc/compodoc": "^1.1.23", + "@ionic/angular-toolkit": "^11.0.1", + "@ionic/cli": "^7.1.5", + "@schematics/angular": "~17.0.3", + "@swc/helpers": "^0.5.1", + "@types/clone": "^2.1.1", + "@types/howler": "^2.2.2", + "@types/intro.js": "^3.0.1", + "@types/jasmine": "~4.3.0", + "@types/lokijs": "^1.5.7", + "@types/marked": "^2.0.3", + "@types/node": "^18.18.13", + "@types/nouislider": "^15.0.0", + "@types/swiper": "~4.2.0", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", + "codelyzer": "^6.0.2", + "concurrently": "^6.2.0", + "cspell": "^6.20.1", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.1.0", + "eslint-config-standard-with-typescript": "latest", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jasmine": "^4.1.3", + "eslint-plugin-jsdoc": "40.1.1", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-prefer-arrow": "1.2.3", + "eslint-plugin-promise": "^6.1.1", + "firebase-tools": "^13.6.0", + "husky": "^8.0.3", + "jasmine-core": "~4.5.0", + "jasmine-spec-reporter": "~7.0.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "lint-staged": "^15.2.2", + "prettier": "^3.2.5", + "typescript": "5.2.2" + }, + "resolutions": { + "@supabase/gotrue-js": "2.61.0" + }, + "lint-staged": { + "*.{ts,scss,html}": [ + "prettier --write" + ] + }, + "browser": { + "__hack__": "add support for lokijs in browser by ignoring fs", + "fs": false + }, + "engines": { + "node": ">=18.x" + }, + "packageManager": "yarn@3.3.1" +} From 7200103966258e0d62aaa852b23a865fb6a537fe Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Tue, 17 Sep 2024 13:33:00 +0100 Subject: [PATCH 186/204] empty commit to test gh action trigger From 44757d0b2d58be7f31aa5545763a2f8ca7a1d605 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 17 Sep 2024 10:10:38 -0700 Subject: [PATCH 187/204] chore: code tidying --- .../src/commands/app-data/convert/report/report.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/report/report.ts b/packages/scripts/src/commands/app-data/convert/report/report.ts index 967532f61..492601d1a 100644 --- a/packages/scripts/src/commands/app-data/convert/report/report.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.ts @@ -2,12 +2,11 @@ * TODO * - Include sheet types/subtypes reporter * - Include asset references - * - Document authors to gitignore outputs (if not wanted) + * - Handle test fail trying to run from app data converter spec + * - Add spec to app data converter that it generates reports (just folder) * * Future - * - Specific feature manifests that declare action/component namespaces * - possible recommendations/optimisations from reports - * - how to handle implicit deps (one component uses another) * - possibly will require runtime error/warning/prompt * - handle dynamic * - QA components/actions that don't exist @@ -26,11 +25,10 @@ import { generateMarkdownTable } from "./report.utils"; import { logOutput } from "shared"; import chalk from "chalk"; -// Testing notes -// yarn workflow sync_sheets --skip-download - /** - * + * Create summary reports based on converted app data + * Individual reports are created by child reporters, with outputs stored in both + * json and markdown formats for easier interpretation **/ export class ReportGenerator { constructor(private deployment: IDeploymentConfigJson) {} @@ -39,7 +37,6 @@ export class ReportGenerator { const { template_actions, template_components } = await new TemplateSummaryReport().process( data ); - const outputReports = { template_actions, template_components }; await this.writeOutputs(outputReports); } From b937e0d943bd1837519b590d2f292195e1857e11 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 17 Sep 2024 10:16:40 -0700 Subject: [PATCH 188/204] chore: rename deployment config example --- packages/data-models/deployment.model.ts | 4 ++-- packages/scripts/src/commands/deployment/common.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index bb5271e85..81fd4d06d 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -193,11 +193,11 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { }; /** Full example of just all config once merged with defaults */ -export const DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS: IDeploymentConfig = { +export const DEPLOYMENT_CONFIG_DEFAULTS: IDeploymentConfig = { ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, // NOTE - app_config will be populated during config generation app_config: {} as any, - name: "Full Config Example", + name: "", google_drive: { assets_folder_id: "", sheets_folder_id: "", diff --git a/packages/scripts/src/commands/deployment/common.ts b/packages/scripts/src/commands/deployment/common.ts index 1e39febaa..10e8050cf 100644 --- a/packages/scripts/src/commands/deployment/common.ts +++ b/packages/scripts/src/commands/deployment/common.ts @@ -3,7 +3,7 @@ import { logWarning } from "shared"; import { readJSONSync } from "fs-extra"; import path from "path"; -import { DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS, getDefaultAppConfig } from "data-models"; +import { DEPLOYMENT_CONFIG_DEFAULTS, getDefaultAppConfig } from "data-models"; import type { IDeploymentConfig, IDeploymentConfigGenerated, @@ -23,7 +23,7 @@ export function generateDeploymentConfig(name: string) { // apply overrides to single nested properties const app_config = toEmptyObject(getDefaultAppConfig()); // combine with deployment config defaults - const config: IDeploymentConfigGenerated = { ...DEPLOYMENT_CONFIG_EXAMPLE_DEFAULTS, app_config }; + const config: IDeploymentConfigGenerated = { ...DEPLOYMENT_CONFIG_DEFAULTS, app_config }; config.name = name; return config; } From 4e1356446f6d8285faa057fc77beeaaa51bc773d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 17 Sep 2024 18:26:08 -0700 Subject: [PATCH 189/204] refactor: skin service config updates --- src/app/shared/services/skin/skin.service.ts | 96 ++++++++++++-------- src/app/shared/utils/utils.ts | 6 +- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/src/app/shared/services/skin/skin.service.ts b/src/app/shared/services/skin/skin.service.ts index 0d910b599..1386e8796 100644 --- a/src/app/shared/services/skin/skin.service.ts +++ b/src/app/shared/services/skin/skin.service.ts @@ -1,8 +1,8 @@ import { Injectable } from "@angular/core"; -import { BehaviorSubject } from "rxjs"; import { LocalStorageService } from "src/app/shared/services/local-storage/local-storage.service"; import { IAppConfig, IAppSkin } from "data-models"; -import { arrayToHashmap } from "../../utils"; +import { updatedDiff } from "deep-object-diff"; +import { arrayToHashmap, deepMergeObjects, RecursivePartial } from "../../utils"; import { AppConfigService } from "../app-config/app-config.service"; import { TemplateService } from "../../components/template/services/template.service"; import { ThemeService } from "src/app/feature/theme/services/theme.service"; @@ -12,11 +12,11 @@ import { SyncServiceBase } from "../syncService.base"; providedIn: "root", }) export class SkinService extends SyncServiceBase { - // A hashmap of all skins available to the current deployment + /** A hashmap of all skins available to the current deployment */ private availableSkins: Record; - private activeSkin$ = new BehaviorSubject(undefined); - private appConfig: IAppConfig; - private skinsConfig: IAppConfig["APP_SKINS"]; + + /** Track overrides required to undo a previously applied skin (if applying another) */ + private revertOverride: RecursivePartial = {}; constructor( private localStorageService: LocalStorageService, @@ -34,17 +34,8 @@ export class SkinService extends SyncServiceBase { this.appConfigService, this.themeService, this.templateService, - this.appConfigService, ]); - this.subscribeToAppConfigChanges(); - // Retrieve the last active skin and apply it. Fallback on deployment's default skin - // if there is no last active skin, or if it is not "available" in current appConfig - const lastActiveSkinName = this.getActiveSkinName(); - let targetSkinName = this.skinsConfig.defaultSkinName; - if (lastActiveSkinName && this.availableSkins.hasOwnProperty(lastActiveSkinName)) { - targetSkinName = lastActiveSkinName; - } - this.setSkin(targetSkinName, true); + this.loadActiveSkin(); } /** @@ -53,12 +44,10 @@ export class SkinService extends SyncServiceBase { * @param [isInit=false] Whether or not the function is being triggered by the service's initialisation * */ public setSkin(skinName: string, isInit = false) { - if (skinName in this.availableSkins) { + if (this.availableSkins.hasOwnProperty(skinName)) { const targetSkin = this.availableSkins[skinName]; - // console.log("[SET SKIN]", skinName, targetSkin); - this.activeSkin$.next(targetSkin); - // Update appConfig to reflect any overrides defined by the skin - this.appConfigService.setAppConfig(targetSkin.appConfig); + this.applyConfigOverride(targetSkin); + if (!isInit) { // Update default values when skin changed to allow for skin-specific global overrides // Don't run on initialisation, since the skin and appConfig services must init before the template service and its dependencies @@ -80,34 +69,65 @@ export class SkinService extends SyncServiceBase { return this.localStorageService.getProtected("APP_SKIN"); } - /** Get the full active skin, from the skin name saved in local storage */ - public getActiveSkin() { - const activeSkinName = this.getActiveSkin(); - return this.availableSkins[activeSkinName]; + /** + * Skin overrides are designed to be merged on top of the default app config + * When applying a new skin calculate the config changes required to both + * revert any previous skin override and apply new + */ + private applyConfigOverride(skin: IAppSkin) { + // Base override combines previous skin revert and current skin config + const baseConfig = deepMergeObjects(this.revertOverride, skin.appConfig); + + // Generate full overrides and revert + const override: RecursivePartial = {}; + const revert: RecursivePartial = {}; + const config = this.appConfigService.appConfig(); + + for (const [key, value] of Object.entries(baseConfig)) { + // As skins only provide partial update for app config, merge each partial + // update with the current value + const update = deepMergeObjects({}, config[key], value); + override[key] = update; + // Track what has changed to be able to revert back in future + revert[key] = updatedDiff(update, config[key]); + } + + // Apply Changes + console.log("[SKIN] SET", { skin, override, revert }); + this.appConfigService.setAppConfig(override); + this.revertOverride = revert; + return override; + } + + /** + * Load the active app skin. Loads previously stored configuration if available, + * with fallback to default app skin + */ + private loadActiveSkin() { + const { available, defaultSkinName } = this.appConfigService.appConfig().APP_SKINS; + this.availableSkins = arrayToHashmap(available, "name"); + const activeSkinName = this.getActiveSkinName(); + if (activeSkinName && this.availableSkins.hasOwnProperty(activeSkinName)) { + this.setSkin(activeSkinName, true); + } else { + this.setSkin(defaultSkinName, true); + } } private applySkinThemeChanges() { - const targetSkinDefaultTheme = this.appConfig.APP_THEMES.defaultThemeName; - if (targetSkinDefaultTheme) { - this.themeService.setTheme(targetSkinDefaultTheme); + const { available, defaultThemeName } = this.appConfigService.appConfig().APP_THEMES; + if (defaultThemeName) { + this.themeService.setTheme(defaultThemeName); } // If target skin has no default theme and the current theme is not available in the target skin, // then set theme to the first available theme of the target skin else if (!this.isCurrentThemeAvailableInTargetSkin()) { - this.themeService.setTheme(this.appConfig.APP_THEMES.available[0]); + this.themeService.setTheme(available[0]); } } private isCurrentThemeAvailableInTargetSkin() { const currentTheme = this.themeService.getCurrentTheme(); - return this.appConfig.APP_THEMES.available.includes(currentTheme); - } - - subscribeToAppConfigChanges() { - this.appConfigService.appConfig$.subscribe((appConfig: IAppConfig) => { - this.appConfig = appConfig; - this.skinsConfig = this.appConfig.APP_SKINS; - this.availableSkins = arrayToHashmap(this.skinsConfig.available, "name"); - }); + return this.appConfigService.appConfig().APP_THEMES.available.includes(currentTheme); } } diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 301b754d8..23b7ded27 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -375,7 +375,7 @@ export function stringToIntegerHash(str: string) { * @param target * @param ...sources */ -export function deepMergeObjects(target: any = {}, ...sources: any) { +export function deepMergeObjects>(target: T = {} as T, ...sources: any) { if (!sources.length) return target; const source = sources.shift(); @@ -393,8 +393,8 @@ export function deepMergeObjects(target: any = {}, ...sources: any) { return deepMergeObjects(target, ...sources); } -export function deepDiffObjects(a: T, b: U) { - return diff(a, b) as RecursivePartial; +export function deepDiffObjects(original: T, updated: U) { + return diff(original, updated) as RecursivePartial; } /** From 07ae09bb145aafa18249f1a8778dd79b42f1996e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 17 Sep 2024 22:13:35 -0700 Subject: [PATCH 190/204] refactor: skin service and add tests --- .../shared/services/skin/skin.service.spec.ts | 111 +++++++++++++++++- src/app/shared/services/skin/skin.service.ts | 38 +++--- 2 files changed, 127 insertions(+), 22 deletions(-) diff --git a/src/app/shared/services/skin/skin.service.spec.ts b/src/app/shared/services/skin/skin.service.spec.ts index 0479c4c2a..c2279d8a8 100644 --- a/src/app/shared/services/skin/skin.service.spec.ts +++ b/src/app/shared/services/skin/skin.service.spec.ts @@ -1,16 +1,121 @@ import { TestBed } from "@angular/core/testing"; import { SkinService } from "./skin.service"; +import { LocalStorageService } from "../local-storage/local-storage.service"; +import { MockLocalStorageService } from "../local-storage/local-storage.service.spec"; +import { AppConfigService } from "../app-config/app-config.service"; +import { MockAppConfigService } from "../app-config/app-config.service.spec"; +import { TemplateService } from "../../components/template/services/template.service"; +import { ThemeService } from "src/app/feature/theme/services/theme.service"; +import { IAppConfig, IAppSkin } from "packages/data-models"; +class MockThemeService implements Partial { + ready() { + return true; + } + setTheme() {} + getCurrentTheme() { + return "mock_them"; + } +} + +class MockTemplateService implements Partial { + ready() { + return true; + } + async initialiseDefaultFieldAndGlobals() { + return; + } +} + +const MOCK_SKIN_1: IAppSkin = { + name: "MOCK_SKIN_1", + appConfig: { APP_HEADER_DEFAULTS: { title: "mock 1", colour: "primary" } }, +}; +const MOCK_SKIN_2: IAppSkin = { + name: "MOCK_SKIN_2", + appConfig: { APP_HEADER_DEFAULTS: { title: "mock 2", variant: "compact" } }, +}; + +const MOCK_APP_CONFIG: Partial = { + APP_HEADER_DEFAULTS: { + title: "default", + collapse: false, + colour: "none", + should_minimize_app_on_back: () => true, + should_show_back_button: () => true, + should_show_menu_button: () => true, + variant: "default", + }, + APP_SKINS: { + available: [MOCK_SKIN_1, MOCK_SKIN_2], + defaultSkinName: "MOCK_SKIN_1", + }, +}; + +/** + * Call standalone tests via: + * yarn ng test --include src/app/shared/services/skin/skin.service.spec.ts + */ describe("SkinService", () => { let service: SkinService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [ + { provide: LocalStorageService, useValue: new MockLocalStorageService() }, + { + provide: AppConfigService, + useValue: new MockAppConfigService(MOCK_APP_CONFIG), + }, + { provide: TemplateService, useValue: new MockTemplateService() }, + // TODO - create better mock and test methods + { provide: ThemeService, useValue: new MockThemeService() }, + ], + }); service = TestBed.inject(SkinService); }); - it("should be created", () => { - expect(service).toBeTruthy(); + it("creates hashmap of available skins on init", () => { + const skins = service["availableSkins"]; + expect(skins).toEqual({ MOCK_SKIN_1, MOCK_SKIN_2 }); + }); + + it("loads default skin on init", () => { + expect(service.getActiveSkinName()).toEqual("MOCK_SKIN_1"); }); + + it("generates override and revert configs", () => { + expect(service["revertOverride"]).toEqual({ + APP_HEADER_DEFAULTS: { title: "default", colour: "none" }, + }); + }); + + it("reverts previous override when applying another skin", () => { + // MOCK_SKIN_1 will already be applied on load + const override = service["generateOverrideConfig"](MOCK_SKIN_2); + console.log({ override }); + // creates a deep merge of override properties on top of current + expect(override).toEqual({ + APP_HEADER_DEFAULTS: { + // revert changes only available in skin_1 + colour: "none", + // apply changes from skin_2 + title: "mock 2", + variant: "compact", + }, + }); + const revert = service["generateRevertConfig"](MOCK_SKIN_2); + + // creates config revert to undo just the skin changes + expect(revert).toEqual({ + APP_HEADER_DEFAULTS: { + // only revert changes remaining from skin_2 + title: "default", + variant: "default", + }, + }); + }); + + // TODO - add tests for setSkin method and side-effects }); diff --git a/src/app/shared/services/skin/skin.service.ts b/src/app/shared/services/skin/skin.service.ts index 1386e8796..503f3533a 100644 --- a/src/app/shared/services/skin/skin.service.ts +++ b/src/app/shared/services/skin/skin.service.ts @@ -46,7 +46,12 @@ export class SkinService extends SyncServiceBase { public setSkin(skinName: string, isInit = false) { if (this.availableSkins.hasOwnProperty(skinName)) { const targetSkin = this.availableSkins[skinName]; - this.applyConfigOverride(targetSkin); + + const override = this.generateOverrideConfig(targetSkin); + const revert = this.generateRevertConfig(targetSkin); + console.log("[SKIN] SET", { targetSkin, override, revert }); + this.appConfigService.setAppConfig(override); + this.revertOverride = revert; if (!isInit) { // Update default values when skin changed to allow for skin-specific global overrides @@ -74,29 +79,24 @@ export class SkinService extends SyncServiceBase { * When applying a new skin calculate the config changes required to both * revert any previous skin override and apply new */ - private applyConfigOverride(skin: IAppSkin) { - // Base override combines previous skin revert and current skin config - const baseConfig = deepMergeObjects(this.revertOverride, skin.appConfig); + private generateOverrideConfig(skin: IAppSkin) { + // Merge onto new object to avoid changing stored revertOverride + const base: RecursivePartial = {}; + return deepMergeObjects(base, this.revertOverride, skin.appConfig); + } - // Generate full overrides and revert - const override: RecursivePartial = {}; + /** Determine config that would need to be applied to revert the new update */ + private generateRevertConfig(skin: IAppSkin) { const revert: RecursivePartial = {}; const config = this.appConfigService.appConfig(); - - for (const [key, value] of Object.entries(baseConfig)) { - // As skins only provide partial update for app config, merge each partial - // update with the current value - const update = deepMergeObjects({}, config[key], value); - override[key] = update; + for (const key of Object.keys(skin.appConfig)) { + // When reverting the skin, should target the current config value unless + // previously overridden (in which case target initial value) + const revertTarget = deepMergeObjects({}, config[key], this.revertOverride[key]); // Track what has changed to be able to revert back in future - revert[key] = updatedDiff(update, config[key]); + revert[key] = updatedDiff(skin.appConfig[key], revertTarget); } - - // Apply Changes - console.log("[SKIN] SET", { skin, override, revert }); - this.appConfigService.setAppConfig(override); - this.revertOverride = revert; - return override; + return revert; } /** From fa189025ee781c4ff4bfc233f41cb14f4f6587a6 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 17 Sep 2024 22:14:00 -0700 Subject: [PATCH 191/204] fix: tests --- .../app-config/app-config.service.spec.ts | 20 +++++++++-- .../deployment/deployment.service.spec.ts | 33 +++++++++++++++---- .../local-storage.service.spec.ts | 7 ++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/app/shared/services/app-config/app-config.service.spec.ts b/src/app/shared/services/app-config/app-config.service.spec.ts index 928c7d276..c87b16810 100644 --- a/src/app/shared/services/app-config/app-config.service.spec.ts +++ b/src/app/shared/services/app-config/app-config.service.spec.ts @@ -3,26 +3,40 @@ 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 { DeploymentService } from "../deployment/deployment.service"; +import { IAppConfigOverride } from "packages/data-models"; +import { deepMergeObjects } from "../../utils"; /** Mock calls for field values from the template field service to return test data */ export class MockAppConfigService implements Partial { + appConfig = signal(undefined as any); appConfig$ = new BehaviorSubject(undefined as any); // allow additional specs implementing service to provide their own partial appConfig - constructor(mockAppConfig: Partial = {}) { - this.appConfig$.next(mockAppConfig as any); + constructor(private mockAppConfig: Partial = {}) { + this.setAppConfig(); } public ready(timeoutValue?: number) { return true; } + + public setAppConfig(overrides: IAppConfigOverride = {}) { + // merge onto empty object to avoid shared references across tests + const mergedConfig = deepMergeObjects({}, this.mockAppConfig, overrides) as IAppConfig; + this.appConfig$.next(mergedConfig); + this.appConfig.set(mergedConfig); + } } describe("AppConfigService", () => { let service: AppConfigService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [{ provide: DeploymentService, useValue: { config: {} } }], + }); service = TestBed.inject(AppConfigService); }); diff --git a/src/app/shared/services/deployment/deployment.service.spec.ts b/src/app/shared/services/deployment/deployment.service.spec.ts index 693b2ef1f..956a9a482 100644 --- a/src/app/shared/services/deployment/deployment.service.spec.ts +++ b/src/app/shared/services/deployment/deployment.service.spec.ts @@ -1,7 +1,6 @@ -import { DeploymentService } from "./deployment.service"; -import { HttpClient, HttpErrorResponse } from "@angular/common/http"; +import { DEPLOYMENT_CONFIG, DeploymentService } from "./deployment.service"; +import { TestBed } from "@angular/core/testing"; import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "packages/data-models"; -import { asyncData, asyncError } from "src/test/utils"; const mockConfig: IDeploymentRuntimeConfig = { ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, @@ -14,7 +13,28 @@ const mockConfig: IDeploymentRuntimeConfig = { */ describe("Deployment Service", () => { let service: DeploymentService; - let httpClientSpy: jasmine.SpyObj; + + beforeEach(async () => { + TestBed.configureTestingModule({ + providers: [{ provide: DEPLOYMENT_CONFIG, useValue: mockConfig }], + }); + service = TestBed.inject(DeploymentService); + }); + + it("Loads deployment from injection token", async () => { + expect(service.config.name).toEqual(mockConfig.name); + }); +}); + +// LEGACY - should be refactored to test json load during bootstrap + +/** + * +// NOTE - prefer use of spy to `HttpTestingController` as allows to specify responses +// in advance of request (controller must be called after start of init but before complete) + +import { asyncData, asyncError } from "src/test/utils"; + beforeEach(async () => { // NOTE - prefer use of spy to `HttpTestingController` as allows to specify responses @@ -29,7 +49,7 @@ describe("Deployment Service", () => { expect(service.config().name).toEqual(mockConfig.name); }); - it("Handles missing deployment json", async () => { + it("Handles missing deployment json", async () => { const errorResponse = new HttpErrorResponse({ error: "test 404 error", status: 404, @@ -40,4 +60,5 @@ describe("Deployment Service", () => { expect(service.config().name).toEqual(DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS.name); // TODO - could also consider check that logger gets called }); -}); + + */ diff --git a/src/app/shared/services/local-storage/local-storage.service.spec.ts b/src/app/shared/services/local-storage/local-storage.service.spec.ts index d3fe77245..f1a80fb26 100644 --- a/src/app/shared/services/local-storage/local-storage.service.spec.ts +++ b/src/app/shared/services/local-storage/local-storage.service.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from "@angular/core/testing"; import { LocalStorageService } from "./local-storage.service"; +import { IProtectedFieldName } from "packages/data-models"; /** Mock calls to localstorage to store values in-memory */ export class MockLocalStorageService implements Partial { @@ -14,6 +15,12 @@ export class MockLocalStorageService implements Partial { public ready(): boolean { return true; } + public getProtected(field: IProtectedFieldName): string { + return this.getString(`_${field}`); + } + public setProtected(field: IProtectedFieldName, value: string) { + return this.setString(`_${field}`, value); + } } /** From 6eea0a6b54f968e8577c6e01accfb8dfbc78a5c4 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 17 Sep 2024 22:26:09 -0700 Subject: [PATCH 192/204] chore: code tidying --- src/app/shared/services/skin/skin.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/services/skin/skin.service.ts b/src/app/shared/services/skin/skin.service.ts index 503f3533a..665ae5722 100644 --- a/src/app/shared/services/skin/skin.service.ts +++ b/src/app/shared/services/skin/skin.service.ts @@ -89,7 +89,7 @@ export class SkinService extends SyncServiceBase { private generateRevertConfig(skin: IAppSkin) { const revert: RecursivePartial = {}; const config = this.appConfigService.appConfig(); - for (const key of Object.keys(skin.appConfig)) { + for (const key of Object.keys(skin.appConfig || {})) { // When reverting the skin, should target the current config value unless // previously overridden (in which case target initial value) const revertTarget = deepMergeObjects({}, config[key], this.revertOverride[key]); From 0aa1d5ec90ee342d7853c7a3ed0673d6420c2a96 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 17 Sep 2024 22:35:02 -0700 Subject: [PATCH 193/204] chore: code tidying --- src/app/shared/services/app-config/app-config.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/services/app-config/app-config.service.ts b/src/app/shared/services/app-config/app-config.service.ts index 7d273eb7a..ee1c23946 100644 --- a/src/app/shared/services/app-config/app-config.service.ts +++ b/src/app/shared/services/app-config/app-config.service.ts @@ -77,8 +77,8 @@ export class AppConfigService extends SyncServiceBase { const mergedConfig = deepMergeObjects(getDefaultAppConfig(), overrides); this.handleConfigSideEffects(overrides, mergedConfig); - this.appConfig$.next(mergedConfig); this.appConfig.set(mergedConfig); + this.appConfig$.next(mergedConfig); } private handleConfigSideEffects(overrides: IAppConfigOverride = {}, config: IAppConfig) { From 8cb804a95e272fc9116006741b7215a9c246a8e1 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 17 Sep 2024 22:41:06 -0700 Subject: [PATCH 194/204] chore: code tidying --- src/app/shared/services/skin/skin.service.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/shared/services/skin/skin.service.spec.ts b/src/app/shared/services/skin/skin.service.spec.ts index c2279d8a8..b836d336f 100644 --- a/src/app/shared/services/skin/skin.service.spec.ts +++ b/src/app/shared/services/skin/skin.service.spec.ts @@ -15,7 +15,7 @@ class MockThemeService implements Partial { } setTheme() {} getCurrentTheme() { - return "mock_them"; + return "mock_theme"; } } @@ -94,7 +94,6 @@ describe("SkinService", () => { it("reverts previous override when applying another skin", () => { // MOCK_SKIN_1 will already be applied on load const override = service["generateOverrideConfig"](MOCK_SKIN_2); - console.log({ override }); // creates a deep merge of override properties on top of current expect(override).toEqual({ APP_HEADER_DEFAULTS: { From 27712b0befd3ce0440a2d4bbfbc527b09d30e715 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 18 Sep 2024 15:17:35 +0100 Subject: [PATCH 195/204] test: extend skin service tests --- .../theme/services/theme.service.spec.ts | 32 +++++++++++- .../shared/services/skin/skin.service.spec.ts | 50 +++++++++++++++---- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/app/feature/theme/services/theme.service.spec.ts b/src/app/feature/theme/services/theme.service.spec.ts index 45052e902..b1322e66d 100644 --- a/src/app/feature/theme/services/theme.service.spec.ts +++ b/src/app/feature/theme/services/theme.service.spec.ts @@ -1,12 +1,42 @@ import { TestBed } from "@angular/core/testing"; import { ThemeService } from "./theme.service"; +import { LocalStorageService } from "src/app/shared/services/local-storage/local-storage.service"; +import { MockLocalStorageService } from "src/app/shared/services/local-storage/local-storage.service.spec"; +import { AppConfigService } from "src/app/shared/services/app-config/app-config.service"; +import { MockAppConfigService } from "src/app/shared/services/app-config/app-config.service.spec"; +import { IAppConfig } from "packages/data-models"; + +export class MockThemeService implements Partial { + ready() { + return true; + } + setTheme() {} + getCurrentTheme() { + return "mock_theme"; + } +} + +const MOCK_APP_CONFIG: Partial = { + APP_THEMES: { + available: ["MOCK_THEME_1", "MOCK_THEME_2"], + defaultThemeName: "MOCK_THEME_1", + }, +}; describe("ThemeService", () => { let service: ThemeService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [ + { provide: LocalStorageService, useValue: new MockLocalStorageService() }, + { + provide: AppConfigService, + useValue: new MockAppConfigService(MOCK_APP_CONFIG), + }, + ], + }); service = TestBed.inject(ThemeService); }); diff --git a/src/app/shared/services/skin/skin.service.spec.ts b/src/app/shared/services/skin/skin.service.spec.ts index b836d336f..6d908902a 100644 --- a/src/app/shared/services/skin/skin.service.spec.ts +++ b/src/app/shared/services/skin/skin.service.spec.ts @@ -7,17 +7,10 @@ import { AppConfigService } from "../app-config/app-config.service"; import { MockAppConfigService } from "../app-config/app-config.service.spec"; import { TemplateService } from "../../components/template/services/template.service"; import { ThemeService } from "src/app/feature/theme/services/theme.service"; +import { MockThemeService } from "src/app/feature/theme/services/theme.service.spec"; import { IAppConfig, IAppSkin } from "packages/data-models"; - -class MockThemeService implements Partial { - ready() { - return true; - } - setTheme() {} - getCurrentTheme() { - return "mock_theme"; - } -} +import { deepMergeObjects } from "../../utils"; +import clone from "clone"; class MockTemplateService implements Partial { ready() { @@ -51,6 +44,10 @@ const MOCK_APP_CONFIG: Partial = { available: [MOCK_SKIN_1, MOCK_SKIN_2], defaultSkinName: "MOCK_SKIN_1", }, + APP_THEMES: { + available: ["MOCK_THEME_1", "MOCK_THEME_2"], + defaultThemeName: "MOCK_THEME_1", + }, }; /** @@ -85,6 +82,11 @@ describe("SkinService", () => { expect(service.getActiveSkinName()).toEqual("MOCK_SKIN_1"); }); + it("loads active skin from local storage on init if available", () => { + service["localStorageService"].setProtected("APP_SKIN", "MOCK_SKIN_2"); + expect(service.getActiveSkinName()).toEqual("MOCK_SKIN_2"); + }); + it("generates override and revert configs", () => { expect(service["revertOverride"]).toEqual({ APP_HEADER_DEFAULTS: { title: "default", colour: "none" }, @@ -116,5 +118,31 @@ describe("SkinService", () => { }); }); - // TODO - add tests for setSkin method and side-effects + it("sets skin: sets active skin name", () => { + service["setSkin"](MOCK_SKIN_2.name); + expect(service.getActiveSkinName()).toEqual("MOCK_SKIN_2"); + service["setSkin"](MOCK_SKIN_1.name); + expect(service.getActiveSkinName()).toEqual("MOCK_SKIN_1"); + }); + + it("sets skin: sets revertOverride correctly", () => { + // MOCK_SKIN_1 will already be applied on load + service["setSkin"](MOCK_SKIN_2.name); + expect(service["revertOverride"]).toEqual({ + APP_HEADER_DEFAULTS: { + title: "default", + variant: "default", + }, + }); + }); + + it("sets skin: updates AppConfigService.appConfig values", () => { + // MOCK_SKIN_1 will already be applied on load + service["setSkin"](MOCK_SKIN_2.name); + expect(service["appConfigService"].appConfig() as Partial).toEqual( + deepMergeObjects(clone(MOCK_APP_CONFIG), clone(MOCK_SKIN_2).appConfig) + ); + }); + + // TODO - add further tests for setSkin method and side-effects }); From f72b68298f8b8c7131457f649e2bfd1602fab084 Mon Sep 17 00:00:00 2001 From: Johnny McQuade Date: Wed, 18 Sep 2024 16:08:36 +0100 Subject: [PATCH 196/204] fix: handle skin change --- src/app/shared/services/skin/skin.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/shared/services/skin/skin.service.ts b/src/app/shared/services/skin/skin.service.ts index 665ae5722..df0c5c24b 100644 --- a/src/app/shared/services/skin/skin.service.ts +++ b/src/app/shared/services/skin/skin.service.ts @@ -7,6 +7,7 @@ import { AppConfigService } from "../app-config/app-config.service"; import { TemplateService } from "../../components/template/services/template.service"; import { ThemeService } from "src/app/feature/theme/services/theme.service"; import { SyncServiceBase } from "../syncService.base"; +import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root", @@ -19,8 +20,9 @@ export class SkinService extends SyncServiceBase { private revertOverride: RecursivePartial = {}; constructor( - private localStorageService: LocalStorageService, private appConfigService: AppConfigService, + private deploymentService: DeploymentService, + private localStorageService: LocalStorageService, private templateService: TemplateService, private themeService: ThemeService ) { @@ -81,7 +83,7 @@ export class SkinService extends SyncServiceBase { */ private generateOverrideConfig(skin: IAppSkin) { // Merge onto new object to avoid changing stored revertOverride - const base: RecursivePartial = {}; + const base: RecursivePartial = this.deploymentService.config.app_config || {}; return deepMergeObjects(base, this.revertOverride, skin.appConfig); } From f1f6fcbf76e03012451a3f0bf94c9b70d12e2779 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 18 Sep 2024 11:01:13 -0700 Subject: [PATCH 197/204] fix: app config generate and tests --- .../app-config/app-config.service.spec.ts | 48 +++++++++++++++++-- .../services/app-config/app-config.service.ts | 30 ++++++------ .../deployment/deployment.service.spec.ts | 11 +++++ .../shared/services/skin/skin.service.spec.ts | 9 ++++ src/app/shared/services/skin/skin.service.ts | 4 +- 5 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/app/shared/services/app-config/app-config.service.spec.ts b/src/app/shared/services/app-config/app-config.service.spec.ts index c87b16810..2b73be5c0 100644 --- a/src/app/shared/services/app-config/app-config.service.spec.ts +++ b/src/app/shared/services/app-config/app-config.service.spec.ts @@ -5,8 +5,14 @@ import { BehaviorSubject } from "rxjs/internal/BehaviorSubject"; import { IAppConfig } from "../../model"; import { signal } from "@angular/core"; import { DeploymentService } from "../deployment/deployment.service"; -import { IAppConfigOverride } from "packages/data-models"; +import { + getDefaultAppConfig, + IAppConfigOverride, + IDeploymentRuntimeConfig, +} from "packages/data-models"; import { deepMergeObjects } from "../../utils"; +import { firstValueFrom } from "rxjs/internal/firstValueFrom"; +import { MockDeploymentService } from "../deployment/deployment.service.spec"; /** Mock calls for field values from the template field service to return test data */ export class MockAppConfigService implements Partial { @@ -30,17 +36,51 @@ export class MockAppConfigService implements Partial { } } +const MOCK_DEPLOYMENT_CONFIG: Partial = { + app_config: { APP_FOOTER_DEFAULTS: { templateName: "mock_footer" } }, +}; + +/** + * Call standalone tests via: + * yarn ng test --include src/app/shared/services/app-config/app-config.service.spec.ts + */ describe("AppConfigService", () => { let service: AppConfigService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ provide: DeploymentService, useValue: { config: {} } }], + providers: [ + { provide: DeploymentService, useValue: new MockDeploymentService(MOCK_DEPLOYMENT_CONFIG) }, + ], }); service = TestBed.inject(AppConfigService); }); - it("should be created", () => { - expect(service).toBeTruthy(); + it("applies default config overrides on init", () => { + expect(service.appConfig().APP_HEADER_DEFAULTS.title).toEqual( + getDefaultAppConfig().APP_HEADER_DEFAULTS.title + ); + }); + + 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", + }); + // also ensure doesn't unset default deployment + expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("mock_footer"); + }); + + it("emits partial changes on app config update", async () => { + firstValueFrom(service.changes$).then((v) => { + expect(v).toEqual({ APP_HEADER_DEFAULTS: { title: "partial changes" } }); + }); + + service.setAppConfig({ APP_HEADER_DEFAULTS: { title: "partial changes" } }); }); }); diff --git a/src/app/shared/services/app-config/app-config.service.ts b/src/app/shared/services/app-config/app-config.service.ts index ee1c23946..04c6bd3df 100644 --- a/src/app/shared/services/app-config/app-config.service.ts +++ b/src/app/shared/services/app-config/app-config.service.ts @@ -13,14 +13,23 @@ import { Router } from "@angular/router"; providedIn: "root", }) export class AppConfigService extends SyncServiceBase { + /** + * Initial config is generated by merging default app config with deployment-specific overrides + * It is accessed via a read-only getter to avoid update from methods + **/ + private readonly initialConfig: IAppConfig = deepMergeObjects( + getDefaultAppConfig(), + this.deploymentService.config.app_config + ); + /** Signal representation of current appConfig value */ - public appConfig = signal(getDefaultAppConfig()); + public appConfig = signal(this.initialConfig); /** * @deprecated - prefer use of config signal and computed/effect bindings * List of constants provided by data-models combined with deployment-specific overrides and skin-specific overrides **/ - public appConfig$ = new BehaviorSubject(getDefaultAppConfig()); + public appConfig$ = new BehaviorSubject(this.initialConfig); /** Tracking observable of deep changes to app config, exposed in `changes` public method */ private appConfigChanges$: Observable>; @@ -54,28 +63,19 @@ export class AppConfigService extends SyncServiceBase { this.initialise(); } - /** When service initialises load any deployment-specific config overrides */ + /** When service initialises load initial config to trigger any side-effects */ private initialise() { - // When first loading handle side-effects from default config (e.g. initial routing). - // Deployment-specific side-effects will be handled when setting the appConfig - const defaultConfig = getDefaultAppConfig(); - this.handleConfigSideEffects(defaultConfig, defaultConfig); - - // Set app config using deployment overrides - this.setAppConfig(this.deploymentService.config.app_config); + this.setAppConfig(this.initialConfig); } /** * Generate a complete app config by deep-merging app config overrides - * with the default config - * @param overrides - * @returns + * with the initial config */ public setAppConfig(overrides: IAppConfigOverride = {}) { // Ignore case where no overrides provides or overrides already applied if (Object.keys(overrides).length === 0) return; - - const mergedConfig = deepMergeObjects(getDefaultAppConfig(), overrides); + const mergedConfig = deepMergeObjects({} as IAppConfig, this.initialConfig, overrides); this.handleConfigSideEffects(overrides, mergedConfig); this.appConfig.set(mergedConfig); this.appConfig$.next(mergedConfig); diff --git a/src/app/shared/services/deployment/deployment.service.spec.ts b/src/app/shared/services/deployment/deployment.service.spec.ts index 956a9a482..d337640da 100644 --- a/src/app/shared/services/deployment/deployment.service.spec.ts +++ b/src/app/shared/services/deployment/deployment.service.spec.ts @@ -7,6 +7,17 @@ const mockConfig: IDeploymentRuntimeConfig = { name: "test", }; +export class MockDeploymentService implements Partial { + public readonly config: IDeploymentRuntimeConfig; + + constructor(config: Partial) { + this.config = { ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, ...config }; + } + public ready(): boolean { + return true; + } +} + /** * Call standalone tests via: * yarn ng test --include src/app/shared/services/deployment/deployment.service.spec.ts diff --git a/src/app/shared/services/skin/skin.service.spec.ts b/src/app/shared/services/skin/skin.service.spec.ts index 6d908902a..91163628c 100644 --- a/src/app/shared/services/skin/skin.service.spec.ts +++ b/src/app/shared/services/skin/skin.service.spec.ts @@ -48,6 +48,9 @@ const MOCK_APP_CONFIG: Partial = { available: ["MOCK_THEME_1", "MOCK_THEME_2"], defaultThemeName: "MOCK_THEME_1", }, + APP_FOOTER_DEFAULTS: { + templateName: "mock_footer", + }, }; /** @@ -82,6 +85,12 @@ describe("SkinService", () => { expect(service.getActiveSkinName()).toEqual("MOCK_SKIN_1"); }); + it("does not change non-overridden values", () => { + expect(service["appConfigService"].appConfig().APP_FOOTER_DEFAULTS).toEqual({ + templateName: "mock_footer", + }); + }); + it("loads active skin from local storage on init if available", () => { service["localStorageService"].setProtected("APP_SKIN", "MOCK_SKIN_2"); expect(service.getActiveSkinName()).toEqual("MOCK_SKIN_2"); diff --git a/src/app/shared/services/skin/skin.service.ts b/src/app/shared/services/skin/skin.service.ts index df0c5c24b..3cea4f82d 100644 --- a/src/app/shared/services/skin/skin.service.ts +++ b/src/app/shared/services/skin/skin.service.ts @@ -7,7 +7,6 @@ import { AppConfigService } from "../app-config/app-config.service"; import { TemplateService } from "../../components/template/services/template.service"; import { ThemeService } from "src/app/feature/theme/services/theme.service"; import { SyncServiceBase } from "../syncService.base"; -import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root", @@ -21,7 +20,6 @@ export class SkinService extends SyncServiceBase { constructor( private appConfigService: AppConfigService, - private deploymentService: DeploymentService, private localStorageService: LocalStorageService, private templateService: TemplateService, private themeService: ThemeService @@ -83,7 +81,7 @@ export class SkinService extends SyncServiceBase { */ private generateOverrideConfig(skin: IAppSkin) { // Merge onto new object to avoid changing stored revertOverride - const base: RecursivePartial = this.deploymentService.config.app_config || {}; + const base: RecursivePartial = {}; return deepMergeObjects(base, this.revertOverride, skin.appConfig); } From 745867154dce23ad04098b84c4f23cbbcf3101b2 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 18 Sep 2024 15:07:43 -0700 Subject: [PATCH 198/204] feat: add flows-by-type reporter --- .../src/commands/app-data/convert/index.ts | 2 - .../app-data/convert/report/report.ts | 9 ++-- .../report/reporters/flows-by-type.spec.ts | 41 +++++++++++++++++ .../convert/report/reporters/flows-by-type.ts | 34 ++++++++++++++ .../report/reporters/template-summary.ts | 2 +- .../commands/app-data/convert/utils/index.ts | 2 - .../app-data/convert/utils/logging.ts | 46 ------------------- 7 files changed, 82 insertions(+), 54 deletions(-) create mode 100644 packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.spec.ts create mode 100644 packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.ts delete mode 100644 packages/scripts/src/commands/app-data/convert/utils/logging.ts diff --git a/packages/scripts/src/commands/app-data/convert/index.ts b/packages/scripts/src/commands/app-data/convert/index.ts index 939ff2de2..dd9f52b3e 100644 --- a/packages/scripts/src/commands/app-data/convert/index.ts +++ b/packages/scripts/src/commands/app-data/convert/index.ts @@ -11,7 +11,6 @@ import { JsonFileCache } from "./cacheStrategy/jsonFile"; import { generateFolderFlatMap, createChildFileLogger, - logSheetsSummary, getLogs, Logger, getLogFiles, @@ -156,7 +155,6 @@ export class AppDataConverter { /** Create log of total warnings and errors */ private logOutputs(result: IParsedWorkbookData) { this.writeOutputJsons(result); - logSheetsSummary(result); const warnings = getLogs("warn"); if (warnings.length > 0) { const warningLogFile = getLogFiles().warn; diff --git a/packages/scripts/src/commands/app-data/convert/report/report.ts b/packages/scripts/src/commands/app-data/convert/report/report.ts index 492601d1a..6454f70cf 100644 --- a/packages/scripts/src/commands/app-data/convert/report/report.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.ts @@ -1,7 +1,6 @@ /** * TODO - * - Include sheet types/subtypes reporter - * - Include asset references + * - Report referenced assets * - Handle test fail trying to run from app data converter spec * - Add spec to app data converter that it generates reports (just folder) * @@ -24,11 +23,14 @@ import { writeFile, ensureDir, emptyDir } from "fs-extra"; import { generateMarkdownTable } from "./report.utils"; import { logOutput } from "shared"; import chalk from "chalk"; +import { FlowByTypeReport } from "./reporters/flows-by-type"; /** * Create summary reports based on converted app data * Individual reports are created by child reporters, with outputs stored in both * json and markdown formats for easier interpretation + * + * Run on existing data via `yarn workflow sync_sheets --skip-download` **/ export class ReportGenerator { constructor(private deployment: IDeploymentConfigJson) {} @@ -37,7 +39,8 @@ export class ReportGenerator { const { template_actions, template_components } = await new TemplateSummaryReport().process( data ); - const outputReports = { template_actions, template_components }; + const { flows_by_type } = await new FlowByTypeReport().process(data); + const outputReports = { template_actions, template_components, flows_by_type }; await this.writeOutputs(outputReports); } diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.spec.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.spec.ts new file mode 100644 index 000000000..b06712045 --- /dev/null +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.spec.ts @@ -0,0 +1,41 @@ +import { IParsedWorkbookData } from "../../types"; +import { FlowByTypeReport } from "./flows-by-type"; + +const MOCK_WORKBOOK_DATA: IParsedWorkbookData = { + template: [ + { + flow_type: "template", + flow_name: "mock_template_1", + rows: [], + }, + { + flow_type: "data_list", + flow_name: "mock_data_list_1", + rows: [], + }, + { + flow_type: "data_list", + flow_subtype: "mock_subtype", + flow_name: "mock_data_list_2", + rows: [], + }, + { + flow_type: "data_list", + flow_name: "mock_data_list_3", + rows: [], + }, + ], +}; + +/** yarn workspace scripts test -t flows-by-type.spec.ts */ +describe("Flows By Type Report", () => { + it("Enumerates flows by type and subtype", async () => { + const { flows_by_type } = await new FlowByTypeReport().process(MOCK_WORKBOOK_DATA); + console.log(flows_by_type); + expect(flows_by_type.data).toEqual([ + { type: "data_list", subtype: null, total: 2 }, + { type: "data_list", subtype: "mock_subtype", total: 1 }, + { type: "template", subtype: null, total: 1 }, + ]); + }); +}); diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.ts new file mode 100644 index 000000000..31dabff22 --- /dev/null +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.ts @@ -0,0 +1,34 @@ +import { IParsedWorkbookData } from "../../types"; +import { IReport } from "../report.types"; + +/** Generate a list of all flows by type and subtype */ +export class FlowByTypeReport { + public async process(data: IParsedWorkbookData) { + const countBySubtype = {}; + Object.values(data).forEach((flows) => { + flows.forEach((flow) => { + let type = flow.flow_type; + if (flow.flow_subtype) type += `.${flow.flow_subtype}`; + if (!countBySubtype[type]) countBySubtype[type] = 0; + countBySubtype[type]++; + }); + }); + const summary = Object.keys(countBySubtype) + .sort() + .map((key) => { + const [type, subtype] = key.split("."); + return { type, subtype: subtype || null, total: countBySubtype[key] }; + }); + + const reports: Record<"flows_by_type", IReport> = { + flows_by_type: { + type: "table", + title: "Flows By Type", + level: "info", + data: summary, + }, + }; + + return reports; + } +} diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts index 0d63f4419..46cf4cf16 100644 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts @@ -10,7 +10,7 @@ interface ITemplateSummary { } /** - * Generates a list of all components and action types used within templates + * Generate a list of all components and action types used within templates * This will later be used to develop a list of features required * * TODO diff --git a/packages/scripts/src/commands/app-data/convert/utils/index.ts b/packages/scripts/src/commands/app-data/convert/utils/index.ts index e48d790d5..3265a8f1e 100644 --- a/packages/scripts/src/commands/app-data/convert/utils/index.ts +++ b/packages/scripts/src/commands/app-data/convert/utils/index.ts @@ -3,8 +3,6 @@ export * from "./app-data-condition.utils"; export * from "./app-data-override.utils"; export * from "./app-data-string.utils"; -export * from "./logging"; - // re-export some shared utils for ease of import // TODO - should refactor source code to import from shared export { diff --git a/packages/scripts/src/commands/app-data/convert/utils/logging.ts b/packages/scripts/src/commands/app-data/convert/utils/logging.ts deleted file mode 100644 index 91a2485fc..000000000 --- a/packages/scripts/src/commands/app-data/convert/utils/logging.ts +++ /dev/null @@ -1,46 +0,0 @@ -import chalk from "chalk"; - -import { IParsedWorkbookData } from "../types"; - -/** Collate totals of flows by subtype and log */ -export function logSheetsSummary(data: IParsedWorkbookData) { - const countBySubtype = {}; - Object.values(data).forEach((flows) => { - flows.forEach((flow) => { - let type = flow.flow_type; - if (flow.flow_subtype) type += `.${flow.flow_subtype}`; - if (!countBySubtype[type]) countBySubtype[type] = 0; - countBySubtype[type]++; - }); - }); - const logOutput = Object.keys(countBySubtype) - .sort() - .map((key) => { - const [type, subtype] = key.split("."); - return { type, subtype: subtype || null, total: countBySubtype[key] }; - }); - console.log("\nSheet Summary"); - console.table(logOutput); -} - -export function logCacheActionsSummary(actions: any) { - // log summary - const summary = {}; - Object.entries(actions).forEach(([key, value]) => (summary[key] = value.length)); - console.log("\nFile Summary\n", summary); -} - -export function logSheetErrorSummary(warnings: any[], errors: any[]) { - if (warnings.length > 0) { - console.log(chalk.red(warnings.length, "warnings")); - for (const warning of warnings) { - console.log(warning); - } - } - if (errors.length > 0) { - console.log(chalk.red(errors.length, "errors")); - for (const err of errors) { - console.log(err); - } - } -} From 98ac88d59604e833686bcff41b2efc5875309962 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 18 Sep 2024 16:39:50 -0700 Subject: [PATCH 199/204] fix: tests --- packages/scripts/.gitignore | 3 +- .../commands/app-data/convert/convert.spec.ts | 20 ++++++++++--- .../app-data/convert/report/report.ts | 28 ++++++++----------- .../convert/report/report.utils.spec.ts | 20 +++++++++++++ .../app-data/convert/report/report.utils.ts | 8 ++++-- .../report/reporters/flows-by-type.spec.ts | 1 - .../convert/report/reporters/index.ts | 1 - .../report/reporters/template-summary.ts | 6 ++-- packages/scripts/test/helpers/utils.ts | 9 +++--- 9 files changed, 63 insertions(+), 33 deletions(-) delete mode 100644 packages/scripts/src/commands/app-data/convert/report/reporters/index.ts diff --git a/packages/scripts/.gitignore b/packages/scripts/.gitignore index e01aa8289..e0c60ded2 100644 --- a/packages/scripts/.gitignore +++ b/packages/scripts/.gitignore @@ -7,4 +7,5 @@ dist exec test/data/cache -test/data/output \ No newline at end of file +test/data/output +test/data/reports \ No newline at end of file diff --git a/packages/scripts/src/commands/app-data/convert/convert.spec.ts b/packages/scripts/src/commands/app-data/convert/convert.spec.ts index 8532c8e3e..9a2ead811 100644 --- a/packages/scripts/src/commands/app-data/convert/convert.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/convert.spec.ts @@ -7,22 +7,29 @@ import { clearLogs } from "shared"; import { TEST_DATA_PATHS } from "../../../../test/helpers/utils"; import { ActiveDeployment } from "../../deployment/get"; +import { IDeploymentConfigJson } from "data-models"; -const { SHEETS_CACHE_FOLDER, SHEETS_INPUT_FOLDER, SHEETS_OUTPUT_FOLDER } = TEST_DATA_PATHS; +const { SHEETS_CACHE_FOLDER, SHEETS_INPUT_FOLDER, SHEETS_OUTPUT_FOLDER, TEST_DATA_DIR } = + TEST_DATA_PATHS; const paths = { inputFolders: [resolve(SHEETS_INPUT_FOLDER, "sheets")], outputFolder: resolve(SHEETS_OUTPUT_FOLDER, "sheets"), cacheFolder: resolve(SHEETS_CACHE_FOLDER), + reportsFolder: resolve(TEST_DATA_DIR, "reports"), }; -// HACK - avoid loading active deployment -jest.spyOn(ActiveDeployment, "get").mockReturnValue({ +const mockDeployment: Partial = { app_data: { sheets_filter_function: () => true, assets_filter_function: () => true, output_path: paths.outputFolder, }, -} as any); + // HACK - output reports get populated relative to workspace path so use test_data DIR + _workspace_path: TEST_DATA_DIR, +}; + +// HACK - avoid loading active deployment +jest.spyOn(ActiveDeployment, "get").mockReturnValue(mockDeployment as IDeploymentConfigJson); /** yarn workspace scripts test -t convert.spec.ts */ describe("App Data Converter", () => { @@ -59,6 +66,11 @@ describe("App Data Converter", () => { const outputFolders = readdirSync(paths.outputFolder); expect(outputFolders).toEqual(["data_list", "data_pipe", "template"]); }); + it("Generates summary conversion reports", async () => { + await converter.run(); + const reports = readdirSync(paths.reportsFolder); + expect(reports).toEqual(["summary.json", "summary.md"]); + }); it("Supports input from multiple source folders", async () => { const multipleSourceConverter = new AppDataConverter({ ...paths, diff --git a/packages/scripts/src/commands/app-data/convert/report/report.ts b/packages/scripts/src/commands/app-data/convert/report/report.ts index 6454f70cf..cb9647fcf 100644 --- a/packages/scripts/src/commands/app-data/convert/report/report.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.ts @@ -1,29 +1,23 @@ /** - * TODO - * - Report referenced assets - * - Handle test fail trying to run from app data converter spec - * - Add spec to app data converter that it generates reports (just folder) * - * Future - * - possible recommendations/optimisations from reports - * - possibly will require runtime error/warning/prompt - * - handle dynamic - * - QA components/actions that don't exist - * - possibly export list of COMPONENTS_AVAILABLE (or similar... or just use main list lookup) - * - also consider asset report (but would need to ensure dynamic assets included, plus param list + template value) - * - handle implicit components (check imports (?)) + * Potential Reports + * - Referenced assets (incl. param list, template value and data _asset columns) + * - Unused templates + * - Recommended Optimisations + * */ +import chalk from "chalk"; import { IDeploymentConfigJson } from "data-models"; +import { writeFile, ensureDir, emptyDir } from "fs-extra"; +import { resolve, dirname } from "path"; +import { logOutput } from "shared"; + import { IParsedWorkbookData } from "../types"; -import { TemplateSummaryReport } from "./reporters"; import { IReport } from "./report.types"; -import { resolve, dirname } from "path"; -import { writeFile, ensureDir, emptyDir } from "fs-extra"; import { generateMarkdownTable } from "./report.utils"; -import { logOutput } from "shared"; -import chalk from "chalk"; import { FlowByTypeReport } from "./reporters/flows-by-type"; +import { TemplateSummaryReport } from "./reporters/template-summary"; /** * Create summary reports based on converted app data diff --git a/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts b/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts index abcd2ddaf..52918f3fd 100644 --- a/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts @@ -23,4 +23,24 @@ describe("report utils", () => { */ ); }); + + it("generateMarkdownTable with named columns", () => { + const res = generateMarkdownTable( + [ + { key_1: "value_1a", key_2: "value_1b" }, + { key_1: "value_2a", key_2: "value_2b" }, + ], + ["key_1"] + ); + expect(res).toEqual( + `| key_1 |\n| --- |\n| value_1a |\n| value_2a |` + /* When formatted output in form: + + | key_1 | + | --- | + | value_1a | + | value_2a | + */ + ); + }); }); diff --git a/packages/scripts/src/commands/app-data/convert/report/report.utils.ts b/packages/scripts/src/commands/app-data/convert/report/report.utils.ts index 68e6ef55e..6c13339d4 100644 --- a/packages/scripts/src/commands/app-data/convert/report/report.utils.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.utils.ts @@ -18,9 +18,11 @@ export function sortJsonByKey>(json: T) { * | value_1 | value_2 | * ``` */ -export function generateMarkdownTable(data: Record[]) { - const columns = Object.keys(data[0]); - +export function generateMarkdownTable(data: Record[], columns?: string[]) { + // infer columns from data if not provided + if (!columns) { + columns = Object.keys(data[0] || {}); + } const rows: string[][] = []; rows.push(columns); rows.push(columns.map(() => "---")); diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.spec.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.spec.ts index b06712045..c7bbcccac 100644 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.spec.ts @@ -31,7 +31,6 @@ const MOCK_WORKBOOK_DATA: IParsedWorkbookData = { describe("Flows By Type Report", () => { it("Enumerates flows by type and subtype", async () => { const { flows_by_type } = await new FlowByTypeReport().process(MOCK_WORKBOOK_DATA); - console.log(flows_by_type); expect(flows_by_type.data).toEqual([ { type: "data_list", subtype: null, total: 2 }, { type: "data_list", subtype: "mock_subtype", total: 1 }, diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/index.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/index.ts deleted file mode 100644 index 90f267d70..000000000 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./template-summary"; diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts index 46cf4cf16..0bb9aa5a6 100644 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts @@ -13,8 +13,10 @@ interface ITemplateSummary { * Generate a list of all components and action types used within templates * This will later be used to develop a list of features required * - * TODO - * - consider check for template references for unused templates (would need to include data refs) + * TODO (future improvements) + * - Handle implicit components (will require component manifests or checking imports) + * - QA components and actions to flag ones that don't exist + * - Identify dynamic references and track separately (initially just text @data.some_ref, later could be populated) */ export class TemplateSummaryReport { private summary: ITemplateSummary = { actions: {}, components: {} }; diff --git a/packages/scripts/test/helpers/utils.ts b/packages/scripts/test/helpers/utils.ts index 29de4769e..0d131d776 100644 --- a/packages/scripts/test/helpers/utils.ts +++ b/packages/scripts/test/helpers/utils.ts @@ -21,10 +21,11 @@ export function useMockLogger(callOriginal = true) { return { error, warning }; } -const testDataDir = resolve(SCRIPTS_WORKSPACE_PATH, "test", "data"); +const TEST_DATA_DIR = resolve(SCRIPTS_WORKSPACE_PATH, "test", "data"); /** Common paths used for test data */ export const TEST_DATA_PATHS = { - SHEETS_CACHE_FOLDER: resolve(testDataDir, "cache"), - SHEETS_INPUT_FOLDER: resolve(testDataDir, "input"), - SHEETS_OUTPUT_FOLDER: resolve(testDataDir, "output"), + SHEETS_CACHE_FOLDER: resolve(TEST_DATA_DIR, "cache"), + SHEETS_INPUT_FOLDER: resolve(TEST_DATA_DIR, "input"), + SHEETS_OUTPUT_FOLDER: resolve(TEST_DATA_DIR, "output"), + TEST_DATA_DIR, }; From 82cca6e050517c1a0df335aaa8a6ce34f57f6636 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 18 Sep 2024 16:48:06 -0700 Subject: [PATCH 200/204] chore: code tidying --- .../src/commands/app-data/convert/report/report.ts | 9 --------- .../convert/report/reporters/template-summary.ts | 5 ----- 2 files changed, 14 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/report/report.ts b/packages/scripts/src/commands/app-data/convert/report/report.ts index cb9647fcf..b5651393e 100644 --- a/packages/scripts/src/commands/app-data/convert/report/report.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.ts @@ -1,12 +1,3 @@ -/** - * - * Potential Reports - * - Referenced assets (incl. param list, template value and data _asset columns) - * - Unused templates - * - Recommended Optimisations - * - */ - import chalk from "chalk"; import { IDeploymentConfigJson } from "data-models"; import { writeFile, ensureDir, emptyDir } from "fs-extra"; diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts index 0bb9aa5a6..13c31519e 100644 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts @@ -12,11 +12,6 @@ interface ITemplateSummary { /** * Generate a list of all components and action types used within templates * This will later be used to develop a list of features required - * - * TODO (future improvements) - * - Handle implicit components (will require component manifests or checking imports) - * - QA components and actions to flag ones that don't exist - * - Identify dynamic references and track separately (initially just text @data.some_ref, later could be populated) */ export class TemplateSummaryReport { private summary: ITemplateSummary = { actions: {}, components: {} }; From 8e4b098311ff4678de1141f15ae36191ba2b70e6 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 19 Sep 2024 12:15:34 -0700 Subject: [PATCH 201/204] refactor: sortObjectKeys --- .../convert/report/report.utils.spec.ts | 7 +------ .../app-data/convert/report/report.utils.ts | 9 --------- .../report/reporters/template-summary.ts | 4 ++-- packages/shared/src/utils/file-utils.spec.ts | 20 +------------------ packages/shared/src/utils/index.ts | 1 + .../shared/src/utils/object-utils.spec.ts | 17 ++++++++++++++++ packages/shared/src/utils/object-utils.ts | 15 ++++++++++++++ 7 files changed, 37 insertions(+), 36 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts b/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts index 52918f3fd..5a0fac580 100644 --- a/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.utils.spec.ts @@ -1,12 +1,7 @@ -import { generateMarkdownTable, sortJsonByKey } from "./report.utils"; +import { generateMarkdownTable } from "./report.utils"; /** yarn workspace scripts test -t report.utils.spec.ts */ describe("report utils", () => { - it("sortJsonByKey", () => { - const mockJson = { b: "first entry", a: "second entry" }; - const res = sortJsonByKey(mockJson); - expect(Object.keys(res)).toStrictEqual(["a", "b"]); - }); it("generateMarkdownTable", () => { const res = generateMarkdownTable([ { key_1: "value_1a", key_2: "value_1b" }, diff --git a/packages/scripts/src/commands/app-data/convert/report/report.utils.ts b/packages/scripts/src/commands/app-data/convert/report/report.utils.ts index 6c13339d4..5d39c5268 100644 --- a/packages/scripts/src/commands/app-data/convert/report/report.utils.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.utils.ts @@ -1,12 +1,3 @@ -// TODO - move to generic location (possibly object-utils once #2423 merged) -export function sortJsonByKey>(json: T) { - const sorted = {}; - for (const [key, value] of Object.entries(json).sort((a, b) => (a[0] > b[0] ? 1 : -1))) { - sorted[key] = value; - } - return sorted as T; -} - /** * Convert an array to a basic markdown-formatted table * @example diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts index 13c31519e..06ee8ff04 100644 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts @@ -1,7 +1,7 @@ import { FlowTypes } from "data-models"; +import { sortJsonKeys } from "shared/src/utils"; import { IParsedWorkbookData } from "../../types"; -import { sortJsonByKey } from "../report.utils"; import { IReport } from "../report.types"; interface ITemplateSummary { @@ -55,7 +55,7 @@ export class TemplateSummaryReport { /** Convert type records to array for report */ private getReportData(data: Record) { - const sorted = sortJsonByKey(data); + const sorted = sortJsonKeys(data); return Object.entries(sorted).map(([type, { count }]) => ({ type, count })); } } diff --git a/packages/shared/src/utils/file-utils.spec.ts b/packages/shared/src/utils/file-utils.spec.ts index 8d82b62d1..d11ac8fc0 100644 --- a/packages/shared/src/utils/file-utils.spec.ts +++ b/packages/shared/src/utils/file-utils.spec.ts @@ -1,4 +1,4 @@ -import { setNestedProperty, sortJsonKeys } from "./file-utils"; +import { setNestedProperty } from "./file-utils"; describe("setNestedProperty", () => { it("Sets object deep property", () => { @@ -16,21 +16,3 @@ describe("setNestedProperty", () => { expect(res).toEqual({ a: { b: { c: 1 } } }); }); }); - -describe("sortJsonKeys", () => { - it("Sorts nested json by key", () => { - const input = { - b: "foo", - c: null, - a: { - f: 6, - e: 5, - }, - d: [], - }; - const res = sortJsonKeys(input); - expect(Object.keys(res)).toEqual(["a", "b", "c", "d"]); - expect(Object.keys(res.a)).toEqual(["e", "f"]); - expect(Object.values(res.a)).toEqual([5, 6]); - }); -}); diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 4f4eda8c9..3a9542a8f 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -3,5 +3,6 @@ export * from "./cli-utils"; export * from "./delimiters"; export * from "./file-utils"; export * from "./logging.utils"; +export * from "./object-utils"; export * from "./string-utils"; export * from "./typescript-utils"; diff --git a/packages/shared/src/utils/object-utils.spec.ts b/packages/shared/src/utils/object-utils.spec.ts index ff45fa657..648933552 100644 --- a/packages/shared/src/utils/object-utils.spec.ts +++ b/packages/shared/src/utils/object-utils.spec.ts @@ -2,6 +2,7 @@ import { cleanEmptyObject, isEmptyObjectDeep, isObjectLiteral, + sortJsonKeys, toEmptyObject, } from "./object-utils"; @@ -54,4 +55,20 @@ describe("Object Utils", () => { number: 1, }); }); + + it("sortJsonKeys", () => { + const input = { + b: "foo", + c: null, + a: { + f: 6, + e: 5, + }, + d: [], + }; + const res = sortJsonKeys(input); + expect(Object.keys(res)).toEqual(["a", "b", "c", "d"]); + expect(Object.keys(res.a)).toEqual(["e", "f"]); + expect(Object.values(res.a)).toEqual([5, 6]); + }); }); diff --git a/packages/shared/src/utils/object-utils.ts b/packages/shared/src/utils/object-utils.ts index 6add2ddeb..09f574623 100644 --- a/packages/shared/src/utils/object-utils.ts +++ b/packages/shared/src/utils/object-utils.ts @@ -62,3 +62,18 @@ export function cleanEmptyObject(obj: Record) { } return cleaned; } + +/** Order a nested json object literal in alphabetical key order */ +export const sortJsonKeys = >(json: T): T => { + // return non json-type data as-is + if (!isObjectLiteral(json)) { + return json; + } + // recursively sort any nested json by key + return Object.keys(json) + .sort() + .reduce((obj, key) => { + obj[key] = sortJsonKeys(json[key]); + return obj; + }, {}) as T; +}; From 7ca238b7d9a55bc77c739c2ca1863b6282e8616f Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 19 Sep 2024 12:19:54 -0700 Subject: [PATCH 202/204] chore: code tidying --- packages/shared/src/utils/file-utils.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/shared/src/utils/file-utils.ts b/packages/shared/src/utils/file-utils.ts index 3d47250f4..718333baf 100644 --- a/packages/shared/src/utils/file-utils.ts +++ b/packages/shared/src/utils/file-utils.ts @@ -578,18 +578,3 @@ export const cleanupEmptyFolders = (folder: string) => { fs.rmdirSync(folder); } }; - -/** Order a nested json-like object in alphabetical key order */ -export const sortJsonKeys = >(json: T): T => { - // return non json-type data as-is - if (!json || {}.constructor !== json.constructor) { - return json; - } - // recursively sort any nested json by key - return Object.keys(json) - .sort() - .reduce((obj, key) => { - obj[key] = sortJsonKeys(json[key]); - return obj; - }, {}) as T; -}; From c374ccc2bae9a0a37fd3763dcb5d31680cc8ef65 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 19 Sep 2024 12:36:58 -0700 Subject: [PATCH 203/204] chore: improve types --- .../app-data/convert/report/report.types.ts | 2 +- .../convert/report/reporters/flows-by-type.ts | 28 ++++++++----- .../report/reporters/template-summary.ts | 39 +++++++++++-------- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/packages/scripts/src/commands/app-data/convert/report/report.types.ts b/packages/scripts/src/commands/app-data/convert/report/report.types.ts index 680bacc9f..df3a494d2 100644 --- a/packages/scripts/src/commands/app-data/convert/report/report.types.ts +++ b/packages/scripts/src/commands/app-data/convert/report/report.types.ts @@ -8,7 +8,7 @@ interface IReportBase { title: string; } -interface IReportTable extends IReportBase { +export interface IReportTable extends IReportBase { data: Record[]; type: "table"; } diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.ts index 31dabff22..36304256a 100644 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.ts +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/flows-by-type.ts @@ -1,5 +1,15 @@ import { IParsedWorkbookData } from "../../types"; -import { IReport } from "../report.types"; +import { IReportTable } from "../report.types"; + +interface IReportData { + type: string; + subtype: string; + total: number; +} + +interface IFlowByTypeReport extends IReportTable { + data: IReportData[]; +} /** Generate a list of all flows by type and subtype */ export class FlowByTypeReport { @@ -13,22 +23,20 @@ export class FlowByTypeReport { countBySubtype[type]++; }); }); - const summary = Object.keys(countBySubtype) + const summary: IReportData[] = Object.keys(countBySubtype) .sort() .map((key) => { const [type, subtype] = key.split("."); return { type, subtype: subtype || null, total: countBySubtype[key] }; }); - const reports: Record<"flows_by_type", IReport> = { - flows_by_type: { - type: "table", - title: "Flows By Type", - level: "info", - data: summary, - }, + const flows_by_type: IFlowByTypeReport = { + type: "table", + title: "Flows By Type", + level: "info", + data: summary, }; - return reports; + return { flows_by_type }; } } diff --git a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts index 06ee8ff04..c868ebb03 100644 --- a/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts +++ b/packages/scripts/src/commands/app-data/convert/report/reporters/template-summary.ts @@ -2,7 +2,16 @@ import { FlowTypes } from "data-models"; import { sortJsonKeys } from "shared/src/utils"; import { IParsedWorkbookData } from "../../types"; -import { IReport } from "../report.types"; +import { IReportTable } from "../report.types"; + +interface IReportData { + type: string; + count: number; +} + +interface ITemplateSummaryReport extends IReportTable { + data: IReportData[]; +} interface ITemplateSummary { components: Record; @@ -35,26 +44,24 @@ export class TemplateSummaryReport { } } - const reports: Record<"template_components" | "template_actions", IReport> = { - template_components: { - type: "table", - title: "Components", - level: "info", - data: this.getReportData(this.summary.components), - }, - template_actions: { - type: "table", - title: "Actions", - level: "info", - data: this.getReportData(this.summary.actions), - }, + const template_components: ITemplateSummaryReport = { + type: "table", + title: "Components", + level: "info", + data: this.getReportData(this.summary.components), + }; + const template_actions: ITemplateSummaryReport = { + type: "table", + title: "Actions", + level: "info", + data: this.getReportData(this.summary.actions), }; - return reports; + return { template_components, template_actions }; } /** Convert type records to array for report */ - private getReportData(data: Record) { + private getReportData(data: Record): IReportData[] { const sorted = sortJsonKeys(data); return Object.entries(sorted).map(([type, { count }]) => ({ type, count })); } From bf883266ca84d19cf39ed94cada34873b86ee8da Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Fri, 20 Sep 2024 16:11:19 -0700 Subject: [PATCH 204/204] fix: scripts app_config --- packages/scripts/src/commands/app-data/postProcess/assets.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/scripts/src/commands/app-data/postProcess/assets.ts b/packages/scripts/src/commands/app-data/postProcess/assets.ts index ba6528d90..9b8dff42f 100644 --- a/packages/scripts/src/commands/app-data/postProcess/assets.ts +++ b/packages/scripts/src/commands/app-data/postProcess/assets.ts @@ -170,7 +170,8 @@ export class AssetsPostProcessor { const filtered: typeof sourceAssets = {}; const { assets_filter_function } = this.activeDeployment.app_data; const { filter_language_codes } = this.activeDeployment.translations; - const filter_theme_names = this.activeDeployment.app_config.APP_THEMES.available; + // themes are defined in runtime app config which may not be available during scripts + const filter_theme_names = this.activeDeployment.app_config.APP_THEMES?.available || []; // remove contents file from gdrive download delete sourceAssets["_contents.json"];