From 7d328fddd3e0cc99c3ad4f55e86f7bf8b7ec0706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Mon, 22 Jul 2024 12:21:40 +0100 Subject: [PATCH 1/3] chore: Update `IndexedNode` to always have a `parentId` property --- src/models/session/logic.ts | 7 +++---- .../session/mocks/complex-flow-breadcrumbs.ts | 4 ++-- .../session/mocks/section-flow-breadcrumbs.ts | 6 +++--- .../session/mocks/simple-flow-breadcrumbs.ts | 4 ++-- src/types/flow.ts | 16 ++++++++++++---- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/models/session/logic.ts b/src/models/session/logic.ts index 4e274902..0a80deb0 100644 --- a/src/models/session/logic.ts +++ b/src/models/session/logic.ts @@ -14,7 +14,7 @@ import { ComponentType } from "../../types"; export function sortFlow(flow: FlowGraph): OrderedFlow { let sectionId: string | undefined; const nodes: IndexedNode[] = []; - const searchNodeEdges = (id: string, parentId?: string) => { + const searchNodeEdges = (id: string, parentId: string) => { // skip already added nodes if (nodes.map((n) => n.id).includes(id)) return; const foundNode = flow[id]; @@ -27,7 +27,7 @@ export function sortFlow(flow: FlowGraph): OrderedFlow { sectionId = foundNode.type == ComponentType.Section ? id : sectionId; nodes.push({ id, - parentId: parentId || null, + parentId, sectionId, type: foundNode.type!, edges: foundNode.edges, @@ -37,10 +37,9 @@ export function sortFlow(flow: FlowGraph): OrderedFlow { searchNodeEdges(childEdgeId, id); }); }; - let parentId: string; + const parentId = "_root"; flow._root.edges.forEach((rootEdgeId) => { searchNodeEdges(rootEdgeId, parentId); - parentId = rootEdgeId; }); return nodes; } diff --git a/src/models/session/mocks/complex-flow-breadcrumbs.ts b/src/models/session/mocks/complex-flow-breadcrumbs.ts index ca43c035..8d763c29 100644 --- a/src/models/session/mocks/complex-flow-breadcrumbs.ts +++ b/src/models/session/mocks/complex-flow-breadcrumbs.ts @@ -228,7 +228,7 @@ export const flow: FlowGraph = { export const orderedFlow: OrderedFlow = [ { id: "Imks7j68BD", - parentId: null, + parentId: "_root", data: { fn: "item", text: "shopping trolley", @@ -497,7 +497,7 @@ export const normalizedFlow: NormalizedFlow = [ allRequired: false, }, type: ComponentType.Checklist, - parentId: null, + parentId: "_root", rootNodeId: "Imks7j68BD", edges: ["EqfqaqZ6CH", "I8DznYCKVg", "pXFKKRG6lE", "7tV1uvR9ng"], }, diff --git a/src/models/session/mocks/section-flow-breadcrumbs.ts b/src/models/session/mocks/section-flow-breadcrumbs.ts index 0a0fb0c5..af968508 100644 --- a/src/models/session/mocks/section-flow-breadcrumbs.ts +++ b/src/models/session/mocks/section-flow-breadcrumbs.ts @@ -80,7 +80,7 @@ export const flow: FlowGraph = { export const orderedFlow: OrderedFlow = [ { id: "firstSection", - parentId: null, + parentId: "_root", sectionId: "firstSection", data: { title: "First Section", @@ -169,7 +169,7 @@ export const normalizedFlow: NormalizedFlow = [ id: "firstSection", sectionId: "firstSection", rootNodeId: "firstSection", - parentId: null, + parentId: "_root", data: { title: "First Section", }, @@ -265,7 +265,7 @@ export const sectionNodes: NormalizedFlow = [ id: "firstSection", sectionId: "firstSection", rootNodeId: "firstSection", - parentId: null, + parentId: "_root", data: { title: "First Section", }, diff --git a/src/models/session/mocks/simple-flow-breadcrumbs.ts b/src/models/session/mocks/simple-flow-breadcrumbs.ts index f51df02f..5bf88bca 100644 --- a/src/models/session/mocks/simple-flow-breadcrumbs.ts +++ b/src/models/session/mocks/simple-flow-breadcrumbs.ts @@ -55,7 +55,7 @@ export const flow: FlowGraph = { export const orderedFlow: OrderedFlow = [ { id: "firstQuestion", - parentId: null, + parentId: "_root", data: { text: "First Question", }, @@ -109,7 +109,7 @@ export const orderedFlow: OrderedFlow = [ export const normalizedFlow: NormalizedFlow = [ { id: "firstQuestion", - parentId: null, + parentId: "_root", rootNodeId: "firstQuestion", data: { text: "First Question", diff --git a/src/types/flow.ts b/src/types/flow.ts index 16d6f9fa..d4a26e61 100644 --- a/src/types/flow.ts +++ b/src/types/flow.ts @@ -14,20 +14,28 @@ export interface Node { data?: Record; } +type RootNode = { + edges: Edges; +}; + export type FlowGraph = { - _root: { - edges: Edges; - }; + _root: RootNode; [key: string]: Node; }; export type IndexedNode = Node & { id: string; type: ComponentType; - parentId: string | null; // null if it is the first node + parentId: string; sectionId?: string; }; +export type IndexedFlowGraph = { + _root: RootNode; +} & { + [key: string]: IndexedNode; +}; + export type OrderedFlow = Array; export interface NormalizedNode extends IndexedNode { From 2ebabb34714d4e00270c3531c9960b46d8448656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 24 Jul 2024 07:44:03 +0100 Subject: [PATCH 2/3] wip --- src/models/session/logic.test.ts | 4 ++-- src/models/session/logic.ts | 4 ++-- .../session/mocks/complex-flow-breadcrumbs.ts | 8 +++---- .../session/mocks/section-flow-breadcrumbs.ts | 24 +++++++++---------- .../session/mocks/simple-flow-breadcrumbs.ts | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/models/session/logic.test.ts b/src/models/session/logic.test.ts index 6d611810..50b0c26d 100644 --- a/src/models/session/logic.test.ts +++ b/src/models/session/logic.test.ts @@ -91,7 +91,7 @@ describe("findNextNodeOfType", () => { }); expect(nextNode).toEqual({ id: "secondQuestion", - parentId: "firstQuestion", + parentId: "_root", type: ComponentType.Question, edges: ["secondAnswer"], data: { @@ -130,7 +130,7 @@ describe("findNextNodeOfType", () => { }); expect(nextSectionNode).toEqual({ id: "Section3", - parentId: "EndOfSection2Notice", + parentId: "_root", sectionId: "Section3", data: { title: "Section Three" }, type: ComponentType.Section, diff --git a/src/models/session/logic.ts b/src/models/session/logic.ts index 0a80deb0..fdda57c8 100644 --- a/src/models/session/logic.ts +++ b/src/models/session/logic.ts @@ -28,9 +28,9 @@ export function sortFlow(flow: FlowGraph): OrderedFlow { nodes.push({ id, parentId, - sectionId, + ...(sectionId && { sectionId }), type: foundNode.type!, - edges: foundNode.edges, + ...(foundNode.edges && { edges: foundNode.edges }), data: foundNode.data, }); foundNode.edges?.forEach((childEdgeId) => { diff --git a/src/models/session/mocks/complex-flow-breadcrumbs.ts b/src/models/session/mocks/complex-flow-breadcrumbs.ts index 8d763c29..ba07fdac 100644 --- a/src/models/session/mocks/complex-flow-breadcrumbs.ts +++ b/src/models/session/mocks/complex-flow-breadcrumbs.ts @@ -275,7 +275,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "HV0gV8DOil", - parentId: "Imks7j68BD", + parentId: "_root", data: { fn: "item", text: "shopping trolley (should be skipped)", @@ -322,7 +322,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "2PT6bTPTqj", - parentId: "HV0gV8DOil", + parentId: "_root", data: { fn: "item", text: "contains", @@ -411,7 +411,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "3H2bGdzpIN", - parentId: "2PT6bTPTqj", + parentId: "_root", data: { fn: "item", text: "Does the basket contain apples?", @@ -438,7 +438,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "AFX3QwbOCd", - parentId: "3H2bGdzpIN", + parentId: "_root", data: { fn: "item", text: "Which does the basket contain?", diff --git a/src/models/session/mocks/section-flow-breadcrumbs.ts b/src/models/session/mocks/section-flow-breadcrumbs.ts index af968508..c1afe1bf 100644 --- a/src/models/session/mocks/section-flow-breadcrumbs.ts +++ b/src/models/session/mocks/section-flow-breadcrumbs.ts @@ -89,7 +89,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "firstQuestion", - parentId: "firstSection", + parentId: "_root", sectionId: "firstSection", data: { text: "First Question", @@ -108,7 +108,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "secondSection", - parentId: "firstQuestion", + parentId: "_root", sectionId: "secondSection", data: { title: "Second Section", @@ -117,7 +117,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "secondQuestion", - parentId: "secondSection", + parentId: "_root", sectionId: "secondSection", data: { text: "Second Question", @@ -136,7 +136,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "thirdSection", - parentId: "secondQuestion", + parentId: "_root", sectionId: "thirdSection", data: { title: "Third Section", @@ -145,7 +145,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "thirdQuestion", - parentId: "thirdSection", + parentId: "_root", sectionId: "thirdSection", data: { text: "Third Question", @@ -179,7 +179,7 @@ export const normalizedFlow: NormalizedFlow = [ id: "firstQuestion", sectionId: "firstSection", rootNodeId: "firstQuestion", - parentId: "firstSection", + parentId: "_root", data: { text: "First Question", }, @@ -200,7 +200,7 @@ export const normalizedFlow: NormalizedFlow = [ id: "secondSection", sectionId: "secondSection", rootNodeId: "secondSection", - parentId: "firstQuestion", + parentId: "_root", data: { title: "Second Section", }, @@ -209,7 +209,7 @@ export const normalizedFlow: NormalizedFlow = [ { id: "secondQuestion", sectionId: "secondSection", - rootNodeId: "secondQuestion", + rootNodeId: "_root", parentId: "secondSection", data: { text: "Second Question", @@ -221,7 +221,7 @@ export const normalizedFlow: NormalizedFlow = [ id: "secondAnswer", sectionId: "secondSection", rootNodeId: "secondQuestion", - parentId: "secondQuestion", + parentId: "_root", data: { text: "Answer 2", }, @@ -231,7 +231,7 @@ export const normalizedFlow: NormalizedFlow = [ id: "thirdSection", sectionId: "thirdSection", rootNodeId: "thirdSection", - parentId: "secondQuestion", + parentId: "_root", data: { title: "Third Section", }, @@ -241,7 +241,7 @@ export const normalizedFlow: NormalizedFlow = [ id: "thirdQuestion", sectionId: "thirdSection", rootNodeId: "thirdQuestion", - parentId: "thirdSection", + parentId: "secondQuestion", data: { text: "Third Question", }, @@ -285,7 +285,7 @@ export const sectionNodes: NormalizedFlow = [ id: "thirdSection", sectionId: "thirdSection", rootNodeId: "thirdSection", - parentId: "secondQuestion", + parentId: "_root", data: { title: "Third Section", }, diff --git a/src/models/session/mocks/simple-flow-breadcrumbs.ts b/src/models/session/mocks/simple-flow-breadcrumbs.ts index 5bf88bca..2d87c195 100644 --- a/src/models/session/mocks/simple-flow-breadcrumbs.ts +++ b/src/models/session/mocks/simple-flow-breadcrumbs.ts @@ -72,7 +72,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "secondQuestion", - parentId: "firstQuestion", + parentId: "_root", data: { text: "Second Question", }, @@ -89,7 +89,7 @@ export const orderedFlow: OrderedFlow = [ }, { id: "thirdQuestion", - parentId: "secondQuestion", + parentId: "_root", data: { text: "Third Question", }, From 339cb5006ab56f15d3c37d4ce01a6f26f6756759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 24 Jul 2024 17:44:25 +0100 Subject: [PATCH 3/3] chore: Remove redundant code --- src/models/session/logic.test.ts | 65 +----------- src/models/session/logic.ts | 40 +------- src/models/session/mocks/branching-flow.ts | 110 --------------------- 3 files changed, 4 insertions(+), 211 deletions(-) delete mode 100644 src/models/session/mocks/branching-flow.ts diff --git a/src/models/session/logic.test.ts b/src/models/session/logic.test.ts index 50b0c26d..b8d6d06b 100644 --- a/src/models/session/logic.test.ts +++ b/src/models/session/logic.test.ts @@ -1,7 +1,5 @@ import type { OrderedBreadcrumbs, OrderedFlow } from "../../types"; -import { ComponentType } from "../../types"; -import { findNextNodeOfType, sortBreadcrumbs, sortFlow } from "./logic"; -import * as branching from "./mocks/branching-flow"; +import { sortBreadcrumbs, sortFlow } from "./logic"; import * as complex from "./mocks/complex-flow-breadcrumbs"; import * as large from "./mocks/large-real-life-flow"; import * as sectioned from "./mocks/section-flow-breadcrumbs"; @@ -77,67 +75,6 @@ describe("sortBreadcrumbs", () => { }); }); -describe("findNextNodeOfType", () => { - test("it finds the next node in a simple flow", () => { - const nextNode = findNextNodeOfType({ - flow: simple.flow, - breadcrumbs: { - firstQuestion: { - auto: false, - answers: ["firstAnswer"], - }, - }, - componentType: ComponentType.Question, - }); - expect(nextNode).toEqual({ - id: "secondQuestion", - parentId: "_root", - type: ComponentType.Question, - edges: ["secondAnswer"], - data: { - text: "Second Question", - }, - }); - }); - - test("it finds the next node in a branching flow", () => { - const multiselectChecklistBreadcrumbs = { - Section1: { auto: true }, - Question1: { auto: false, answers: ["Question1AnswerA"] }, - Section2: { auto: true }, - Question2: { auto: false, answers: ["Question2AnswerB"] }, - Checklist1: { - auto: false, - answers: ["ChecklistOptionB", "ChecklistOptionC"], - }, - }; - const nextNoticeNode = findNextNodeOfType({ - flow: branching.flow, - breadcrumbs: multiselectChecklistBreadcrumbs, - componentType: ComponentType.Notice, - }); - expect(nextNoticeNode).toEqual({ - id: "NoticeB", - parentId: "ChecklistOptionB", // this isn't really the parent but the previous node - sectionId: "Section2", - data: { color: "#EFEFEF", title: "Reached B", resetButton: false }, - type: ComponentType.Notice, - }); - const nextSectionNode = findNextNodeOfType({ - flow: branching.flow, - breadcrumbs: multiselectChecklistBreadcrumbs, - componentType: ComponentType.Section, - }); - expect(nextSectionNode).toEqual({ - id: "Section3", - parentId: "_root", - sectionId: "Section3", - data: { title: "Section Three" }, - type: ComponentType.Section, - }); - }); -}); - function expectReasonableExecutionTime(fn: () => T, timeout: number): T { const testStartTime = new Date().getTime(); const out = fn(); diff --git a/src/models/session/logic.ts b/src/models/session/logic.ts index fdda57c8..5bcf4dcd 100644 --- a/src/models/session/logic.ts +++ b/src/models/session/logic.ts @@ -37,10 +37,11 @@ export function sortFlow(flow: FlowGraph): OrderedFlow { searchNodeEdges(childEdgeId, id); }); }; - const parentId = "_root"; + flow._root.edges.forEach((rootEdgeId) => { - searchNodeEdges(rootEdgeId, parentId); + searchNodeEdges(rootEdgeId, "_root"); }); + return nodes; } @@ -106,41 +107,6 @@ export function sortBreadcrumbs( return orderedBreadcrumbs; } -export function findNextNodeOfType({ - flow, - breadcrumbs, - componentType, -}: { - flow: FlowGraph; - breadcrumbs: Breadcrumbs; - componentType: ComponentType; -}): IndexedNode | undefined { - const orderedBreadcrumbs = sortBreadcrumbs(flow, breadcrumbs); - const lastCrumb = orderedBreadcrumbs.at(-1)!; - const sortedFlow = sortFlow(flow); - const sortedFlowLastNodeIndex = sortedFlow.findIndex( - (n) => n.id == lastCrumb.id, - ); - const truncatedFlow = sortedFlow.slice(sortedFlowLastNodeIndex); - - for (const node of truncatedFlow) { - const parentNodeIsLastCrumb = node.parentId === lastCrumb.id; - if (parentNodeIsLastCrumb && node.type === componentType) { - return node; - } - const parentNodeIsCrumbAnswer = - node.parentId && - lastCrumb.answers && - lastCrumb.answers.includes(node.parentId!); - if (parentNodeIsCrumbAnswer && node.type === componentType) { - return node; - } - } - // when the above fails, fallback to a naïve scan of matching upcoming nodes - // FIXME: this approach does not respect flow branching logic - return truncatedFlow.find((n) => n.type === componentType); -} - const isSectionNode = (nodeOrCrumb: Node | Crumb): nodeOrCrumb is Node => "type" in nodeOrCrumb && nodeOrCrumb.type === ComponentType.Section; diff --git a/src/models/session/mocks/branching-flow.ts b/src/models/session/mocks/branching-flow.ts deleted file mode 100644 index a94dbb2e..00000000 --- a/src/models/session/mocks/branching-flow.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { FlowGraph } from "../../../types"; -import { ComponentType } from "../../../types"; - -export const flow: FlowGraph = { - _root: { - edges: [ - "Section1", - "Question1", - "Section2", - "Question2", - "EndOfSection2Notice", - "Section3", - "PreSendContent", - "Send", - "Confirmation", - ], - }, - ChecklistOptionC: { - data: { text: "C" }, - type: ComponentType.Answer, - edges: ["NoticeC"], - }, - Question1: { - data: { text: "Question 1" }, - type: ComponentType.Question, - edges: ["Question1AnswerA", "Question1AnswerB", "Question1AnswerC"], - }, - Section2: { data: { title: "Section Two" }, type: ComponentType.Section }, - Question1AnswerB: { data: { text: "B" }, type: ComponentType.Answer }, - NoticeC: { - data: { color: "#EFEFEF", title: "Reached C", resetButton: false }, - type: ComponentType.Notice, - }, - PreSendContent: { - data: { content: "

About to send

" }, - type: ComponentType.Content, - }, - Question1AnswerC: { data: { text: "C" }, type: ComponentType.Answer }, - NoticeB: { - data: { color: "#EFEFEF", title: "Reached B", resetButton: false }, - type: ComponentType.Notice, - }, - Send: { - data: { title: "Send", destinations: ["email"] }, - type: ComponentType.Send, - }, - NoticeD: { - data: { color: "#EFEFEF", title: "Reached D", resetButton: false }, - type: ComponentType.Notice, - }, - NoticeA: { - data: { color: "#EFEFEF", title: "Reached A", resetButton: false }, - type: ComponentType.Notice, - }, - ChecklistOptionB: { - data: { text: "B" }, - type: ComponentType.Answer, - edges: ["NoticeB"], - }, - Checklist1: { - data: { text: "Multi-select", allRequired: false }, - type: ComponentType.Checklist, - edges: [ - "ChecklistOptionA", - "ChecklistOptionB", - "ChecklistOptionC", - "ChecklistOptionD", - ], - }, - Question2AnswerA: { data: { text: "A" }, type: ComponentType.Answer }, - ChecklistOptionD: { - data: { text: "D" }, - type: ComponentType.Answer, - edges: ["NoticeD"], - }, - Section1: { data: { title: "Section One" }, type: ComponentType.Section }, - Question2: { - data: { text: "Question 2" }, - type: ComponentType.Question, - edges: ["Question2AnswerA", "Question2AnswerB"], - }, - Question1AnswerA: { data: { text: "A" }, type: ComponentType.Answer }, - Question2AnswerB: { - data: { text: "B" }, - type: ComponentType.Answer, - edges: ["Checklist1"], - }, - ChecklistOptionA: { - data: { text: "A" }, - type: ComponentType.Answer, - edges: ["NoticeA"], - }, - Section3: { data: { title: "Section Three" }, type: ComponentType.Section }, - Confirmation: { - data: { - heading: "Application sent", - moreInfo: "

You will be contacted

", - contactInfo: "You can contact us at planning@abc.gov.uk", - }, - type: ComponentType.Confirmation, - }, - EndOfSection2Notice: { - data: { - color: "#EFEFEF", - title: "Reached the end of section two", - resetButton: false, - }, - type: ComponentType.Notice, - }, -};