diff --git a/src/common/utils/flowVisualiser/flowParser.ts b/src/common/utils/flowVisualiser/flowParser.ts index be5a416a8..0446e7eb3 100644 --- a/src/common/utils/flowVisualiser/flowParser.ts +++ b/src/common/utils/flowVisualiser/flowParser.ts @@ -8,6 +8,7 @@ import { NODE_CONFIG } from "./renderConfig.js"; +import * as yaml from 'js-yaml'; import { XMLParser } from "fast-xml-parser"; interface FlowMap { @@ -42,7 +43,7 @@ interface FlowObj { * E X P O R T E D *=================================================================*/ -export async function parseFlow(xml: string, renderAs: "plantuml" | "mermaid" = "mermaid", options: any = {}): Promise<{ flowMap: FlowMap, uml: string }> { +export async function parseFlow(xml: string, renderAs: "mermaid" | "plantuml" = "mermaid", options: any = {}): Promise<{ flowMap: FlowMap, uml: string }> { try { const parser = new XMLParser(); const flowObj = parser.parse(xml).Flow; @@ -51,21 +52,19 @@ export async function parseFlow(xml: string, renderAs: "plantuml" | "mermaid" = if (Object.keys(flowMap).length === 0) { throw new Error("no-renderable-content-found"); } - - switch (renderAs) { - case 'mermaid': - return { - flowMap: flowMap, - uml: await generateMermaidContent(flowMap, options) - }; - case 'plantuml': - return { - flowMap: flowMap, - uml: await generatePlantUMLContent(flowMap) - }; - default: - throw new Error("unknown-renderAs-" + renderAs); + if (renderAs === "mermaid") { + return { + flowMap: flowMap, + uml: await generateMermaidContent(flowMap, options) + }; + } + else if (renderAs === 'plantuml') { + return { + flowMap: flowMap, + uml: await generatePlantUMLContent(flowMap) + }; } + throw new Error("unknown-renderAs-" + renderAs); } catch (error) { console.error("salesforce-flow-visualiser", error); throw (error); @@ -73,85 +72,77 @@ export async function parseFlow(xml: string, renderAs: "plantuml" | "mermaid" = } - /*=================================================================== * P R I V A T E *=================================================================*/ async function createFlowMap(flowObj: any): Promise { const flowMap: FlowMap = {}; for (const property in flowObj) { - switch (property) { - case 'constants': - case 'description': - case 'formulas': - case 'label': - case 'processType': - case 'status': - case 'textTemplates': - flowMap[property] = flowObj[property]; - break; - case 'start': - flowMap[property] = flowObj[property]; - flowMap[property].type = property; - flowMap[property].nextNode = flowObj[property].connector?.targetReference; - flowMap[property].scheduledPaths = (!flowMap[property].scheduledPaths) ? [] : (flowMap[property].scheduledPaths.length) ? flowMap[property].scheduledPaths : [flowMap[property].scheduledPaths]; - break; - default: - // If only one entry (e.g one loop) then it will be an object, not an Array, so make it an Array of one - if (!flowObj[property].length) { - flowObj[property] = [flowObj[property]] - } - // Loop through array and create an mapped entry for each - for (const el of flowObj[property]) { - if (el.name) { - let nextNode; - let tmpRules; - switch (property) { - case 'decisions': - nextNode = (el.defaultConnector) ? el.defaultConnector.targetReference : "END"; - tmpRules = (el.rules.length) ? el.rules : [el.rules]; - el.rules2 = tmpRules.map((ruleEl: any) => { - return { - name: ruleEl.name, - label: ruleEl.label, - nextNode: ruleEl.connector, - nextNodeLabel: el.defaultConnectorLabel, - } - }); - break; - case 'loops': - nextNode = (el.noMoreValuesConnector) ? el.noMoreValuesConnector.targetReference : "END"; - break; - default: - if (el.connector) { - nextNode = el.connector.targetReference; + // Common first discriptive elements + if (['constants', 'description', 'environments', 'formulas', 'interviewLabel', 'label', 'processType', 'status', 'textTemplates'].includes(property)) { + flowMap[property] = flowObj[property]; + } + // Start lement + else if (property === 'start') { + flowMap[property] = flowObj[property]; + flowMap[property].type = property; + flowMap[property].nextNode = flowObj[property].connector?.targetReference; + flowMap[property].scheduledPaths = (!flowMap[property].scheduledPaths) ? [] : (flowMap[property].scheduledPaths.length) ? flowMap[property].scheduledPaths : [flowMap[property].scheduledPaths]; + } + else { + // If only one entry (e.g one loop) then it will be an object, not an Array, so make it an Array of one + if (!flowObj[property].length) { + flowObj[property] = [flowObj[property]] + } + // Loop through array and create an mapped entry for each + for (const el of flowObj[property]) { + if (el.name) { + let nextNode; + let tmpRules; + switch (property) { + case 'decisions': + nextNode = (el.defaultConnector) ? el.defaultConnector.targetReference : "END"; + tmpRules = (el.rules.length) ? el.rules : [el.rules]; + el.rules2 = tmpRules.map((ruleEl: any) => { + return { + name: ruleEl.name, + label: ruleEl.label, + nextNode: ruleEl.connector, + nextNodeLabel: el.defaultConnectorLabel, } - break; - } - - if ((NODE_CONFIG)[property]) { - const mappedEl = { - name: el.name, - label: el.label, - type: property, - nextNode: nextNode, - faultPath: el.faultConnector?.targetReference, - nextNodeLabel: el.defaultConnectorLabel, - nextValueConnector: (el.nextValueConnector) ? - el.nextValueConnector.targetReference : null, - rules: el.rules2, - elementSubtype: el.elementSubtype, - actionType: el.actionType + }); + break; + case 'loops': + nextNode = (el.noMoreValuesConnector) ? el.noMoreValuesConnector.targetReference : "END"; + break; + default: + if (el.connector) { + nextNode = el.connector.targetReference; } - flowMap[el.name] = mappedEl; - } else if (property === 'variables') { - flowMap.variables = flowObj[property]; + break; + } + + if ((NODE_CONFIG)[property]) { + const mappedEl = { + name: el.name, + label: el.label, + type: property, + nextNode: nextNode, + faultPath: el.faultConnector?.targetReference, + nextNodeLabel: el.defaultConnectorLabel, + nextValueConnector: (el.nextValueConnector) ? + el.nextValueConnector.targetReference : null, + rules: el.rules2, + elementSubtype: el.elementSubtype, + actionType: el.actionType } + flowMap[el.name] = mappedEl; + } else if (property === 'variables') { + flowMap.variables = flowObj[property]; } } - break; + } } - } return (flowMap); } @@ -173,7 +164,7 @@ function getFlowType(flowMap: FlowMap): string { case "RecordBeforeSave": return "Record triggered flow: Before Save (" + flowMap.start.object + ")"; case "PlatformEvent": - return "PlatformEvent triggered flow (" + flowMap.start.object + ")"; + return "Platform Event triggered flow (" + flowMap.start.object + ")"; default: return "AutoLaunched flow - No trigger"; } @@ -187,9 +178,10 @@ async function generateMermaidContent(flowMap: FlowMap, options: any): Promise { } async function getNodeDefStr(flowMap: FlowMap): Promise { - let nodeDetailMd = "## Nodes Content\n\n" + let nodeDetailMd = "
NODES CONTENT (expand to view)\n\n" let nodeDefStr = "START(( START ))\n"; for (const property in flowMap) { const type = flowMap[property].type; let label: string = ((NODE_CONFIG)[type]) ? (NODE_CONFIG)[type].label : ""; let icon: string = ((NODE_CONFIG)[type]) ? (NODE_CONFIG)[type].mermaidIcon : null; let nodeCopy; - let nodeText; let tooltipClassMermaid; if (type === 'actionCalls') { icon = ((NODE_CONFIG)[type].mermaidIcon[flowMap[property].actionType]) ? @@ -308,15 +299,14 @@ async function getNodeDefStr(flowMap: FlowMap): Promise { delete nodeCopy[nodeKey] } } - nodeText = JSON.stringify(nodeCopy, null, 2); - tooltipClassMermaid = `click ${property} "#${property}" "${nodeText.replace(/["{}]/gm, "").split("\n").join("
")}"`; - nodeDetailMd += `### ${property}\n\n${nodeText.replace(/["{}]/gm, "").split("\n").join("
")}\n\n` + tooltipClassMermaid = `click ${property} "#${property}" "${yaml.dump(nodeCopy).replace(/"/gm, "").split("\n").join("
")}"`; + nodeDetailMd += `### ${property}\n\n${yaml.dump(nodeCopy).replace(/"/gm, "").replace(/^(\s+)/gm, match => ' '.repeat(match.length)).split("\n").join("
\n")}\n\n` nodeDefStr += tooltipClassMermaid + "\n\n" } } return { nodeDefStr: (nodeDefStr + "END(( END ))\n\n"), - nodeDetailMd: nodeDetailMd + nodeDetailMd: nodeDetailMd + "
\n\n" }; } diff --git a/src/common/utils/mermaidUtils.ts b/src/common/utils/mermaidUtils.ts index ce67a6324..3dff6a7eb 100644 --- a/src/common/utils/mermaidUtils.ts +++ b/src/common/utils/mermaidUtils.ts @@ -167,31 +167,31 @@ function buildFinalCompareMarkdown(mixedLines: any[], compareMdLines, isMermaid) // Tables lines if (!isMermaid && status === "removed" && styledLine.startsWith("|")) { - styledLine = "|🟥" + styledLine.split("|").filter(e => e !== "").map((col: string) => `${col}`).join("|") + "|"; + styledLine = "|🟥" + styledLine.split("|").filter(e => e !== "").map((col: string) => `${col}`).join("|") + "|"; } else if (!isMermaid && status === "added" && styledLine.startsWith("|")) { - styledLine = "|🟩" + styledLine.split("|").filter(e => e !== "").map((col: string) => `${col}`).join("|") + "|"; + styledLine = "|🟩" + styledLine.split("|").filter(e => e !== "").map((col: string) => `${col}`).join("|") + "|"; } // Normal lines else if (!isMermaid && status === "removed" && styledLine !== "") { - styledLine = `🟥${styledLine}`; + styledLine = `🟥${styledLine}`; } else if (!isMermaid && status === "added" && styledLine !== "") { - styledLine = `🟩${styledLine}`; + styledLine = `🟩${styledLine}`; } // Boxes lines else if (isMermaid === true && status === "removed" && currentLine.split(":::").length === 2) { styledLine = styledLine + "Removed" if (styledLine.split('"').length === 3) { const splits = styledLine.split('"'); - styledLine = splits[0] + '"🟥' + splits[1] + '"' + splits[2] + styledLine = splits[0] + '"🟥' + splits[1] + '"' + splits[2] } } else if (isMermaid === true && status === "added" && currentLine.split(":::").length === 2) { styledLine = styledLine + "Added" if (styledLine.split('"').length === 3) { const splits = styledLine.split('"'); - styledLine = splits[0] + '"🟩' + splits[1] + '"' + splits[2] + styledLine = splits[0] + '"🟩' + splits[1] + '"' + splits[2] } } else if (isMermaid === true && currentLine.includes(":::")) { @@ -204,7 +204,7 @@ function buildFinalCompareMarkdown(mixedLines: any[], compareMdLines, isMermaid) styledLine = styledLine + "Changed" if (styledLine.split('"').length === 3) { const splits = styledLine.split('"'); - styledLine = splits[0] + '"🟧' + splits[1] + '"' + splits[2] + styledLine = splits[0] + '"🟧' + splits[1] + '"' + splits[2] } } } @@ -214,14 +214,14 @@ function buildFinalCompareMarkdown(mixedLines: any[], compareMdLines, isMermaid) styledLine = styledLine.replace("-->", "-.->") + ":::removedLink" if (styledLine.split("|").length === 3) { const splits = styledLine.split("|"); - styledLine = splits[0] + "|🟥" + splits[1] + "|" + splits[2] + styledLine = splits[0] + "|🟥" + splits[1] + "|" + splits[2] } } else if (isMermaid === true && status === "added" && currentLine.includes('-->')) { styledLine = styledLine.replace("-->", "==>") + ":::addedLink" if (styledLine.split("|").length === 3) { const splits = styledLine.split("|"); - styledLine = splits[0] + "|🟩" + splits[1] + "|" + splits[2] + styledLine = splits[0] + "|🟩" + splits[1] + "|" + splits[2] } } compareMdLines.push(styledLine);