├── .gitignore ├── images ├── icon.png └── readme-image.jpg ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── .vscodeignore ├── tsconfig.json ├── src ├── utilities │ ├── getUri.ts │ └── getNonce.ts ├── test │ ├── suite │ │ ├── extension.test.ts │ │ └── index.ts │ └── runTest.ts ├── extension.ts ├── webview │ └── main.ts └── panels │ └── FlowPanel.ts ├── test ├── suite │ ├── extension.test.js │ └── index.js └── runTest.js ├── .eslintrc.json ├── .github └── workflows │ └── continuousIntegration.yml ├── LICENSE.txt ├── CHANGELOG.md ├── README.md ├── esbuild.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | out 5 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhalfpenny/sfflowvisualiser-vscode/HEAD/images/icon.png -------------------------------------------------------------------------------- /images/readme-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddhalfpenny/sfflowvisualiser-vscode/HEAD/images/readme-image.jpg -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | src/** 5 | .gitignore 6 | **/jsconfig.json 7 | **/*.map 8 | **/.eslintrc.json 9 | **/tsconfig.json 10 | **/*.ts 11 | **/*.d.ts 12 | .git/** 13 | package.lock.json -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2022", 5 | "outDir": "out", 6 | "lib": ["ES2022", "DOM"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utilities/getUri.ts: -------------------------------------------------------------------------------- 1 | // file: src/utilities/getUri.ts 2 | 3 | import { Uri, Webview } from "vscode"; 4 | 5 | export function getUri( 6 | webview: Webview, 7 | extensionUri: Uri, 8 | pathList: string[], 9 | ) { 10 | return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); 11 | } 12 | -------------------------------------------------------------------------------- /src/utilities/getNonce.ts: -------------------------------------------------------------------------------- 1 | export function getNonce() { 2 | let text = ""; 3 | const possible = 4 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 5 | for (let i = 0; i < 32; i++) { 6 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 7 | } 8 | return text; 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$esbuild-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/suite/extension.test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | const vscode = require("vscode"); 6 | // const myExtension = require('../extension'); 7 | 8 | suite("Extension Test Suite", () => { 9 | vscode.window.showInformationMessage("Start all tests."); 10 | 11 | test("Sample test", () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from "vscode"; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite("Extension Test Suite", () => { 9 | vscode.window.showInformationMessage("Start all tests."); 10 | 11 | test("Sample test", () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/naming-convention": [ 11 | "warn", 12 | { 13 | "selector": "import", 14 | "format": ["camelCase", "PascalCase"] 15 | } 16 | ], 17 | "@typescript-eslint/semi": "warn", 18 | "curly": "warn", 19 | "eqeqeq": "warn", 20 | "no-throw-literal": "warn", 21 | "semi": "off" 22 | }, 23 | "ignorePatterns": ["out", "dist", "**/*.d.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/continuousIntegration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | ref: ${{ github.head_ref }} 19 | persist-credentials: false 20 | 21 | - name: Ensure code is formatted using Prettier 22 | uses: creyD/prettier_action@v4.3 23 | with: 24 | dry: True 25 | github_token: ${{ secrets.PERSONAL_GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2023 Todd Halfpenny 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error("Failed to run tests", err); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /test/runTest.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const { runTests } = require("@vscode/test-electron"); 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../"); 10 | 11 | // The path to the extension test script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error("Failed to run tests", err); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "sfflowvisualiser" extension will be documented in this file. 4 | 5 | ### 0.2.1 6 | 7 | - Moved to a webview to render the information and flow 8 | - Output includes Constants, Variable and TextTemplates 9 | - Ouput includes Start conditions 10 | - Flow can be zoomed in/put 11 | - Flow can be saved as a .png file 12 | - Removed dependancy on [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) extension 13 | 14 | ### 0.1.0 15 | 16 | #### Bug fixes 17 | 18 | - Support for special mermaid chars in node labels (e.g braces). Done via update to a dep lib 19 | 20 | ## 0.0.2 21 | 22 | #### Bug fixes 23 | 24 | - Fixed incorrect dep 25 | 26 | ## 0.0.1 27 | 28 | - Initial release 29 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import Mocha from "mocha"; 3 | import { glob } from "glob"; 4 | 5 | export async function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | const files = await glob("**/**.test.js", { cwd: testsRoot }); 14 | 15 | // Add files to the test suite 16 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 17 | 18 | try { 19 | return new Promise((c, e) => { 20 | // Run the mocha test 21 | mocha.run((failures) => { 22 | if (failures > 0) { 23 | e(new Error(`${failures} tests failed.`)); 24 | } else { 25 | c(); 26 | } 27 | }); 28 | }); 29 | } catch (err) { 30 | console.error(err); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/suite/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const Mocha = require("mocha"); 3 | const glob = require("glob"); 4 | 5 | async function run() { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | const files = await glob("**/**.test.js", { cwd: testsRoot }); 14 | 15 | // Add files to the test suite 16 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 17 | 18 | try { 19 | return new Promise((c, e) => { 20 | // Run the mocha test 21 | mocha.run((failures) => { 22 | if (failures > 0) { 23 | e(new Error(`${failures} tests failed.`)); 24 | } else { 25 | c(); 26 | } 27 | }); 28 | }); 29 | } catch (err) { 30 | console.error(err); 31 | } 32 | } 33 | 34 | module.exports = { 35 | run, 36 | }; 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "args": [ 21 | "--extensionDevelopmentPath=${workspaceFolder}", 22 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 23 | ], 24 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 25 | "preLaunchTask": "${defaultBuildTask}" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { FlowPanel } from "./panels/FlowPanel"; 3 | import { parseFlow } from "salesforce-flow-visualiser"; 4 | 5 | export function activate(context: vscode.ExtensionContext) { 6 | let renderWebView = vscode.commands.registerCommand( 7 | "sfflowvisualiser.generateWebview", 8 | () => { 9 | generateWebView(context.extensionUri, "mermaid"); 10 | }, 11 | ); 12 | 13 | context.subscriptions.push(renderWebView); 14 | } 15 | 16 | async function getParsedXML(mode: any, options: any) { 17 | return new Promise(async (resolve, reject) => { 18 | const xmlData = vscode.window.activeTextEditor?.document.getText(); 19 | try { 20 | const res = await parseFlow(xmlData as string, mode, options); 21 | resolve(res); 22 | } catch (error) { 23 | reject(error); 24 | } 25 | }); 26 | } 27 | 28 | async function generateWebView(extensionUri: vscode.Uri, mode: any) { 29 | try { 30 | const parsedXmlRes = await getParsedXML(mode, { wrapInMarkdown: false }); 31 | FlowPanel.render(extensionUri, parsedXmlRes); 32 | } catch (error) { 33 | console.error(error); 34 | } 35 | } 36 | 37 | export function deactivate() {} 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salesforce Flow Visualiser 2 | 3 | A VS Code extension to create a visual represenation of Salesforce Flow `.flow-meta.xml` files. 4 | 5 | ## Features 6 | 7 | It provides an easy to use representation of Salesforce Flow files. As well as a graphical view of the flow itself it also ouputs; 8 | 9 | - Start Condiitions 10 | - Resources: 11 | - Constants 12 | - Text Templates 13 | - Variables 14 | 15 | You can zoom in/out on the flow itself, and can also save the image to a .png file. 16 | 17 | ## Usage 18 | 19 | 1. Have a Salesforce Flow `.flow-meta.xml` file open. 20 | 1. Open the VS Code _Command palette_ with **SHIFT+CTRL+P**. 21 | 1. Run the _Flow Visualiser: Render_ command. 22 | 23 | ## Requirements 24 | 25 | The v0.2 extension does not require any other extensions to be installed. 26 | 27 | The v0.1 extension relied on the [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) extension also being installed. 28 | 29 | ## Extension Settings 30 | 31 | There aren't currently any settings for this extension. 32 | 33 | ## Known Issues 34 | 35 | - HTML Special Characters (e.g. `"`) are being represented as their decoded versions( e.g `"`). I believe this is down to the `fast-xml-parser` lib that's being used downstream - it's on the list to look at. 36 | 37 | ## Release Notes 38 | 39 | Users appreciate release notes as you update your extension. 40 | 41 | ### 0.2.2 42 | 43 | - Added missing icon 44 | 45 | ### 0.2.1 46 | 47 | - Moved to a webview to render the information and flow 48 | - Output includes Constants, Variable and TextTemplates 49 | - Ouput includes Start conditions 50 | - Flow can be zoomed in/put 51 | - Flow can be saved as a .png file 52 | - Removed dependancy on [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) extension 53 | 54 | ### 0.1.0 55 | 56 | - Support for special mermaid chars in node labels (e.g braces) 57 | 58 | ### 0.0.2 59 | 60 | - Fixed incorrect dep 61 | 62 | ## 0.0.1 63 | 64 | - Initial pre-release version 65 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | // file: esbuild.js 2 | 3 | const { build } = require("esbuild"); 4 | 5 | const baseConfig = { 6 | bundle: true, 7 | minify: process.env.NODE_ENV === "production", 8 | sourcemap: process.env.NODE_ENV !== "production", 9 | }; 10 | 11 | const extensionConfig = { 12 | ...baseConfig, 13 | platform: "node", 14 | mainFields: ["module", "main"], 15 | format: "cjs", 16 | entryPoints: ["./src/extension.ts"], 17 | outfile: "./out/extension.js", 18 | external: ["vscode"], 19 | }; 20 | 21 | (async () => { 22 | try { 23 | await build(extensionConfig); 24 | console.log("build complete"); 25 | } catch (err) { 26 | process.stderr.write(err.stderr); 27 | process.exit(1); 28 | } 29 | })(); 30 | 31 | const watchConfig = { 32 | watch: { 33 | onRebuild(error, result) { 34 | console.log("[watch] build started"); 35 | if (error) { 36 | error.errors.forEach((error) => 37 | console.error( 38 | `> ${error.location.file}:${error.location.line}:${error.location.column}: error: ${error.text}`, 39 | ), 40 | ); 41 | } else { 42 | console.log("[watch] build finished"); 43 | } 44 | }, 45 | }, 46 | }; 47 | 48 | const webviewConfig = { 49 | ...baseConfig, 50 | target: "es2020", 51 | format: "esm", 52 | entryPoints: ["./src/webview/main.ts"], 53 | outfile: "./out/webview.js", 54 | }; 55 | 56 | (async () => { 57 | const args = process.argv.slice(2); 58 | try { 59 | if (args.includes("--watch")) { 60 | // Build and watch extension and webview code 61 | console.log("[watch] build started"); 62 | await build({ 63 | ...extensionConfig, 64 | ...watchConfig, 65 | }); 66 | await build({ 67 | ...webviewConfig, 68 | ...watchConfig, 69 | }); 70 | console.log("[watch] build finished"); 71 | } else { 72 | // Build extension and webview code 73 | await build(extensionConfig); 74 | await build(webviewConfig); 75 | console.log("build complete"); 76 | } 77 | } catch (err) { 78 | process.stderr.write(err.stderr); 79 | process.exit(1); 80 | } 81 | })(); 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sfflowvisualiser", 3 | "displayName": "Salesforce Flow Visualiser", 4 | "description": "Salesforce Flow Visualiser VS Code extension", 5 | "icon": "images/icon.png", 6 | "version": "0.2.2", 7 | "engines": { 8 | "vscode": "^1.83.0" 9 | }, 10 | "categories": [ 11 | "Visualization", 12 | "Formatters" 13 | ], 14 | "keywords": [ 15 | "Salesforce", 16 | "Flow", 17 | "Visualiser", 18 | "Visualizer", 19 | "Mermaid", 20 | "Diagram" 21 | ], 22 | "activationEvents": [], 23 | "main": "./out/extension.js", 24 | "contributes": { 25 | "commands": [ 26 | { 27 | "command": "sfflowvisualiser.generateWebview", 28 | "title": "Flow visualiser: Render" 29 | } 30 | ], 31 | "menus": { 32 | "commandPalette": [ 33 | { 34 | "command": "sfflowvisualiser.generateWebview", 35 | "when": "editorLangId == xml" 36 | } 37 | ] 38 | } 39 | }, 40 | "author": "Todd Halfpenny", 41 | "license": "ISC", 42 | "repository": { 43 | "type": "git", 44 | "url": "git@github.com:toddhalfpenny/sfflowvisualiser-vscode.git" 45 | }, 46 | "publisher": "ToddHalfpenny", 47 | "extensionDependencies": [], 48 | "scripts": { 49 | "vscode:prepublish": "npm run esbuild-base -- --minify", 50 | "esbuild-base": "esbuild src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node", 51 | "compile": "node ./esbuild.js", 52 | "package": "NODE_ENV=production node ./esbuild.js", 53 | "watch": "node ./esbuild.js --watch", 54 | "pretest": "npm run compile && npm run lint", 55 | "lint": "eslint src --ext ts", 56 | "test": "node ./out/test/runTest.js" 57 | }, 58 | "devDependencies": { 59 | "@types/mocha": "^10.0.3", 60 | "@types/node": "18.x", 61 | "@types/vscode": "1.83.0", 62 | "@types/vscode-webview": "^1.57.4", 63 | "@typescript-eslint/eslint-plugin": "^6.9.0", 64 | "@typescript-eslint/parser": "^6.9.0", 65 | "@vscode/test-electron": "^2.3.6", 66 | "esbuild": "^0.16.17", 67 | "eslint": "^8.52.0", 68 | "glob": "^10.3.10", 69 | "mocha": "^10.2.0", 70 | "typescript": "^5.2.2" 71 | }, 72 | "dependencies": { 73 | "@vscode/webview-ui-toolkit": "^1.2.2", 74 | "mermaid": "10.4.0", 75 | "salesforce-flow-visualiser": "^1.0.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/webview/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | provideVSCodeDesignSystem, 3 | vsCodeButton, 4 | Button, 5 | vsCodeDataGrid, 6 | vsCodeDataGridCell, 7 | vsCodeDataGridRow, 8 | vsCodeTextArea, 9 | DataGrid, 10 | DataGridCell, 11 | } from "@vscode/webview-ui-toolkit"; 12 | 13 | import mermaid from "mermaid"; 14 | 15 | provideVSCodeDesignSystem().register( 16 | vsCodeButton(), 17 | vsCodeDataGrid(), 18 | vsCodeDataGridCell(), 19 | vsCodeDataGridRow(), 20 | vsCodeTextArea(), 21 | ); 22 | 23 | const vscode = acquireVsCodeApi(); 24 | 25 | let svgStr: string = ""; 26 | let flowMap: any; 27 | let flowWidth = 2000; 28 | 29 | window.addEventListener("load", main); 30 | 31 | mermaid.parseError = function (err, hash) { 32 | vscode.postMessage({ 33 | command: "PARSE ERROR", 34 | text: err, 35 | }); 36 | }; 37 | 38 | async function main() { 39 | const saveAsPngButton = document.getElementById("saveasnpng") as Button; 40 | saveAsPngButton?.addEventListener("click", handleSaveAsPng); 41 | 42 | const zoomInButton = document.getElementById("zoomin") as Button; 43 | zoomInButton?.addEventListener("click", handleZoomIn); 44 | const zoomOutButton = document.getElementById("zoomout") as Button; 45 | zoomOutButton?.addEventListener("click", handleZoomOut); 46 | 47 | try { 48 | const configSpan = document.getElementById("mermaid-wrapper"); 49 | const darkModeTheme = configSpan?.dataset.darkModeTheme; 50 | const lightModeTheme = configSpan?.dataset.lightModeTheme; 51 | 52 | mermaid.initialize({ 53 | startOnLoad: false, 54 | theme: 55 | document.body.classList.contains("vscode-dark") || 56 | document.body.classList.contains("vscode-high-contrast") 57 | ? darkModeTheme ?? "dark" 58 | : lightModeTheme ?? "default", 59 | securityLevel: "loose", 60 | }); 61 | const element = document.getElementsByClassName("flow-mermaid")[0]; 62 | const mmdStr = element.textContent ?? ""; 63 | await mermaid.parse(mmdStr); 64 | 65 | await mermaid.run({ 66 | querySelector: ".flow-mermaid", 67 | suppressErrors: true, 68 | }); 69 | vscode.postMessage({ 70 | command: "rendered", 71 | }); 72 | const svg = document.querySelector("svg"); 73 | if (svg) { 74 | svg.addEventListener("click", (evt) => { 75 | const target = evt?.target ? evt.target.innerText : "no-target"; 76 | vscode.postMessage({ 77 | command: "nodeClicked", 78 | text: target, 79 | }); 80 | }); 81 | } 82 | } catch (e: any) { 83 | vscode.postMessage({ 84 | command: "CAUGHT", 85 | text: e.message, 86 | }); 87 | } 88 | } 89 | 90 | window.addEventListener("message", (event) => { 91 | const message = event.data; // The JSON data our extension sent 92 | 93 | switch (message.command) { 94 | case "parsedXml": 95 | flowMap = message.message; 96 | break; 97 | } 98 | }); 99 | 100 | function handleSaveAsPng() { 101 | const svgImage = document.createElement("img"); 102 | 103 | const svg2 = document.querySelector("svg"); 104 | var box = svg2?.viewBox.baseVal; 105 | 106 | document.body.appendChild(svgImage); 107 | svgImage.onload = function () { 108 | const canvas = document.createElement("canvas"); 109 | if (box) { 110 | canvas.width = box.width; 111 | canvas.height = box.height; 112 | } 113 | const canvasCtx = canvas.getContext("2d"); 114 | canvasCtx?.drawImage(svgImage, 0, 0); 115 | var a = document.createElement("a"); 116 | a.href = canvas.toDataURL("image/png"); 117 | a.setAttribute( 118 | "download", 119 | flowMap["label"].replaceAll(" ", "-").toLowerCase() + ".flow.png", 120 | ); 121 | a.dispatchEvent(new MouseEvent("click")); 122 | }; 123 | const svg = document.querySelector("svg"); 124 | const svgData = new XMLSerializer().serializeToString(svg as SVGSVGElement); 125 | svgImage.src = 126 | "data:image/svg+xml;charset=utf-8;base64," + 127 | btoa(unescape(encodeURIComponent(svgData))); 128 | } 129 | 130 | function handleZoomIn() { 131 | const elem = document.getElementById("flow-mermaid"); 132 | if (elem) { 133 | let flowWidth = elem.getBoundingClientRect().width; 134 | flowWidth += 1000; 135 | (elem.style)["min-width"] = flowWidth + "px"; 136 | } 137 | } 138 | 139 | function handleZoomOut() { 140 | const elem = document.getElementById("flow-mermaid"); 141 | if (elem) { 142 | let flowWidth = elem.getBoundingClientRect().width; 143 | flowWidth -= 1000; 144 | (elem.style)["min-width"] = flowWidth + "px"; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/panels/FlowPanel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getUri } from "../utilities/getUri"; 3 | import { getNonce } from "../utilities/getNonce"; 4 | 5 | export class FlowPanel { 6 | public static currentPanel: FlowPanel | undefined; 7 | private readonly _panel: vscode.WebviewPanel; 8 | private _disposables: vscode.Disposable[] = []; 9 | private _flowMap: any; 10 | 11 | private constructor( 12 | panel: vscode.WebviewPanel, 13 | extensionUri: vscode.Uri, 14 | parsedXml: any, 15 | ) { 16 | this._panel = panel; 17 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables); 18 | this._panel.webview.html = this._getWebviewContent( 19 | this._panel.webview, 20 | extensionUri, 21 | parsedXml, 22 | ); 23 | this._setWebviewMessageListener(this._panel.webview, parsedXml); 24 | } 25 | 26 | public dispose() { 27 | FlowPanel.currentPanel = undefined; 28 | 29 | this._panel.dispose(); 30 | 31 | while (this._disposables.length) { 32 | const disposable = this._disposables.pop(); 33 | if (disposable) { 34 | disposable.dispose(); 35 | } 36 | } 37 | } 38 | 39 | public static render(extensionUri: vscode.Uri, parsedXml: any) { 40 | if (FlowPanel.currentPanel) { 41 | FlowPanel.currentPanel._panel.reveal(vscode.ViewColumn.One); 42 | } else { 43 | // console.log('render'); 44 | const flowMap = parsedXml.flowMap; 45 | const panel = vscode.window.createWebviewPanel( 46 | "flow-render", 47 | flowMap.label, 48 | vscode.ViewColumn.One, 49 | { 50 | enableScripts: true, 51 | localResourceRoots: [vscode.Uri.joinPath(extensionUri, "out")], 52 | }, 53 | ); 54 | 55 | FlowPanel.currentPanel = new FlowPanel(panel, extensionUri, parsedXml); 56 | } 57 | } 58 | 59 | private _getWebviewContent( 60 | webview: vscode.Webview, 61 | extensionUri: vscode.Uri, 62 | parsedXml: any, 63 | ) { 64 | const webviewUri = getUri(webview, extensionUri, ["out", "webview.js"]); 65 | const nonce = getNonce(); 66 | this._flowMap = parsedXml.flowMap; 67 | 68 | const constants = this.getConstants(this._flowMap.constants); 69 | const variables = this.getVariables(this._flowMap.variables); 70 | const formulas = this.getFormulas(this._flowMap.formulas); 71 | const startConditions = this.getStartConditions(this._flowMap.start); 72 | const textTemplates = this.getTextTemplates(this._flowMap.textTemplates); 73 | // console.log(this._flowMap); 74 | 75 | return /*html*/ ` 76 | 77 | 78 | 79 | 80 | 81 | 88 | ${this._flowMap.label} 89 | 131 | 132 | 133 |

${this._flowMap.label}

134 |

${this.getFlowType(this._flowMap)}

135 |

${this._flowMap.status}

136 | ${startConditions} 137 | ${constants} 138 | ${formulas} 139 | ${variables} 140 | ${textTemplates} 141 |

Flow

142 | Save as .png 143 | Zoom + 144 | - Zoom 145 |
146 |
${parsedXml.uml}
147 |
148 | 149 | 150 | 153 | 154 | 155 | `; 156 | } 157 | 158 | private _setWebviewMessageListener(webview: vscode.Webview, parsedXml: any) { 159 | webview.onDidReceiveMessage( 160 | (message: any) => { 161 | const command = message.command; 162 | switch (command) { 163 | case "rendered": 164 | this._panel.webview.postMessage({ 165 | command: "parsedXml", 166 | message: this._flowMap, 167 | }); 168 | return; 169 | case "nodeClicked": 170 | // TODO - handle showing more info, perhaps in a side pane? 171 | break; 172 | } 173 | }, 174 | undefined, 175 | this._disposables, 176 | ); 177 | } 178 | 179 | private getFlowType(flowMap: any): string { 180 | if (flowMap.processType === "Flow") { 181 | return "Screen flow"; 182 | } else { 183 | switch (flowMap.start.triggerType) { 184 | case "Scheduled": 185 | return "Scheduled flow;"; 186 | case "RecordAfterSave": 187 | return ( 188 | "Record triggered flow: After Save (" + flowMap.start.object + ")" 189 | ); 190 | case "RecordBeforeSave": 191 | return ( 192 | "Record triggered flow: Before Save (" + flowMap.start.object + ")" 193 | ); 194 | case "PlatformEvent": 195 | return "PlatformEvent triggered flow (" + flowMap.start.object + ")"; 196 | default: 197 | return "Autolaunched flow - No trigger"; 198 | } 199 | } 200 | } 201 | 202 | private getStartConditions(start: any): string { 203 | if (start.object) { 204 | const recTriggerType = 205 | start.recordTriggerType === "CreateAndUpdate" 206 | ? "Create and update" 207 | : start.recordTriggerType; 208 | const filtersStr = this.getFilterStr(start); 209 | 210 | return /*html*/ ` 211 |
212 | Start Conditions 213 |

${start.object}

214 |

${recTriggerType}

215 | ${filtersStr} 216 |
217 | `; 218 | } else { 219 | return ""; 220 | } 221 | } 222 | 223 | private getVariables(variables: any): string { 224 | if (variables) { 225 | variables = variables.length ? variables : [variables]; 226 | let variablestr = /*html*/ ` 227 |
228 | Variables 229 | 230 | 231 | Name 232 | Data Type 233 | Is Collection 234 | In Input 235 | Is Output 236 | Object Type 237 | `; 238 | for (const variable of variables) { 239 | variablestr += /*html*/ ` 240 | 241 | ${variable.name} 242 | ${variable.dataType} 243 | ${variable.isCollection} 244 | ${variable.isInput} 245 | ${variable.isOutput} 246 | ${variable.objectType} 247 | 248 | `; 249 | } 250 | variablestr += "
"; 251 | return variablestr; 252 | } else { 253 | return ""; 254 | } 255 | } 256 | 257 | private getConstants(constants: any): string { 258 | if (constants) { 259 | constants = constants.length ? constants : [constants]; 260 | let constantstr = /*html*/ ` 261 |
262 | Constants 263 | 264 | 265 | Name 266 | Data Type 267 | Value 268 | `; 269 | for (const constant of constants) { 270 | let constantValue = ""; 271 | for (const prop in constant.value) { 272 | constantValue = constant.value[prop]; 273 | } 274 | constantstr += /*html*/ ` 275 | 276 | ${constant.name} 277 | ${constant.dataType} 278 | ${constantValue} 279 | 280 | `; 281 | } 282 | constantstr += "
"; 283 | return constantstr; 284 | } else { 285 | return ""; 286 | } 287 | } 288 | 289 | private getFormulas(formulas: any): string { 290 | if (formulas) { 291 | formulas = formulas.length ? formulas : [formulas]; 292 | 293 | let formulaStr = /*html*/ ` 294 |
295 | Formulas 296 | `; 297 | for (const formula of formulas) { 298 | // TEMP - THIS IS WHAT WE WERE DOING BUT It's coming from our lib wrong. 299 | // Issue is that this converts all special chars back to HTML entities, and they might not be like that in the XML 300 | // var decoded = this.encodeHTML(formula.expression); 301 | 302 | console.log(formula.expression); 303 | var decoded = formula.expression.replaceAll('"', """); 304 | 305 | formulaStr += /*html*/ ` 306 |
307 |
308 |

${formula.name}

309 |

${formula.dataType}

310 |
311 | 312 |
313 | `; 314 | } 315 | formulaStr += "
"; 316 | return formulaStr; 317 | } else { 318 | return ""; 319 | } 320 | } 321 | 322 | private encodeHTML(str: any) { 323 | const code = { 324 | " ": "nbsp;", 325 | "¢": "cent;", 326 | "£": "pound;", 327 | "¥": "yen;", 328 | "€": "euro;", 329 | "©": "copy;", 330 | "®": "reg;", 331 | "<": "lt;", 332 | ">": "gt;", 333 | '"': "quot;", 334 | "'": "apos;", 335 | }; 336 | return str.replace( 337 | /[\u00A0-\u9999<>\&''""]/gm, 338 | (i: any) => "&" + (code)[i], 339 | ); 340 | } 341 | 342 | private getTextTemplates(textTemplates: any): string { 343 | if (textTemplates) { 344 | textTemplates = textTemplates.length ? textTemplates : [textTemplates]; 345 | 346 | let textTemplateStr = /*html*/ ` 347 |
348 | Text Templates 349 | `; 350 | for (const template of textTemplates) { 351 | textTemplateStr += /*html*/ ` 352 |
353 |
354 |

${template.name}

355 | ${ 356 | template.description 357 | ? "

" + 358 | template.description + 359 | "

" 360 | : "" 361 | } 362 |

${template.isViewedAsPlainText}

363 |
364 | 367 |
368 | `; 369 | } 370 | textTemplateStr += "
"; 371 | return textTemplateStr; 372 | } else { 373 | return ""; 374 | } 375 | } 376 | 377 | private getFilterStr(start: any): string { 378 | let filterLogicStr = ""; 379 | if (start.filterFormula) { 380 | return /*html*/ ` 381 |

${start.filterFormula}

382 | `; 383 | } else { 384 | switch (start.filterLogic) { 385 | case "and": 386 | filterLogicStr = "All conditions are met (AND)"; 387 | break; 388 | case "or": 389 | filterLogicStr = "Any condition is met (OR)"; 390 | break; 391 | case undefined: 392 | return filterLogicStr; 393 | break; 394 | deflt: filterLogicStr = start.filterLogic; 395 | } 396 | 397 | let filtersStr = /*html*/ ` 398 | 399 | 400 | 401 | Field 402 | Operator 403 | Value 404 | `; 405 | start.filters = start.filters.length ? start.filters : [start.filters]; 406 | let i = 1; 407 | for (const filter of start.filters) { 408 | let filterValue = ""; 409 | for (const prop in filter.value) { 410 | filterValue = filter.value[prop]; 411 | } 412 | filtersStr += /*html*/ ` 413 | 414 | ${i} 415 | ${ 416 | filter.field 417 | } 418 | ${this.splitAndCapitalise( 419 | filter.operator, 420 | )} 421 | ${filterValue} 422 | 423 | `; 424 | i++; 425 | } 426 | filtersStr += ""; 427 | 428 | return /*html*/ ` 429 |

${filterLogicStr}

430 | ${filtersStr} 431 | `; 432 | } 433 | } 434 | 435 | private splitAndCapitalise(str: string): string { 436 | return str.replace(/([A-Z])/g, " $1").replace(/^./, function (str) { 437 | return str.toUpperCase(); 438 | }); 439 | } 440 | } 441 | --------------------------------------------------------------------------------