Skip to content

Commit

Permalink
feat: various experimental features
Browse files Browse the repository at this point in the history
  • Loading branch information
tomcarman committed Jul 4, 2024
1 parent fec1f33 commit 9490247
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 97 deletions.
90 changes: 90 additions & 0 deletions src/common/experimental/FlowMermaid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { FlowNodeWrapper } from './FlowNodeWrapper.js';

export function generateMermaid(nodes: FlowNodeWrapper[]): void {
console.log('\n\nGenerating Mermaid flow...');
console.log(generateMermaidFlowWithStyle(nodes));
}

export function generateMermaidFlowWithStyle(nodes: FlowNodeWrapper[]): string {
let mermaid = '```mermaid\n';
mermaid += `
%%{ init: { 'flowchart': { 'curve': 'stepAfter', 'htmlLabels': false } } }%%
flowchart TD
`;

mermaid += `
classDef orangeNodes fill:#FF5D2D,stroke:#FF5D2D,stroke-width:2px,color:#FFF;
classDef pinkNodes fill:#FF538A,stroke:#FF538A,stroke-width:2px,color:#FFF;
classDef blueNodes fill:#032D60,stroke:#032D60,stroke-width:2px,color:#FFF;
classDef lightBlueNodes fill:#1B96FF,stroke:#1B96FF,stroke-width:2px,color:#FFF;
classDef start fill:#0B827C,stroke:#0B827C,stroke-width:2px,color:#FFF;
classDef terminators fill:#EA001E,stroke:#EA001E,stroke-width:2px,color:#FFF,shape:circle;
`;

nodes.sort((a, b) => {
const aLocation = a.location ? a.location[1] : 0;
const bLocation = b.location ? b.location[1] : 0;
return aLocation - bLocation;
});

const orangeNodeTypes = ['assignments', 'decisions', 'loops', 'transforms', 'collectionProcessors', 'waits'];
const pinkNodeTypes = ['recordCreates', 'recordDeletes', 'recordLookups', 'recordUpdates', 'recordRollbacks'];
const blueNodeTypes = ['actionCalls', 'subflows', 'customErrors', 'apexPluginCalls', 'orchestratedStages', 'steps'];
const lightBlueNodeTypes = ['screens'];

nodes.forEach((node) => {
const nodeTypeText = node.type === 'start' ? `\`**${node.label}**\`` : `\`**${node.label}**\n${node.typeLabel}\``;
node.connectors.forEach((connection) => {
const source = node.name.replace(/ /g, '_');
const target = connection.targetReference.replace(/ /g, '_');
const label = connection.connectionLabel ? `|"${connection.connectionLabel}"|` : '';

if (connection.type === 'Terminator') {
mermaid += ` ${source}["${nodeTypeText}"] --> ${label} ${source}_end(("End"))\n`;
} else {
mermaid += ` ${source}["${nodeTypeText}"] --> ${label} ${target}["\`**${
nodes.find((n) => n.name === connection.targetReference)?.label
}**\n${nodes.find((n) => n.name === connection.targetReference)?.typeLabel}\`"]\n`;
}
});
});

nodes.forEach((node) => {
const nodeName = node.name.replace(/ /g, '_');
const nodeTypeText = node.type === 'start' ? `\`**${node.label}**\`` : `\`**${node.label}**\n${node.typeLabel}\``;
let nodeClass = '';

if (orangeNodeTypes.includes(node.type)) {
nodeClass = 'orangeNodes';
mermaid += ` ${nodeName}${node.type === 'decisions' ? `{"${nodeTypeText}"}` : `("${nodeTypeText}")`}\n`;
} else if (pinkNodeTypes.includes(node.type)) {
nodeClass = 'pinkNodes';
mermaid += ` ${nodeName}("${nodeTypeText}")\n`;
} else if (blueNodeTypes.includes(node.type)) {
nodeClass = 'blueNodes';
mermaid += ` ${nodeName}("${nodeTypeText}")\n`;
} else if (lightBlueNodeTypes.includes(node.type)) {
nodeClass = 'lightBlueNodes';
mermaid += ` ${nodeName}("${nodeTypeText}")\n`;
} else if (node.type === 'start') {
nodeClass = 'start';
mermaid += ` ${nodeName}("${nodeTypeText}")\n`;
}

if (nodeClass) {
mermaid += ` class ${nodeName} ${nodeClass};\n`;
}
});

nodes.forEach((node) => {
node.connectors.forEach((connection) => {
if (connection.type === 'Terminator') {
const nodeName = node.name.replace(/ /g, '_') + '_end';
mermaid += ` class ${nodeName} terminators;\n`;
}
});
});

mermaid += '```';
return mermaid;
}
33 changes: 33 additions & 0 deletions src/common/experimental/FlowNodeTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Flow,
FlowAssignment,
FlowCollectionProcessor,
FlowCustomError,
Expand Down Expand Up @@ -51,3 +52,35 @@ export type AnyFlowNode =
export type AnyFlowElement = [
FlowChoice | FlowConstant | FlowDynamicChoiceSet | FlowFormula | FlowStage | FlowTextTemplate | FlowVariable
];

export const FlowNodeTypes: Array<keyof Flow> = [
'assignments',
'collectionProcessors',
'customErrors',
'recordRollbacks',
'screens',
'subflows',
'transforms',
'actionCalls',
'apexPluginCalls',
'orchestratedStages',
'recordCreates',
'recordDeletes',
'recordLookups',
'recordUpdates',
'waits',
'decisions',
'loops',
'steps',
'start',
];

export const FlowElementTypes: Array<keyof Flow> = [
'choices',
'constants',
'dynamicChoiceSets',
'formulas',
'stages',
'textTemplates',
'variables',
];
10 changes: 7 additions & 3 deletions src/common/experimental/FlowNodeWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FlowConnector } from '@salesforce/types/metadata';
import { arrayify } from '../util.js';
import { arrayify, getFlowComponentTypeLabel } from '../util.js';
import type { AnyFlowNode } from './FlowNodeTypes.js';

type Connector = FlowConnector & {
Expand All @@ -9,16 +9,20 @@ type Connector = FlowConnector & {

export class FlowNodeWrapper {
public type: string;
public typeLabel: string;
public name: string;
public label: string;
public location?: [number, number];
// public data: AnyFlowNode;
public data: AnyFlowNode;
public connectors: Connector[] = [];

public constructor(typeOfNode: string, node: AnyFlowNode) {
this.type = typeOfNode;
this.typeLabel = getFlowComponentTypeLabel(typeOfNode, node);
this.name = typeOfNode === 'start' ? 'Start' : node.name ?? 'Unknown Node Name';
this.label = node.label ?? this.name;
this.location = [node.locationX, node.locationY];
// this.data = node;
this.data = node;
this.buildConnections(node);
this.buildTerminators();
}
Expand Down
55 changes: 1 addition & 54 deletions src/common/experimental/FlowWalker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function walk(flow: FlowWrapper): void {
}
}

type PathEntry = {
export type PathEntry = {
nodeName: string;
nodeType: string;
nodeLocation?: [number, number];
Expand Down Expand Up @@ -104,59 +104,6 @@ export function getPaths(flow: FlowWrapper): PathEntry[][] {
return paths;
}

export function generateMermaid(nodes: FlowNodeWrapper[]): void {
console.log('\n\nGenerating Mermaid flow...');
console.log(generateMermaidFlow(nodes));
console.log('\n\nGenerating State flow...');
console.log(generateStateDiagram(nodes));
}

export function generateMermaidFlow(nodes: FlowNodeWrapper[]): string {
let mermaid = 'flowchart TD\n';

nodes.sort((a, b) => {
const aLocation = a.location ? a.location[1] : 0;
const bLocation = b.location ? b.location[1] : 0;
return aLocation - bLocation;
});

nodes.forEach((node) => {
node.connectors.forEach((connection) => {
const source = node.name.replace(/ /g, '_');
const target = connection.targetReference.replace(/ /g, '_');
const label = connection.connectionLabel ? `|"${connection.connectionLabel}"|` : '';

if (connection.type === 'Terminator') {
mermaid += ` ${source}["${node.name}"] --> ${label} ${source}_end["End"]\n`;
} else {
mermaid += ` ${source}["${node.name}"] --> ${label} ${target}["${
nodes.find((n) => n.name === connection.targetReference)?.name
}"]\n`;
}
});
});

return mermaid;
}

function generateStateDiagram(nodes: FlowNodeWrapper[]): string {
let mermaid = 'stateDiagram-v2\n';

nodes.forEach((node) => {
node.connectors.forEach((connection) => {
const source = node.name.replace(/ /g, '_');
const target = connection.targetReference.replace(/ /g, '_');
const label = connection.connectionLabel ? `: ${connection.connectionLabel}` : '';

if (connection.type !== 'Terminator') {
mermaid += ` ${source} --> ${target}${label}\n`;
}
});
});

return mermaid;
}

function printNode(node: FlowNodeWrapper): void {
console.log('\n\n--- NODE ---');
console.log('Name: ', node.name);
Expand Down
38 changes: 3 additions & 35 deletions src/common/experimental/FlowWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Flow } from '@salesforce/types/metadata';
import { FlowNodeWrapper } from './FlowNodeWrapper.js';
import { FlowElementWrapper } from './FlowElementWrapper.js';
import { AnyFlowNode, AnyFlowElement } from './FlowNodeTypes.js';
import { AnyFlowNode, AnyFlowElement, FlowNodeTypes, FlowElementTypes } from './FlowNodeTypes.js';

export class FlowWrapper {
public flowName: string;
Expand All @@ -11,39 +11,7 @@ export class FlowWrapper {
public constructor(flow: Flow) {
this.flowName = flow.label ?? '';

const flowNodes: Array<keyof Flow> = [
'assignments',
'collectionProcessors',
'customErrors',
'recordRollbacks',
'screens',
'subflows',
'transforms',
'actionCalls',
'apexPluginCalls',
'orchestratedStages',
'recordCreates',
'recordDeletes',
'recordLookups',
'recordUpdates',
'waits',
'decisions',
'loops',
'steps',
'start',
];

const flowElements: Array<keyof Flow> = [
'choices',
'constants',
'dynamicChoiceSets',
'formulas',
'stages',
'textTemplates',
'variables',
];

flowNodes.forEach((property) => {
FlowNodeTypes.forEach((property) => {
if (flow[property] !== undefined) {
if (Array.isArray(flow[property])) {
(flow[property] as AnyFlowNode[]).forEach((node) => {
Expand All @@ -55,7 +23,7 @@ export class FlowWrapper {
}
});

flowElements.forEach((property) => {
FlowElementTypes.forEach((property) => {
if (flow[property] !== undefined) {
if (Array.isArray(flow[property])) {
(flow[property] as unknown as AnyFlowElement[]).forEach((node) => {
Expand Down
76 changes: 76 additions & 0 deletions src/common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ import { Messages } from '@salesforce/core';
import { XMLParser } from 'fast-xml-parser';
import { encode } from 'html-entities';
import indexToPosition from 'index-to-position';
import {
FlowApexPluginCall,
FlowRecordCreate,
FlowCollectionProcessor,
FlowActionCall,
} from '@salesforce/types/metadata';
import { warningsCache } from '../commands/metalint/run.js';
import type { Location } from './types.js';
import type { AnyFlowNode } from './experimental/FlowNodeTypes.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sf-metadata-linter', 'metalint.utils');
Expand Down Expand Up @@ -88,3 +95,72 @@ export function getCustomMetadata(files: string[], types?: string[], excludeName
return true;
});
}

export function getFlowComponentTypeLabel(typeOfNode: string, node: AnyFlowNode): string {
if (flowComponentLabels.has(typeOfNode)) {
return flowComponentLabels.get(typeOfNode)!;
}
switch (typeOfNode) {
case 'recordCreates':
return getRecordCreatesLabel(node as FlowRecordCreate);
case 'collectionProcessors':
return getCollectionProcessorsLabel(node as FlowCollectionProcessor);
case 'actionCalls':
return getActionCallsLabel(node as FlowActionCall);
case 'apexPluginCalls':
return getApexPluginCallsLabel(node as FlowApexPluginCall);
default:
return typeOfNode;
}
}

const flowComponentLabels = new Map<string, string>([
['start', 'Start'],
['assignments', 'Assignment'],
['screens', 'Screen'],
['subflows', 'Subflow'],
['recordDeletes', 'Delete Records'],
['recordLookups', 'Get Records'],
['recordUpdates', 'Update Records'],
['customErrors', 'Custom Error'],
['recordRollbacks', 'Roll Back Records'],
['transforms', 'Transform'],
['decisions', 'Decision'],
['loops', 'Loop'],
['steps', 'Step'],
['orchestratedStages', 'Orchestrated Stage'],
['waits', 'Wait'],
]);

function getRecordCreatesLabel(node: FlowRecordCreate): string {
return node.object?.includes('__e') ? 'Platform Event' : 'Create Records';
}

function getCollectionProcessorsLabel(node: FlowCollectionProcessor): string {
const typeLabels = {
SortCollectionProcessor: 'Collection Sort',
FilterCollectionProcessor: 'Collection Filter',
RecommendationMapCollectionProcessor: 'Collection Recommendation Map',
};
return typeLabels[node.collectionProcessorType] || 'Collection';
}

function getActionCallsLabel(node: FlowActionCall): string {
const actionTypeLabels: { [key: string]: string } = {
apex: `Apex: ${node.actionName}`,
emailSimple: `Send Email: ${node.actionName}`,
emailSObject: `Send Email: ${node.actionName}`,
emailAlert: `Email Alert: ${node.actionName}`,
externalService: `External Service: ${node.actionName}`,
chatterPost: `Chatter Post: ${node.actionName}`,
};
let label;
if (node.actionType !== undefined) {
label = actionTypeLabels[node.actionType];
}
return (label ?? `Action: ${node.actionName}`) || 'Action';
}

function getApexPluginCallsLabel(node: FlowApexPluginCall): string {
return `Apex Plugin Call: ${node.apexClass}`;
}
Loading

0 comments on commit 9490247

Please sign in to comment.