├── graphviz └── main │ └── default │ ├── aura │ ├── DiagramCard │ │ ├── DiagramCard.css │ │ ├── DiagramCard.cmp-meta.xml │ │ ├── DiagramCardController.js │ │ └── DiagramCard.cmp │ ├── Pill │ │ ├── Pill.css │ │ ├── Pill.cmp-meta.xml │ │ ├── Pill.cmp │ │ └── PillController.js │ ├── TestApp │ │ ├── TestAppController.js │ │ ├── TestApp.app-meta.xml │ │ └── TestApp.app │ ├── Modal │ │ ├── ModalController.js │ │ ├── Modal.cmp-meta.xml │ │ └── Modal.cmp │ ├── ActionEvent │ │ ├── ActionEvent.evt │ │ └── ActionEvent.evt-meta.xml │ ├── AutoBuildProgress │ │ ├── AutoBuildProgress.css │ │ ├── AutoBuildProgress.cmp-meta.xml │ │ ├── AutoBuildProgressController.js │ │ ├── AutoBuildProgress.cmp │ │ └── AutoBuildProgressHelper.js │ ├── SOQLRenderTests │ │ ├── SOQLRenderTests.app │ │ └── SOQLRenderTests.app-meta.xml │ ├── Panel │ │ ├── Panel.cmp-meta.xml │ │ ├── PanelHelper.js │ │ ├── PanelController.js │ │ └── Panel.cmp │ ├── AppHeader │ │ ├── AppHeader.cmp-meta.xml │ │ └── AppHeader.cmp │ ├── DiagramViewerTests │ │ ├── DiagramViewerTests.app │ │ └── DiagramViewerTests.app-meta.xml │ ├── SOQLRenderer │ │ ├── SOQLRenderer.cmp-meta.xml │ │ ├── SOQLRenderer.css │ │ ├── SOQLRendererHelper.js │ │ ├── SOQLRendererController.js │ │ └── SOQLRenderer.cmp │ ├── AutoBuildStart │ │ ├── AutoBuildStart.evt │ │ └── AutoBuildStart.evt-meta.xml │ ├── DiagramOutput │ │ ├── DiagramOutput.cmp-meta.xml │ │ ├── DiagramOutput.css │ │ ├── DiagramOutputHelper.js │ │ ├── DiagramOutputController.js │ │ └── DiagramOutput.cmp │ ├── DiagramViewer │ │ ├── DiagramViewer.cmp-meta.xml │ │ ├── DiagramViewerHelper.js │ │ ├── DiagramViewer.cmp │ │ └── DiagramViewerController.js │ ├── ERDLightningOut │ │ ├── ERDLightningOut.app-meta.xml │ │ └── ERDLightningOut.app │ ├── AutoBuildSelector │ │ ├── AutoBuildSelector.cmp-meta.xml │ │ ├── AutoBuildSelectorController.js │ │ └── AutoBuildSelector.cmp │ ├── ERDContainer │ │ ├── ERDContainer.cmp-meta.xml │ │ ├── ERDContainer.css │ │ ├── ERDContainerController.js │ │ └── ERDContainer.cmp │ ├── GroupPanel │ │ ├── GroupPanel.cmp-meta.xml │ │ ├── GroupPanel.css │ │ ├── GroupPanelController.js │ │ └── GroupPanel.cmp │ ├── ObjectPanel │ │ ├── ObjectPanel.cmp-meta.xml │ │ ├── ObjectPanel.css │ │ ├── ObjectPanelHelper.js │ │ ├── ObjectPanel.cmp │ │ └── ObjectPanelController.js │ ├── AttributePanel │ │ ├── AttributePanel.cmp-meta.xml │ │ ├── AttributePanel.css │ │ ├── AttributePanel.cmp │ │ └── AttributePanelController.js │ ├── DiagramMutateEvent │ │ ├── DiagramMutateEvent.evt-meta.xml │ │ └── DiagramMutateEvent.evt │ ├── DiagramUpdatedEvent │ │ ├── DiagramUpdatedEvent.evt-meta.xml │ │ └── DiagramUpdatedEvent.evt │ ├── DiagramConfigurator │ │ ├── DiagramConfigurator.cmp-meta.xml │ │ ├── DiagramConfiguratorController.js │ │ └── DiagramConfigurator.cmp │ └── DiagramSettingsChangeEvent │ │ ├── DiagramSettingsChangeEvent.evt-meta.xml │ │ └── DiagramSettingsChangeEvent.evt │ ├── classes │ ├── AutoBuild.cls-meta.xml │ ├── ToolingAPI.cls-meta.xml │ ├── UmlService.cls-meta.xml │ ├── AutoBuildApex.cls-meta.xml │ ├── AutoBuildTrigger.cls-meta.xml │ ├── BuilderSource.cls-meta.xml │ ├── LightningUtility.cls-meta.xml │ ├── ToolingAPIMocks.cls-meta.xml │ ├── ToolingAPITest.cls-meta.xml │ ├── AutoBuildApexTests.cls-meta.xml │ ├── AutoBuildTriggerTests.cls-meta.xml │ ├── BuilderSourceType.cls-meta.xml │ ├── GraphVizForceController.cls-meta.xml │ ├── GraphvizForceControllerTest.cls-meta.xml │ ├── BuilderSource.cls │ ├── BuilderSourceType.cls │ ├── LightningUtility.cls │ ├── GraphvizForceControllerTest.cls │ ├── ToolingAPIMocks.cls │ ├── AutoBuildTrigger.cls │ ├── AutoBuild.cls │ ├── AutoBuildApex.cls │ └── GraphVizForceController.cls │ ├── staticresources │ ├── GVF2TestsJasmine.resource-meta.xml │ ├── pure.resource-meta.xml │ ├── d3v4.resource-meta.xml │ ├── LightningUtils.resource-meta.xml │ ├── VizJS.resource-meta.xml │ ├── d3graphviz.resource-meta.xml │ ├── GraphvizForceUtils.resource-meta.xml │ ├── GraphvizForceUtils.resource │ ├── GVF2TestsJasmine │ │ └── DiagramViewerTests.js │ └── LightningUtils.resource │ ├── tabs │ └── Graphviz_Diagrams.tab-meta.xml │ ├── pages │ ├── AutoBuildBridge.page-meta.xml │ └── AutoBuildBridge.page │ ├── objects │ ├── GraphvizConfig__c │ │ ├── GraphvizConfig__c.object-meta.xml │ │ └── fields │ │ │ ├── Value__c.field-meta.xml │ │ │ └── Name__c.field-meta.xml │ ├── Graphviz_Diagram__c │ │ ├── fields │ │ │ └── Content__c.field-meta.xml │ │ ├── listViews │ │ │ └── All.listView-meta.xml │ │ └── Graphviz_Diagram__c.object-meta.xml │ └── Graphviz_AutoBuild__mdt │ │ ├── fields │ │ ├── Description__c.field-meta.xml │ │ ├── Apex_Class__c.field-meta.xml │ │ └── Lightning_Config_Component__c.field-meta.xml │ │ └── Graphviz_AutoBuild__mdt.object-meta.xml │ ├── flexipages │ └── GraphvizForce_App_UtilityBar.flexipage-meta.xml │ ├── applications │ └── GraphvizForce_App.app-meta.xml │ ├── customMetadata │ ├── Graphviz_AutoBuild.All_Apex.md-meta.xml │ └── Graphviz_AutoBuild.Apex_Triggers.md-meta.xml │ ├── layouts │ ├── Graphviz_Diagram__c-Graphviz Diagram Layout.layout-meta.xml │ └── Graphviz_AutoBuild__mdt-Graphviz AutoBuild Layout.layout-meta.xml │ └── permissionsets │ └── Graphvizforce.permissionset-meta.xml ├── doc ├── assets │ └── basics-screenshot.png ├── ADR │ ├── adr0004-lightning-data-service.md │ ├── adr0001-es5-vs-es6.md │ ├── adr0003-node-modules.md │ └── adr0002-lightning-out.md ├── learning.md ├── diagram-viewer.md └── development.md ├── config ├── project-scratch-def.json └── lts-config.json ├── js ├── pure │ ├── spec │ │ ├── support │ │ │ └── jasmine.json │ │ └── SOQL2RenderSpec.js │ ├── src │ │ ├── pure.js │ │ └── schemas.js │ ├── package.json │ ├── webpack.config.js │ └── test │ │ └── diagramViewSamples.js └── diagram-viewer │ ├── package.json │ ├── webpack.config.js │ ├── src │ └── index.js │ └── index.html ├── sfdx-project.json ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── .circleci └── config.yml ├── README.md ├── docs └── index.css └── package.xml /graphviz/main/default/aura/DiagramCard/DiagramCard.css: -------------------------------------------------------------------------------- 1 | .THIS .description { 2 | min-height: 40px; 3 | } -------------------------------------------------------------------------------- /graphviz/main/default/aura/Pill/Pill.css: -------------------------------------------------------------------------------- 1 | .THIS .slds-checkbox .slds-form-element__label{ 2 | font-size:14px; 3 | } -------------------------------------------------------------------------------- /doc/assets/basics-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevebuik/Graphvizforce-Lightning/HEAD/doc/assets/basics-screenshot.png -------------------------------------------------------------------------------- /graphviz/main/default/aura/TestApp/TestAppController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | doInit: function (component, event, helper) { 3 | 4 | } 5 | }) -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "steve Company", 3 | "edition": "Developer", 4 | "orgPreferences" : { 5 | "enabled": ["S1DesktopEnabled"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/Modal/ModalController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Hides modal when user closes it 4 | */ 5 | close : function(component, event, helper) { 6 | component.set('v.show', false); 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/ActionEvent/ActionEvent.evt: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/AutoBuild.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/ToolingAPI.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/UmlService.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/AutoBuildApex.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/AutoBuildTrigger.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/BuilderSource.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/LightningUtility.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/ToolingAPIMocks.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/ToolingAPITest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/AutoBuildApexTests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/AutoBuildTriggerTests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/BuilderSourceType.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildProgress/AutoBuildProgress.css: -------------------------------------------------------------------------------- 1 | .THIS { 2 | margin-bottom: 1rem; 3 | } 4 | 5 | .THIS .spinnerContainer { 6 | position: relative; 7 | display: inline-block; 8 | width: 30px; 9 | height: 15px; 10 | } -------------------------------------------------------------------------------- /graphviz/main/default/aura/SOQLRenderTests/SOQLRenderTests.app: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/GraphVizForceController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /js/pure/spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": true 11 | } 12 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/GraphvizForceControllerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/Pill/Pill.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Pill 5 | 6 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "graphviz", 5 | "default": true 6 | } 7 | ], 8 | "namespace": "gvf2", 9 | "sfdcLoginUrl": "https://login.salesforce.com", 10 | "sourceApiVersion": "41.0" 11 | } 12 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/Modal/Modal.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Modal 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/Panel/Panel.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | Panel 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/TestApp/TestApp.app-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | TestApp 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AppHeader/AppHeader.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | AppHeader 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramViewerTests/DiagramViewerTests.app: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/ActionEvent/ActionEvent.evt-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | ActionEvent 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramCard/DiagramCard.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | DiagramCard 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/SOQLRenderer/SOQLRenderer.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | SOQLRenderer 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildStart/AutoBuildStart.evt: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildStart/AutoBuildStart.evt-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | AutoBuildStart 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramOutput/DiagramOutput.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | DiagramOutput 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramViewer/DiagramViewer.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 39.0 4 | DiagramViewer 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/staticresources/GVF2TestsJasmine.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Private 4 | application/zip 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/ERDLightningOut/ERDLightningOut.app-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | ERDLightningOut 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/SOQLRenderTests/SOQLRenderTests.app-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | SOQLRenderTests 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildProgress/AutoBuildProgress.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | AutoBuildProgress 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildSelector/AutoBuildSelector.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | AutoBuildSelector 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/ERDContainer/ERDContainer.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | A Lightning Component Bundle 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/GroupPanel/GroupPanel.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | A Lightning Component Bundle 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/GroupPanel/GroupPanel.css: -------------------------------------------------------------------------------- 1 | .THIS .flexContainer{ 2 | max-height:100vh; 3 | overflow:scroll; 4 | } 5 | .THIS .flexList{ 6 | display: -webkit-flex; /* Safari */ 7 | -webkit-flex-wrap: wrap; /* Safari 6.1+ */ 8 | display: flex; 9 | flex-wrap: wrap; 10 | } -------------------------------------------------------------------------------- /graphviz/main/default/aura/ObjectPanel/ObjectPanel.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | A Lightning Component Bundle 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AttributePanel/AttributePanel.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | A Lightning Component Bundle 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramMutateEvent/DiagramMutateEvent.evt-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | DiagramMutateEvent 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramUpdatedEvent/DiagramUpdatedEvent.evt-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | DiagramUpdatedEvent 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramViewerTests/DiagramViewerTests.app-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | DiagramViewerTests 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramConfigurator/DiagramConfigurator.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | A Lightning Component Bundle 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/ERDLightningOut/ERDLightningOut.app: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramConfigurator/DiagramConfiguratorController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Set selected object attribute when onGoToDetails event is captured 4 | */ 5 | onSelectObject : function(component, event, helper){ 6 | component.set('v.selectedObject', event.getParam('scope')); 7 | }, 8 | }) -------------------------------------------------------------------------------- /graphviz/main/default/staticresources/pure.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public 4 | application/javascript 5 | pure 6 | 7 | -------------------------------------------------------------------------------- /graphviz/main/default/tabs/Graphviz_Diagrams.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | Custom78: Map 6 | AutoBuildBridge 7 | 8 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramSettingsChangeEvent/DiagramSettingsChangeEvent.evt-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41.0 4 | DiagramSettingsChangeEvent 5 | 6 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/BuilderSource.cls: -------------------------------------------------------------------------------- 1 | public class BuilderSource { 2 | 3 | @AuraEnabled 4 | public String source; 5 | 6 | @AuraEnabled 7 | public String id; 8 | 9 | public BuilderSource(String source, String id) { 10 | this.source = source; 11 | this.id = id; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphviz/main/default/staticresources/d3v4.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public 4 | application/javascript 5 | d3.js v4 6 | 7 | -------------------------------------------------------------------------------- /js/pure/src/pure.js: -------------------------------------------------------------------------------- 1 | var soql = require('./soql.js'); 2 | var graphviz = require('./graphviz.js'); 3 | var Validator = require('jsonschema').Validator; 4 | var schemas = require('../src/schemas.js'); 5 | 6 | module.exports = { 7 | soql: soql, 8 | graphviz: graphviz, 9 | validator: Validator, 10 | schemas: schemas 11 | }; 12 | -------------------------------------------------------------------------------- /graphviz/main/default/staticresources/LightningUtils.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public 4 | application/javascript 5 | LightningUtils 6 | 7 | -------------------------------------------------------------------------------- /graphviz/main/default/staticresources/VizJS.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public 4 | text/javascript 5 | Javascript library for Graphviz 6 | 7 | -------------------------------------------------------------------------------- /graphviz/main/default/staticresources/d3graphviz.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public 4 | application/javascript 5 | d3-graphviz.js lib 6 | 7 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramOutput/DiagramOutput.css: -------------------------------------------------------------------------------- 1 | .THIS .downloadTextArea textarea { 2 | min-height: 200px; 3 | } 4 | 5 | .THIS .toolbar button { 6 | margin-top: 10px; 7 | } 8 | 9 | .THIS .toolbar button.transient { 10 | background-color: white; 11 | } 12 | 13 | .THIS .sections > * { 14 | border: solid 1px lightgrey; 15 | } -------------------------------------------------------------------------------- /graphviz/main/default/staticresources/GraphvizForceUtils.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public 4 | application/javascript 5 | GraphvizForceUtils 6 | 7 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AttributePanel/AttributePanel.css: -------------------------------------------------------------------------------- 1 | .THIS .slds-checkbox{ 2 | padding: 4px; 3 | } 4 | 5 | .THIS .slds-checkbox .slds-form-element__label{ 6 | font-size:14px; 7 | } 8 | 9 | .THIS .slds-checkbox:nth-of-type(even){ 10 | background: #ffffff; 11 | } 12 | 13 | .THIS .slds-checkbox:nth-of-type(odd){ 14 | background: #eeeeee; 15 | } -------------------------------------------------------------------------------- /graphviz/main/default/pages/AutoBuildBridge.page-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 42.0 4 | false 5 | false 6 | 7 | 8 | -------------------------------------------------------------------------------- /js/pure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "jsonschema": "^1.2.4", 4 | "mustache": "^2.3.0" 5 | }, 6 | "devDependencies": { 7 | "babel-core": "^6.26.3", 8 | "babel-loader": "^7.1.4", 9 | "babel-preset-es2015": "^6.24.1", 10 | "jasmine-node": "^1.14.5", 11 | "webpack": "^4.10.2", 12 | "webpack-cli": "^2.1.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /graphviz/main/default/objects/GraphvizConfig__c/GraphvizConfig__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hierarchy 4 | false 5 | 6 | Public 7 | 8 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/ERDContainer/ERDContainer.css: -------------------------------------------------------------------------------- 1 | .THIS .flexContainer{ 2 | width:100%; 3 | } 4 | 5 | .THIS .flexList{ 6 | display: -webkit-flex; /* Safari */ 7 | -webkit-flex-wrap: wrap; /* Safari 6.1+ */ 8 | display: flex; 9 | flex-wrap: wrap; 10 | } 11 | 12 | .THIS .label-hidden label { 13 | display: none; 14 | } 15 | 16 | .THIS .empty { 17 | margin-bottom: 2rem; 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mdapi 2 | .sfdx 3 | *.iml 4 | .idea 5 | IlluminatedCloud 6 | graphviz/main/default/profiles/Admin.profile-meta.xml 7 | 8 | # Force.com Migration Tool 9 | build.properties 10 | 11 | # Metadata API Package 12 | mdapipkg/ 13 | 14 | # node 15 | /js/pure/node_modules 16 | /js/pure/generated 17 | /js/pure/package-lock.json 18 | /js/diagram-viewer/dist/dev-viewer-app.js 19 | /js/diagram-viewer/node_modules 20 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/ObjectPanel/ObjectPanel.css: -------------------------------------------------------------------------------- 1 | .THIS .flexList{ 2 | display: -webkit-flex; /* Safari */ 3 | -webkit-flex-wrap: wrap; /* Safari 6.1+ */ 4 | display: flex; 5 | flex-wrap: wrap; 6 | } 7 | 8 | .THIS .lineItem{ 9 | padding: 4px; 10 | } 11 | 12 | .THIS .lineItem:nth-of-type(even){ 13 | background: #ffffff; 14 | } 15 | 16 | .THIS .lineItem:nth-of-type(odd){ 17 | background: #eeeeee; 18 | } -------------------------------------------------------------------------------- /graphviz/main/default/aura/TestApp/TestApp.app: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /graphviz/main/default/objects/Graphviz_Diagram__c/fields/Content__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Content__c 4 | false 5 | 6 | 131072 7 | false 8 | LongTextArea 9 | 3 10 | 11 | -------------------------------------------------------------------------------- /graphviz/main/default/objects/Graphviz_Diagram__c/listViews/All.listView-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | All 4 | NAME 5 | Content__c 6 | CREATEDBY_USER 7 | CREATED_DATE 8 | OWNER.ALIAS 9 | Everything 10 | 11 | 12 | -------------------------------------------------------------------------------- /config/lts-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "drivers": { 3 | "chrome": { 4 | "version": "71.0.3578.33", 5 | "arch": "x64", 6 | "baseURL": "https://chromedriver.storage.googleapis.com" 7 | } 8 | }, 9 | "webdriverio": { 10 | "desiredCapabilities": { 11 | "browserName": "chrome", 12 | "chromeOptions": { 13 | "args": [ 14 | "--headless", 15 | "--disable-gpu", 16 | "--no-sandbox" 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramUpdatedEvent/DiagramUpdatedEvent.evt: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramViewer/DiagramViewerHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | transitionTime: function () { 3 | return 400; 4 | }, 5 | attributer: function (datum, index, nodes) { 6 | if (datum.tag == "svg") { 7 | // not sure why zeroes work but leaving it alone since it does. 8 | // diagram/svg fills entire div and scales correctly on resize 9 | datum.attributes.width = -1; 10 | datum.attributes.height = -1; 11 | } 12 | } 13 | }) -------------------------------------------------------------------------------- /graphviz/main/default/flexipages/GraphvizForce_App_UtilityBar.flexipage-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | utilityItems 5 | Region 6 | 7 | GraphvizForce App UtilityBar 8 | 11 | UtilityBar 12 | 13 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/Panel/PanelHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Fires component event when user changes the title, to be handled outside of the panel 4 | */ 5 | handleTitleChange : function(component, event, helper){ 6 | var title = component.get('v.title'); 7 | var newTitle = component.get('v.newTitle'); 8 | if(title !== newTitle){ 9 | component.getEvent("onEditPanelTitle").setParams({scope:{oldValue:title, newValue:newTitle}}).fire(); 10 | } 11 | } 12 | }) -------------------------------------------------------------------------------- /doc/ADR/adr0004-lightning-data-service.md: -------------------------------------------------------------------------------- 1 | ## Context 2 | 3 | LDS is supposed to remove code, specifically Apex. 4 | When used for read and write, it requires a lot of client code, removing the advantage. 5 | 6 | Not supported in Lightning Out! 7 | 8 | Seems well suited for read only i.e. not much code to write. Not suitable for write. 9 | 10 | lightning:recordEditForm is much better if doing a record CRUD form. 11 | 12 | ## Decision 13 | 14 | Stop using LDS. 15 | 16 | ## Status 17 | 18 | 19 | ## Consequences 20 | 21 | -------------------------------------------------------------------------------- /graphviz/main/default/objects/GraphvizConfig__c/fields/Value__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Value__c 4 | The config value 5 | false 6 | 7 | 255 8 | true 9 | false 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /graphviz/main/default/applications/GraphvizForce_App.app-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #0070D2 5 | 6 | Large 7 | 8 | Standard 9 | Graphviz_Diagrams 10 | Lightning 11 | GraphvizForce_App_UtilityBar 12 | 13 | -------------------------------------------------------------------------------- /graphviz/main/default/objects/Graphviz_AutoBuild__mdt/fields/Description__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Description__c 4 | The description displayed to the user in the UI. 5 | false 6 | DeveloperControlled 7 | 8 | false 9 | TextArea 10 | 11 | -------------------------------------------------------------------------------- /graphviz/main/default/objects/GraphvizConfig__c/fields/Name__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Name__c 4 | false 5 | The name of the config value 6 | false 7 | 8 | 255 9 | true 10 | false 11 | Text 12 | true 13 | 14 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/SOQLRenderer/SOQLRenderer.css: -------------------------------------------------------------------------------- 1 | .THIS { 2 | } 3 | 4 | .THIS .from-select { 5 | margin-left: 1rem; 6 | } 7 | 8 | .THIS .from-select .slds-form-element__label { 9 | display: none; 10 | } 11 | 12 | .THIS ul.preview { 13 | font-size: 1rem; 14 | } 15 | 16 | .THIS ul.preview li { 17 | word-wrap: break-word; 18 | } 19 | 20 | .THIS .close-button { 21 | margin-left: 1rem; 22 | } 23 | 24 | .THIS .control { 25 | margin-right: 1rem; 26 | } 27 | 28 | .THIS .soql { 29 | font-size: 1rem; 30 | } 31 | 32 | .THIS .copied-fade { 33 | transition: all 1s ease-in-out; 34 | } -------------------------------------------------------------------------------- /graphviz/main/default/objects/Graphviz_AutoBuild__mdt/Graphviz_AutoBuild__mdt.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Autobuild metadata records are loaded in the Diagram Lightning UI as types of AutoBuilds i.e. options in the select list. 4 | 5 | In Apex they are translated into AutoBuild.BuilderSourceType instances so must have all fields used in the constructor for that class. 6 | 7 | Graphviz AutoBuilds 8 | Public 9 | 10 | -------------------------------------------------------------------------------- /graphviz/main/default/objects/Graphviz_AutoBuild__mdt/fields/Apex_Class__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apex_Class__c 4 | false 5 | The name of the Apex class that implements the AutoBuilder interface 6 | false 7 | DeveloperControlled 8 | 9 | 255 10 | true 11 | Text 12 | true 13 | 14 | -------------------------------------------------------------------------------- /js/pure/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/pure.js', 5 | mode: "production", 6 | output: { 7 | filename: 'pure.resource', 8 | path: path.resolve(__dirname, '../../graphviz/main/default/staticresources'), 9 | libraryTarget: "umd", 10 | library: "pure" 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | query: { 19 | presets: ['es2015'] 20 | } 21 | } 22 | ] 23 | } 24 | }; -------------------------------------------------------------------------------- /graphviz/main/default/objects/Graphviz_AutoBuild__mdt/fields/Lightning_Config_Component__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lightning_Config_Component__c 4 | The name of the component used to collect data before initiating an auto-build. If empty, no data will be collected. 5 | false 6 | DeveloperControlled 7 | 8 | 255 9 | false 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/BuilderSourceType.cls: -------------------------------------------------------------------------------- 1 | public Class BuilderSourceType{ 2 | @AuraEnabled 3 | public String sourceType; 4 | @AuraEnabled 5 | public String label; 6 | @AuraEnabled 7 | public String description; 8 | @AuraEnabled 9 | public String apexClass; 10 | @AuraEnabled 11 | public String configComponent; 12 | public BuilderSourceType(Graphviz_AutoBuild__mdt metaData) { 13 | this.sourceType = metaData.DeveloperName; 14 | this.label = metaData.Label; 15 | this.description = metaData.Description__c; 16 | this.apexClass = metaData.Apex_Class__c; 17 | this.configComponent = metaData.Lightning_Config_Component__c; 18 | } 19 | } -------------------------------------------------------------------------------- /js/diagram-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diagram-viewer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack", 9 | "watch": "webpack --watch" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "webpack": "^4.5.0", 15 | "webpack-cli": "^2.0.14", 16 | "babel-core": "^6.26.3", 17 | "babel-loader": "^7.1.4", 18 | "babel-preset-es2015": "^6.24.1", 19 | "http-server": "^0.11.1" 20 | }, 21 | "dependencies": { 22 | "lightning-container": "^1.0.0", 23 | "d3": "4.13.0", 24 | "d3-graphviz": "2.5.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildSelector/AutoBuildSelectorController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | doInit: function (component, event, helper) { 3 | Core.AuraUtils.execute(component, 'getSourceTypes', {}, function (sources) { 4 | component.set("v.sources", sources) 5 | }); 6 | }, 7 | handleSelection: function (component, event, helper) { 8 | var selectedType = component.find("select").get("v.value"); 9 | if (selectedType != "prompt") { 10 | var startEvent = component.getEvent("startEvent"); 11 | startEvent.setParams({type: selectedType}); 12 | startEvent.fire(); 13 | } 14 | component.find("select").set("v.value", "prompt"); 15 | } 16 | }) -------------------------------------------------------------------------------- /graphviz/main/default/aura/GroupPanel/GroupPanelController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Show modal when user press remove 4 | */ 5 | onRemovePanel : function(component, event, helper) { 6 | component.set('v.showRemoveConfirm', true); 7 | }, 8 | 9 | /** 10 | * Fires event when remove confirmed 11 | */ 12 | onConfirmRemoveGroup : function(component, event, helper) { 13 | component.getEvent('onRemoveGroup').setParams({scope:component.get('v.group')}).fire(); 14 | }, 15 | 16 | /** 17 | * Fires event when group is modified 18 | */ 19 | onEditPanelTitle : function(component, event, helper) { 20 | component.getEvent('onEditGroupName').setParams(event.getParams()).fire(); 21 | }, 22 | }) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM circleci/openjdk 2 | 3 | RUN sudo apt-get update 4 | 5 | RUN curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash - 6 | RUN sudo apt-get install -y nodejs 7 | 8 | RUN curl -L -o /tmp/google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 9 | RUN sudo apt-get install -y libappindicator3-1 fonts-liberation libxss1 xdg-utils 10 | RUN sudo dpkg -i /tmp/google-chrome.deb 11 | RUN sudo sed -i 's|HERE/chrome\"|HERE/chrome\" --disable-setuid-sandbox|g' /opt/google/chrome/google-chrome 12 | 13 | RUN sudo npm install -g sfdx-cli 14 | 15 | # give circleci user write access to sfdx dirs for selenium run 16 | RUN sudo chown -R circleci /usr/lib/node_modules/sfdx-cli 17 | RUN ls -la /usr/lib/node_modules/sfdx-cli 18 | 19 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildProgress/AutoBuildProgressController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | start: function (component, event, helper) { 3 | component.set('v.active', true); 4 | Core.AuraUtils.execute(component, 'getSources', {sourceType: component.get("v.sourceType")}, 5 | $A.getCallback(function (sources) { 6 | component.set("v.sources", sources); 7 | component.set("v.currentSourceIndex", 0); 8 | component.set("v.progress", 0); 9 | if ($A.util.isEmpty(sources)) { 10 | component.set('v.active', false); 11 | } else { 12 | helper.analyzeSource(component, sources[0], $A.getCallback(helper.nextSource)); 13 | } 14 | })); 15 | } 16 | }) -------------------------------------------------------------------------------- /graphviz/main/default/customMetadata/Graphviz_AutoBuild.All_Apex.md-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | Apex_Class__c 7 | AutoBuildApex 8 | 9 | 10 | Description__c 11 | Finds all references to Objects in your Apex classes 12 | 13 | 14 | Lightning_Config_Component__c 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /graphviz/main/default/customMetadata/Graphviz_AutoBuild.Apex_Triggers.md-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | Apex_Class__c 7 | AutoBuildTrigger 8 | 9 | 10 | Description__c 11 | Finds references to fields in Apex triggers 12 | 13 | 14 | Lightning_Config_Component__c 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramCard/DiagramCardController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Fires event when user select a diagram and go to detail view 4 | */ 5 | onViewDiagram : function(component, event, helper){ 6 | component.getEvent('onViewDiagram').setParams({scope:component.get('v.diagram')}).fire(); 7 | }, 8 | 9 | /** 10 | * Show confirmation modal when user press delete 11 | */ 12 | onRemoveDiagram : function(component, event, helper){ 13 | component.set('v.showRemoveConfirm', true); 14 | }, 15 | 16 | /** 17 | * Fires remove diagram event when user confirm deletion 18 | */ 19 | onConfirmDeleteDiagram : function(component, event, helper){ 20 | component.getEvent('onRemoveDiagram').setParams({scope:component.get('v.diagram')}).fire(); 21 | }, 22 | 23 | }) -------------------------------------------------------------------------------- /graphviz/main/default/aura/Pill/Pill.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 |
12 | 13 |
-------------------------------------------------------------------------------- /js/diagram-viewer/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './src/index.js', 6 | output: { 7 | libraryTarget: 'umd', 8 | library: "viewer", 9 | filename: 'dev-viewer-app.js', 10 | path: path.resolve(__dirname, './dist') 11 | }, 12 | // output: { 13 | // libraryTarget: 'umd', 14 | // library: "viewer", 15 | // filename: 'DiagramViewerApp.resource', 16 | // path: path.resolve(__dirname, '../../graphviz/main/default/staticresources') 17 | // }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | exclude: /node_modules/, 23 | loader: 'babel-loader', 24 | query: { 25 | presets: ['es2015'] 26 | } 27 | } 28 | ] 29 | } 30 | }; -------------------------------------------------------------------------------- /doc/learning.md: -------------------------------------------------------------------------------- 1 | ## What can you learn from this project? 2 | 3 | * Using SFDX to create a fresh dev env in 5 mins 4 | * composition vs inheritance (DiagramDataService, DeBounce) 5 | * Testing Lightning components using the LTS 6 | * loading fns from a static resource (global fns vs pure fns) 7 | * instant feedback dev using npm tools, then deploying using UMD 8 | * calling the Tooling/Rest APIs from Lightning and future Apex Tooling APIs 9 | * JSON for persistence instead of custom fields, entities etc 10 | * JSON validation for interim values or attribute values 11 | * ADR docs instead of long form architecture docs 12 | 13 | Future 14 | 15 | * TODO debounce 16 | * TODO CI 17 | 18 | ### js static resource/module 19 | 20 | * UMD allows script and node.js use 21 | * must use "production" mode since eval blocked in "development" mode 22 | * exports not available in devtools console since "window" is proxied (i.e. different) for each Lightning component 23 | -------------------------------------------------------------------------------- /doc/ADR/adr0001-es5-vs-es6.md: -------------------------------------------------------------------------------- 1 | ## Context 2 | 3 | For developers, ES6 provides many nice features but the Lightning Linter defaults to ES5. 4 | It's non-trivial to make the linter use ES6: 5 | https://developer.salesforce.com/forums/?id=9060G000000MVJSQA4 6 | https://salesforce.stackexchange.com/questions/199495/ecma-script-6-eslint-with-lightninglint 7 | 8 | Salesforce supports IE11 for Lightning Experience but IE11 only supports a small set of ES6 features: 9 | http://kangax.github.io/compat-table/es6/#ie11 10 | 11 | ## Decision 12 | 13 | Only use ES5 features in this project, to allow a clean linter run and automated linting in CI. 14 | 15 | ## Status 16 | 17 | Removed all the ES6 errors found by the DX linter and app still works. 18 | 19 | ## Consequences 20 | 21 | Less readable code but higher browser compatibility. 22 | 23 | Caveat: browser compatibility is good but most users of this tool will be admins who generally use Chrome and LTS also uses headless Chrome with full ES6 support. -------------------------------------------------------------------------------- /graphviz/main/default/aura/Pill/PillController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Handler when user changes the checkbox selection 4 | */ 5 | onSelectionChange : function(component, event, helper) { 6 | var value = component.get('v.value'); 7 | if(value.selected){ 8 | // Create well-formed entity object and fire mutate event 9 | value.fields = []; 10 | component.getEvent('onDiagramMutate').setParams({entitiesToAdd:[value]}).fire(); 11 | component.getEvent('onGoToDetails').setParams({scope:value}).fire(); 12 | } 13 | else{ 14 | component.getEvent('onDiagramMutate').setParams({entitiesToRemove:[value]}).fire(); 15 | component.getEvent('onGoToDetails').setParams({scope:null}).fire(); 16 | } 17 | }, 18 | 19 | /** 20 | * Handler when user press arrow button to go to detail view 21 | */ 22 | onGoToDetails : function(component, event, helper) { 23 | var value = component.get('v.value'); 24 | component.getEvent('onGoToDetails').setParams({scope:value}).fire(); 25 | }, 26 | }) -------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildSelector/AutoBuildSelector.cmp: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /graphviz/main/default/classes/LightningUtility.cls: -------------------------------------------------------------------------------- 1 | public class LightningUtility { 2 | 3 | 4 | public class ResultWrapper { 5 | public ServiceStatus serviceStatus; 6 | public Object result; 7 | 8 | public ResultWrapper() { 9 | serviceStatus = new ServiceStatus(); 10 | } 11 | 12 | public void setMessage(String message) { 13 | serviceStatus.message = message; 14 | } 15 | } 16 | 17 | public enum ServiceStatusEnum { 18 | success, failure, warning, information 19 | } 20 | 21 | public class ServiceStatus { 22 | public ServiceStatusEnum status; 23 | public String message; 24 | public DateTime timestamp; 25 | 26 | public ServiceStatus() { 27 | this.status = ServiceStatusEnum.Success; 28 | this.timestamp = DateTime.now(); 29 | } 30 | 31 | public ServiceStatus(String message, ServiceStatusEnum status) { 32 | this.message = message; 33 | this.status = status; 34 | this.timestamp = DateTime.now(); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /graphviz/main/default/aura/SOQLRenderer/SOQLRendererHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | updateEntities: function (component, entities) { 3 | var opts = []; 4 | var from = component.get("v.from"); 5 | entities.forEach(function (entity) { 6 | opts.push({ 7 | label: entity, 8 | value: entity, 9 | selected: entity == from // required to help select dom stay correct when options change. see lightning:select docs race condition 10 | }); 11 | }); 12 | component.set("v.entityOptions", opts); 13 | }, 14 | handleError: function(component, error) { 15 | // the SOQL generation pure fn can throw errors intended to be seen by the user for 16 | // some combinations of entities. handle that here 17 | var isMessageForUser = error.name == "UserException"; 18 | if (isMessageForUser) { 19 | // error intended for the user from pure fns 20 | window.alert(error.message); 21 | } else { 22 | window.alert(error); 23 | } 24 | return isMessageForUser; 25 | } 26 | }) -------------------------------------------------------------------------------- /graphviz/main/default/pages/AutoBuildBridge.page: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 |
13 | 14 | 26 | 27 |
-------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramSettingsChangeEvent/DiagramSettingsChangeEvent.evt: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 9 | 11 | 13 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /graphviz/main/default/aura/Panel/PanelController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Handler when user toggle collapse state 4 | */ 5 | onToggle : function(component, event, helper) { 6 | component.set('v.collapsed', !component.get('v.collapsed')); 7 | }, 8 | 9 | /** 10 | * Handler when user delete the panel 11 | */ 12 | onRemove : function(component, event, helper) { 13 | component.getEvent("onRemovePanel").setParams({scope:'REMOVE'}).fire(); 14 | }, 15 | 16 | /** 17 | * Handler when user enter title edit mode 18 | */ 19 | onEditMode : function(component, event, helper) { 20 | component.set('v.newTitle', component.get('v.title')); 21 | component.set('v.isEditMode', true); 22 | }, 23 | 24 | /** 25 | * Handler when user finish updating title 26 | */ 27 | onBlur : function(component, event, helper) { 28 | component.set('v.isEditMode', false); 29 | helper.handleTitleChange(component, event, helper); 30 | }, 31 | 32 | /** 33 | * Handler when user editing the title 34 | */ 35 | onKeyUp : function(component, event, helper) { 36 | if(event.keyCode === 13){ 37 | component.set('v.isEditMode', false); 38 | helper.handleTitleChange(component, event, helper); 39 | } 40 | }, 41 | }) -------------------------------------------------------------------------------- /graphviz/main/default/aura/ObjectPanel/ObjectPanelHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Calculates the filtered object list and update UI attribute 4 | */ 5 | handleObjectListUpdate : function(component, event, helper){ 6 | var allObjects = component.get('v.allObjects'); 7 | var selectionMap = component.get('v.selectionMap'); 8 | var filteredObjects = []; 9 | var currentState = component.get('v.currentState'); 10 | var term = component.get('v.searchTerm'); 11 | allObjects.forEach(function(object){ 12 | var searchTermExist = term !== undefined && term !== ''; 13 | var searchMatch = (searchTermExist && (object.label.toLowerCase().indexOf(term.toLowerCase()) !== -1 || object.apiName.toLowerCase().indexOf(term.toLowerCase()) !== -1)); 14 | var isSelected = (selectionMap[object.apiName] != null); 15 | if((currentState == 'ALL' && (!searchTermExist || searchMatch)) || (currentState == 'SEARCH' && searchMatch) || (currentState == 'SELECTED' && isSelected && (!searchTermExist || searchMatch))){ 16 | var uiObject = {label:object.label, apiName:object.apiName, selected:isSelected}; 17 | filteredObjects.push(uiObject); 18 | } 19 | }); 20 | filteredObjects.sort(GraphvizForce.DiagramHelper.compare); 21 | component.set('v.filteredObjects', filteredObjects); 22 | }, 23 | }) -------------------------------------------------------------------------------- /graphviz/main/default/aura/AttributePanel/AttributePanel.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 21 |
22 |
-------------------------------------------------------------------------------- /graphviz/main/default/classes/GraphvizForceControllerTest.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class GraphvizForceControllerTest { 3 | 4 | @TestSetup 5 | private static void setupTestData(){ 6 | List testDiagrams = new List(); 7 | for(Integer i=0;i<200;i++){ 8 | Graphviz_Diagram__c diagram = new Graphviz_Diagram__c(); 9 | testDiagrams.add(diagram); 10 | } 11 | insert testDiagrams; 12 | } 13 | 14 | @IsTest 15 | static void testCoverage() { 16 | 17 | Test.startTest(); 18 | List testDiagrams = GraphVizForceController.loadDiagrams(); 19 | System.assertEquals(200, testDiagrams.size()); 20 | 21 | String testSchema = GraphVizForceController.loadSchema(); 22 | System.assertNotEquals(null, testSchema); 23 | 24 | Test.stopTest(); 25 | } 26 | 27 | @IsTest 28 | static void testChildRelationships() { 29 | List wrappers = GraphVizForceController.loadSchemaWrappers(); 30 | for (GraphVizForceController.ObjectWrapper wrapper : wrappers) { 31 | if (wrapper.apiName == 'Case') { 32 | for (GraphVizForceController.ChildRelationshipWrapper childRel : wrapper.childRelationships) { 33 | System.debug(childRel.relationshipName + ' -> '+childRel.childAPIName); 34 | } 35 | } 36 | } 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /doc/ADR/adr0003-node-modules.md: -------------------------------------------------------------------------------- 1 | ## Context 2 | 3 | One of the weaknesses of Salesforce development is the slow save/test feedback cycle 4 | e.g. save a Lightning Component, refresh the page, restore the UI state, confirm the change takes > 20 seconds. 5 | 6 | If code can be executed locally on a developers PC and some kind of UI or text output is useful, feedback can be instant. 7 | This is available in the javascript context when building node.js modules and using the Jasmine-CLI watch feature. 8 | 9 | Two parts of the tool logic are complex and have no dependency on the Lightning framework: 10 | 11 | 1. graphviz rendering from the diagram JSON object 12 | 2. SOQL rendering from the diagram JSON object 13 | 14 | ## Decision 15 | 16 | Build all functions for these two rendering processes as node module and use webpack to combine them into a single static resource. 17 | 18 | For graphviz rendering, tests render artifacts to the local file-system so that a local Graphviz client can watch them and instantly update. 19 | This creates instant graphical feedback for the developer. 20 | 21 | ## Status 22 | 23 | Accepted. Confirmed to work in development and in runtime. 24 | 25 | ## Consequences 26 | 27 | No disadvantages uncovered so far. 28 | 29 | Discovered that the node module will only work in Lightning when deployed as a *UMD* module in *production* mode. 30 | 31 | This makes working on the renderers much more enjoyable and has unlocked our ambitions to add useful features. 32 | 33 | TODO blog post -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramConfigurator/DiagramConfigurator.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 |
-------------------------------------------------------------------------------- /graphviz/main/default/aura/GroupPanel/GroupPanel.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
    11 | 12 | 15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 |

Do you want to remove this group?

25 |
26 | 27 |
-------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramViewer/DiagramViewer.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 11 | 14 | 16 | 17 | 18 | 19 | 20 | 22 | 23 |
24 | 25 |
-------------------------------------------------------------------------------- /graphviz/main/default/staticresources/GraphvizForceUtils.resource: -------------------------------------------------------------------------------- 1 | window.GraphvizForce; 2 | (function(GraphvizForce){ 3 | 4 | var DiagramHelper = (function(){ 5 | return { 6 | compare : function(a,b) { 7 | if (a.label < b.label) 8 | return -1; 9 | if (a.label > b.label) 10 | return 1; 11 | return 0; 12 | }, 13 | 14 | compareName : function(a,b) { 15 | if (a.name < b.name) 16 | return -1; 17 | if (a.name > b.name) 18 | return 1; 19 | return 0; 20 | }, 21 | 22 | // Validate the diagram for persistence 23 | isDiagramValidToPersist: function(diagram){ 24 | var v = new pure.validator(); 25 | var inputValidation = v.validate(diagram, pure.schemas.persisted); 26 | var isValid = inputValidation.errors.length == 0; 27 | if (!isValid) { 28 | console.log(inputValidation.errors); 29 | } 30 | return isValid; 31 | }, 32 | 33 | // Validate the diagram for viewer output 34 | isTranslatedValidToOutput: function(translated){ 35 | var v = new pure.validator(); 36 | var outputValidation = v.validate(translated, pure.schemas.view); 37 | var isValid = outputValidation.errors.length == 0; 38 | return isValid; 39 | }, 40 | }; 41 | })(); 42 | GraphvizForce.DiagramHelper = DiagramHelper; 43 | 44 | })(window.GraphvizForce || ( window.GraphvizForce = {} )); -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramMutateEvent/DiagramMutateEvent.evt: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 9 | 12 | 14 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AppHeader/AppHeader.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | {!v.icon} 14 | 15 |
16 |
17 |
18 |

{!v.title}

19 |

{!v.subTitle}

20 |
21 |
22 | {!v.middleSection} 23 |
24 |
25 | {!v.rightSection} 26 |
27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramViewer/DiagramViewerController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Set initialised when static resources are loaded 4 | */ 5 | doInit: function (component, event, helper) { 6 | component.set('v.initialised', true); 7 | }, 8 | 9 | onContentChange: function (component, event, helper) { 10 | var graphvizContent = component.get('v.graphvizContent'); 11 | var initialised = component.get('v.initialised'); 12 | if (initialised && !$A.util.isEmpty(graphvizContent)) { 13 | 14 | // store the selection and re-use to maintain zoom state 15 | var graphvizSelection = component.get("v.graphvizSelection"); 16 | if ($A.util.isEmpty(graphvizSelection)) { 17 | graphvizSelection = d3.select("#graph").graphviz({useWorker: false}); 18 | component.set("v.graphvizSelection", graphvizSelection); 19 | } 20 | graphvizSelection 21 | .attributer(helper.attributer) 22 | .engine("dot") 23 | .transition(d3.transition("diagram-update").duration(helper.transitionTime()).ease(d3.easeLinear)) 24 | .dot(graphvizContent) 25 | .render() 26 | .zoom(true) 27 | .on("end", function () { 28 | // fire event to allow parent components to use the SVG content from the diagram 29 | var s = component.find("content").getElement(); 30 | component.getEvent('onDiagramRendered').setParams({scope: s.innerHTML}).fire(); 31 | }); 32 | } 33 | }, 34 | 35 | onResetZoom: function (component, event, helper) { 36 | var graphvizSelection = component.get("v.graphvizSelection"); 37 | graphvizSelection.resetZoom(d3.transition("reset").duration(helper.transitionTime()).ease(d3.easeLinear)); 38 | } 39 | }) -------------------------------------------------------------------------------- /doc/diagram-viewer.md: -------------------------------------------------------------------------------- 1 | ## Diagram Viewer Design 2 | 3 | Currently the diagram is rendered by the DiagramViewer component which invokes the d3.js lib 4 | using d3-graphviz to drive it. This works well enough but it is lucky that it works since 5 | the LockerService tends to break 3rd party libs that manipulate the DOM. 6 | 7 | One aspect of this is that the SVG rendered by d3 is set to width -1. 8 | This causes errors in the console but creates the desired behaviours of: 9 | 10 | 1. auto-scaling the svg diagram to fit the window 11 | 2. making d3 transitions smooth 12 | 13 | Using 100% breaks the transition smoothness so we are sticking with -1 for now. 14 | 15 | The correct way to fix this is to measure the width/height of the div containing 16 | the d3 svg and use that in the *attributer* fn. 17 | Currently this doesn't work because the LockerService blocks measuring the size of elements. 18 | 19 | The fix for this is to run the d3 code in a *lightning:container* but that has other problems.... 20 | 21 | ### lightning:container 22 | 23 | The correct way to run d3 is inside a *lightning:container* component. 24 | The [diagram-viewer dir](https://github.com/stevebuik/Graphvizforce-Lightning/tree/master/js/diagram-viewer) contains a prototype of this design and can 25 | be run standalone using a local node.js server. To run this: 26 | 27 | 1. `cd js/diagram-viewer` 28 | 2. `npm install` 29 | 3. in one terminal window `npm run watch` 30 | 4. in another terminal window `./node_modules/.bin/http-server` 31 | 5. open the browser using `http://127.0.0.1:8080` 32 | 33 | Now any change made to index.js or index.html will be reflected instantly on refresh. 34 | 35 | This javascript artifact has the connections for sending a receiving LCC messages but 36 | when run inside a lightning:container it does not work. 37 | Some combination of: 38 | 39 | 1. CSP errors 40 | 2. problems loading the viz.js as a web-worker (disabled) 41 | 3. 42 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: steveb8n/circle-sfdx:latest 6 | steps: 7 | - checkout 8 | - setup_remote_docker: 9 | docker_layer_caching: true 10 | - run: 11 | name: Run node.js pure function tests 12 | working_directory: js/pure 13 | command: | 14 | mkdir -p ~/junit 15 | npm install 16 | ./node_modules/jasmine-node/bin/jasmine-node spec --junitreport --output ~/junit 17 | - run: 18 | name: Create auth token 19 | command: echo $SFDC_AUTH > hubauth.txt 20 | - run: 21 | name: Authenticate to Hub Org (using sfdxurl flow) 22 | command: sfdx force:auth:sfdxurl:store --setdefaultdevhubusername --sfdxurlfile hubauth.txt 23 | - run: 24 | name: Remove auth token 25 | command: rm hubauth.txt 26 | - run: 27 | name: Create Scratch Org 28 | command: sfdx force:org:create --definitionfile config/project-scratch-def.json --durationdays 1 --setdefaultusername --setalias circleci 29 | - run: 30 | name: Install Lightning Testing Service package 31 | command: sfdx force:lightning:test:install 32 | - run: 33 | name: Deploy Source 34 | command: sfdx force:source:push 35 | - run: 36 | name: Run Apex Tests 37 | command: sfdx force:apex:test:run --codecoverage --outputdir ~/junit --resultformat junit --wait 2 38 | - run: 39 | name: Run Lightning Testing Service Tests 40 | command: sfdx force:lightning:test:run --configfile config/lts-config.json --appname DiagramViewerTests.app --resultformat junit --outputdir ~/junit 41 | - store_test_results: 42 | path: ~/junit 43 | - run: 44 | name: Delete Scratch Org (ignoring bash return code) 45 | command: sfdx force:org:delete --noprompt --targetusername circleci || true 46 | when: always 47 | -------------------------------------------------------------------------------- /doc/ADR/adr0002-lightning-out.md: -------------------------------------------------------------------------------- 1 | ## Context 2 | 3 | To create the *Auto-build* feature, access to the Tooling API is required. 4 | There was a [bug that blocked access](https://success.salesforce.com/issues_view?id=a1p3A000000EAUPQA4&title=summer-17-generating-a-session-id-from-lightning-domain-provides-invalid-session-id) to this API from Lightning that was marked as fixed but is not. 5 | 6 | A workaround was found: host the ERD component in a Lightning Out page in a Visualforce page. 7 | The visualforce domains provide a session id that does work with the tooling API. 8 | 9 | This workaround will no longer be needed when the native Apex meta-data api supports Classes and Triggers. 10 | 11 | ## Decision 12 | 13 | Migrated the tab to use a VF page and Lightning Out to init the ERD. 14 | 15 | ## Status 16 | 17 | Accepted. Tested and works except for Toasts. 18 | 19 | Waiting for better meta-data API support in Apex. Currently only Page Layouts supported. 20 | 21 | ## Consequences 22 | 23 | Since the lightning:notificationsLibrary is used to show toasts, this is now broken. 24 | This is because the VF page is run inside an iFrame and cannot send events to the LEX app where toasts are handled. 25 | We will avoid the use of LEX toasts until the above bug is fixed and we can stop using Lightning Out. 26 | 27 | The initial load time of the page is much slower due to Lightning Out bootstrapping itself. 28 | This is not too bad for the user since they can stay on the page and avoid the initialisation delay. 29 | If auto-build is not required, the user can create a *Lightning Component* tab and avoid VF/Lightning Out. This loads much faster. 30 | 31 | Events are not recorded by the Lightning Inspector Chrome plugin. The workaround is to use the TestApp.app to run the app, 32 | which will record events but will stop the auto-build feature from working. 33 | In future, moving the auto-build into a lightning:component will isolate these effects and speed up initial page load. -------------------------------------------------------------------------------- /graphviz/main/default/aura/Modal/Modal.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 27 |
28 |
29 |
-------------------------------------------------------------------------------- /graphviz/main/default/aura/ObjectPanel/ObjectPanel.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 |
-------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildProgress/AutoBuildProgress.cmp: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 12 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 | 32 | Loading Objects and Fields from 33 | {!v.currentSource} 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 |
43 | 44 |
45 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramCard/DiagramCard.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |

14 | {!v.diagram.name} 15 |

16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |
{!v.diagram.entities.length} Object(s)
24 | 25 |
SOQL: {!v.diagram.settings.from}
26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 |

Do you want to delete this diagram?

40 |
41 | 42 |
-------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramOutput/DiagramOutputHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Process copy content action 4 | */ 5 | copyContent: function (component, helper, type) { 6 | var text = type == 'graphvizContent' ? component.get('v.graphvizContent') : component.get('v.svgContent'); 7 | if (text == null) return; 8 | 9 | var success = Core.AuraUtils.copyToClipboard(text); 10 | 11 | if (success) { 12 | window.alert('Copied Successfully.'); 13 | } 14 | }, 15 | 16 | /** 17 | * Process save to local file action 18 | */ 19 | saveToFile: function (label, content) { 20 | var element = document.createElement('a'); 21 | element.setAttribute('href', 'data:svg/plain;charset=utf-8,' + encodeURIComponent(content)); 22 | element.setAttribute('download', label); 23 | element.style.display = 'none'; 24 | document.body.appendChild(element); 25 | element.click(); 26 | document.body.removeChild(element); 27 | }, 28 | 29 | isUserObjectPresent: function(component) { 30 | var diagram = component.get("v.diagram"); 31 | for (var i=0; i < diagram.entities.length; i++) { 32 | if (diagram.entities[i].apiName == "User") { 33 | return true; 34 | } 35 | } 36 | return false; 37 | }, 38 | 39 | /** 40 | * Process diagram rendering 41 | */ 42 | render: function (component) { 43 | var describes = component.get('v.describes'); 44 | var diagram = component.get("v.diagram"); 45 | if (diagram) { 46 | 47 | // Validate diagram and output 48 | var translated; 49 | if(GraphvizForce.DiagramHelper.isDiagramValidToPersist(diagram)){ 50 | translated = pure.graphviz.diagramAsView(diagram, describes); 51 | } else { 52 | window.alert('Diagram is not valid to save'); 53 | } 54 | if(GraphvizForce.DiagramHelper.isTranslatedValidToOutput(translated)){ 55 | var graphvizContent = pure.graphviz.diagramAsText(translated); 56 | component.set('v.graphvizContent', graphvizContent); 57 | } else { 58 | window.alert('Diagram output is not valid'); 59 | } 60 | } 61 | }, 62 | }) -------------------------------------------------------------------------------- /graphviz/main/default/layouts/Graphviz_Diagram__c-Graphviz Diagram Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | true 7 | 8 | 9 | 10 | Readonly 11 | Name 12 | 13 | 14 | Edit 15 | Content__c 16 | 17 | 18 | 19 | 20 | Edit 21 | OwnerId 22 | 23 | 24 | 25 | 26 | 27 | false 28 | false 29 | true 30 | 31 | 32 | 33 | Readonly 34 | CreatedById 35 | 36 | 37 | 38 | 39 | Readonly 40 | LastModifiedById 41 | 42 | 43 | 44 | 45 | 46 | false 47 | false 48 | true 49 | 50 | 51 | 52 | 53 | 54 | false 55 | false 56 | false 57 | false 58 | false 59 | 60 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/ObjectPanel/ObjectPanelController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Sets current state and calculate filtered object list when current state is SEARCH 4 | */ 5 | onSearchState : function(component, event, helper) { 6 | component.set('v.currentState', 'SEARCH'); 7 | component.find("inputSearch").focus(); 8 | helper.handleObjectListUpdate(component, event, helper); 9 | }, 10 | 11 | /** 12 | * Sets current state and calculate filtered object list when current state is ALL 13 | */ 14 | onAllState : function(component, event, helper) { 15 | component.set('v.currentState', 'ALL'); 16 | component.set('v.searchTerm', ''); 17 | component.find("inputSearch").focus(); 18 | helper.handleObjectListUpdate(component, event, helper); 19 | }, 20 | 21 | /** 22 | * Sets current state and calculate filtered object list when current state is SELECTED 23 | */ 24 | onSelectedState : function(component, event, helper) { 25 | component.set('v.currentState', 'SELECTED'); 26 | component.set('v.searchTerm', ''); 27 | component.find("inputSearch").focus(); 28 | helper.handleObjectListUpdate(component, event, helper); 29 | }, 30 | 31 | /** 32 | * Calculate filtered object list with debounce behaviour when user type in search box 33 | */ 34 | onUpdateSearchTerm : function(component, event, helper){ 35 | var searchObjectDebounce = component.get('v.searchObjectDebounce'); 36 | if(searchObjectDebounce == null){ 37 | searchObjectDebounce = Core.SystemUtils.debounce(function() { 38 | // All the taxing stuff you do 39 | helper.handleObjectListUpdate(component, event, helper); 40 | }, 1000); 41 | } 42 | searchObjectDebounce(); 43 | }, 44 | 45 | /** 46 | * App event DiagramUpdatedEvent (type:SELECTION) handler 47 | */ 48 | onDiagramUpdated : function(component, event, helper){ 49 | if(event.getParam('type') == 'SELECTION'){ 50 | component.set('v.searchTerm', ''); 51 | var diagram = component.get('v.diagram'); 52 | var isSelectedObjects = !$A.util.isEmpty(diagram.entities); 53 | component.set('v.currentState', isSelectedObjects ? 'SELECTED' : 'SEARCH'); 54 | component.find("inputSearch").focus(); 55 | helper.handleObjectListUpdate(component, event, helper); 56 | } 57 | }, 58 | }) -------------------------------------------------------------------------------- /graphviz/main/default/objects/Graphviz_Diagram__c/Graphviz_Diagram__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | CancelEdit 9 | Default 10 | 11 | 12 | Clone 13 | Default 14 | 15 | 16 | Delete 17 | Default 18 | 19 | 20 | Edit 21 | Default 22 | 23 | 24 | List 25 | Default 26 | 27 | 28 | New 29 | Default 30 | 31 | 32 | SaveEdit 33 | Default 34 | 35 | 36 | Tab 37 | Default 38 | 39 | 40 | View 41 | Default 42 | 43 | false 44 | SYSTEM 45 | Deployed 46 | false 47 | true 48 | false 49 | false 50 | false 51 | false 52 | true 53 | true 54 | true 55 | 56 | 57 | GD-{0000} 58 | 59 | AutoNumber 60 | 61 | Graphviz Diagrams 62 | 63 | ReadWrite 64 | Public 65 | 66 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/AttributePanel/AttributePanelController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * App event DiagramUpdatedEvent (type:Selection) handler: Clear attribute list when current diagram selection is changed 4 | */ 5 | onDiagramUpdated : function(component, event, helper){ 6 | if(event.getParam('type') == 'SELECTION'){ 7 | component.set('v.attributes', null); 8 | } 9 | }, 10 | 11 | /** 12 | * Populate attribute list when current object is changed 13 | */ 14 | onUpdateAttributes : function(component, event, helper){ 15 | 16 | var allObjects = component.get('v.allObjects'); 17 | var selectionMap = component.get('v.selectionMap'); 18 | var object = component.get('v.object'); 19 | if(object == null){ 20 | component.set('v.attributes', null); 21 | return; 22 | } 23 | 24 | var fields = []; 25 | var attributes = []; 26 | var values = []; 27 | var fieldSelectionMap = selectionMap[object.apiName]; 28 | allObjects.forEach(function(obj){ 29 | if(obj.apiName === object.apiName){ 30 | fields = obj.fields; 31 | return; 32 | } 33 | }); 34 | 35 | fields.forEach(function(field){ 36 | var isSelected = fieldSelectionMap != null && fieldSelectionMap[field.apiName]; 37 | if(isSelected) values.push(field.apiName); 38 | attributes.push({label:field.label, value:field.apiName}); 39 | }); 40 | 41 | // Update attribute list and reset selection 42 | attributes.sort(GraphvizForce.DiagramHelper.compare); 43 | component.set('v.attributes', attributes); 44 | component.set('v.value', values); 45 | }, 46 | 47 | /** 48 | * Fire DiagramMutateEvent when user interact with the attribute list 49 | * TODO: When first field added / all fields removed, the object should be added / removed from the selection map. This will allow the attribute panel to be enabled all the time. 50 | */ 51 | handleChange : function(component, event, helper){ 52 | var value = component.get('v.value'); 53 | var object = component.get('v.object'); 54 | var fieldsMap = {}; 55 | var fields = []; 56 | value.forEach(function(fieldAPIName){ 57 | fields.push({apiName:fieldAPIName}); 58 | }); 59 | fieldsMap[object.apiName] = fields; 60 | component.getEvent('onDiagramMutate').setParams({fieldsMap:fieldsMap, fieldsMode:'OVERWRITE'}).fire(); 61 | }, 62 | }) -------------------------------------------------------------------------------- /graphviz/main/default/classes/ToolingAPIMocks.cls: -------------------------------------------------------------------------------- 1 | global class ToolingAPIMocks implements HttpCalloutMock { 2 | 3 | private Map responses = new Map(); 4 | 5 | // ignoring the request body since tests don't benefit from including it 6 | public class RestRequest { 7 | String method; 8 | String uri; 9 | Map params; 10 | public RestRequest(String method, String uri, Map params) { 11 | this.method = method; 12 | this.uri = uri; 13 | this.params = params; 14 | } 15 | public Integer hashCode() { 16 | String k = '' + method.hashCode() + uri.hashCode() + params.hashCode(); 17 | return k.hashCode(); 18 | } 19 | public Boolean equals(Object other) { 20 | RestRequest otherRequest = (RestRequest) other; 21 | return this.hashCode() == otherRequest.hashCode(); 22 | } 23 | } 24 | 25 | public class RestResponse { 26 | Exception e; 27 | String body; 28 | Integer statusCode; 29 | public RestResponse(Exception e) { 30 | this.e = e; 31 | } 32 | public RestResponse(Integer statusCode, String body) { 33 | this.body = body; 34 | this.statusCode = statusCode; 35 | } 36 | } 37 | 38 | public ToolingAPIMocks(Map responses) { 39 | this.responses = responses; 40 | } 41 | 42 | public static final Integer HTTP_OK = 200; 43 | public static final Integer HTTP_DELETED = 204; 44 | public static final Integer HTTP_CREATED = 201; 45 | 46 | global HttpResponse respond(HttpRequest request) { 47 | 48 | PageReference requestReference = new PageReference(request.getEndpoint()); 49 | Url url = new Url(request.getEndpoint()); 50 | 51 | RestRequest req = new RestRequest(request.getMethod(), url.getPath(), requestReference.getParameters()); 52 | RestResponse res = responses.get(req); 53 | 54 | if (res == NULL) { 55 | throw new System.UnsupportedOperationException('Request not mocked: ' + req); 56 | } else { 57 | if (res.e != NULL) { 58 | throw res.e; 59 | } 60 | 61 | HttpResponse response = new HttpResponse(); 62 | response.setStatusCode(res.statusCode); 63 | if (res.body.length() > 0) { 64 | response.setBody(res.body); 65 | } 66 | return response; 67 | } 68 | } 69 | 70 | 71 | } -------------------------------------------------------------------------------- /graphviz/main/default/staticresources/GVF2TestsJasmine/DiagramViewerTests.js: -------------------------------------------------------------------------------- 1 | describe("Diagram Viewer Tests", function () { 2 | 3 | afterEach(function () { 4 | // Each spec (test) renders its components into the same div, 5 | // so we need to clear that div out at the end of each spec. 6 | $T.clearRenderedTestComponents(); 7 | }); 8 | 9 | describe('Loading', function () { 10 | 11 | it('renders a simple diagram', function (done) { 12 | $T.createComponent("gvf2:DiagramViewer", {}, true) 13 | .then(function (component) { 14 | expect(component.get("v.initialised")).toBe(false); 15 | return $T.waitFor(function () { 16 | if (component.get("v.initialised")) { 17 | return component; 18 | } else { 19 | return false; 20 | } 21 | }) 22 | }) 23 | .then(function (component) { 24 | expect(component.get("v.initialised")).toBe(true); 25 | 26 | var content = 'digraph G { \n' + 27 | 'node [shape=plaintext, fontsize=12]; \n' + 28 | 'edge [arrowhead=crow]; \n' + 29 | 'a [label=< \n' + 30 | ' \n' + 31 | ' \n' + 32 | ' \n' + 33 | '
Object 1
second
third
>]; \n' + 34 | 'b [label=< \n' + 35 | ' \n' + 36 | ' \n' + 37 | ' \n' + 38 | '
Object 2
second
third
>]; \n' + 39 | 'a:c -> b:c; \n' + 40 | '}'; 41 | component.set("v.graphvizContent", content); 42 | 43 | var erdMarkup = component.onContentChange(); 44 | 45 | expect(erdMarkup).not.toBe(null); 46 | 47 | done(); 48 | }).catch(function (e) { 49 | done.fail(e); 50 | }); 51 | }); 52 | }); 53 | }, 10000); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Graphvizforce for Lightning 2 | 3 | A Salesforce Entity Relationship diagramming and visual SOQL builder. 4 | 5 | Status: BETA **Stable**. Needs minor features and bug fixes. 6 | 7 | Distributed as a *managed* package but we will convert to an *unlocked* package as soon as they are available. 8 | 9 | Continuous Integration Build Status: [![CircleCI](https://circleci.com/gh/stevebuik/Graphvizforce-Lightning/tree/master.svg?style=svg)](https://circleci.com/gh/stevebuik/Graphvizforce-Lightning/tree/master) 10 | 11 | # Installation 12 | 13 | Install into a Production or DE Org using [tiny.cc/gvf2p](https://tiny.cc/gvf2p) 14 | 15 | Install into a Sandbox Org using [tiny.cc/gvf2t](https://tiny.cc/gvf2t) 16 | 17 | ## Features 18 | 19 | [Watch the videos](https://stevebuik.github.io/Graphvizforce-Lightning/) 20 | 21 | ## Developers 22 | 23 | If you would like to contribute to this project, we welcome any help. 24 | Please first look at the [issues marked *help wanted*](https://github.com/stevebuik/Graphvizforce-Lightning/issues) and contact us to check if anybody else is already working on it. 25 | 26 | Then [follow these instructions](https://github.com/stevebuik/Graphvizforce-Lightning/tree/master/doc/development.md) to setup your development environment. 27 | 28 | You might also want to clone/setup this project to learn some of the techniques we use. 29 | You can also [read the posts on this blog](http://stevebuikhuizen.online), as we will explain the design in more detail there. 30 | 31 | ## Architecture 32 | 33 | We document our architectural decisions using a standard [Architecture Review Document](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) format. 34 | 35 | These documents can be seen in [the ADR dir](https://github.com/stevebuik/Graphvizforce-Lightning/tree/master/doc/ADR). 36 | 37 | ## Issues 38 | 39 | We manage our roadmap and defects as [Github issues](https://github.com/stevebuik/Graphvizforce-Lightning/issues) 40 | 41 | ## Acknowledgements 42 | 43 | Thanks [Jason Guan](https://www.linkedin.com/in/jason-guan-3463a939/) for working on v1 and initiating the v2. 44 | 45 | Thanks [Andrew Fawcett](https://www.linkedin.com/in/andyfawcett/) for the [UML Canvas](https://github.com/afawcett/apex-umlcanvas) project. We used [2 classes](https://github.com/stevebuik/Graphvizforce-Lightning/blob/master/graphviz/main/default/classes/ToolingAPI.cls) from that project to create the *Auto-Build* feature. 46 | 47 | Thank you if you are using this tool. We volunteer our time for the satisfaction of knowing that people use the tools that we make. 48 | 49 | ## Licence 50 | 51 | Graphvizforce is free and unencumbered public domain software. For more information, see http://unlicense.org/ or the accompanying UNLICENSE file. -------------------------------------------------------------------------------- /graphviz/main/default/aura/AutoBuildProgress/AutoBuildProgressHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | analyzeSource: function (component, source, completeCallback) { 3 | component.set("v.currentSource", source.source); 4 | Core.AuraUtils.execute(component, 'startSource', { 5 | sourceType: component.get("v.sourceType"), 6 | source: JSON.stringify(source) // workaround SFDC bug that fails to deserialize custom classes 7 | }, 8 | this.pollSource(component, source)); 9 | }, 10 | pollSource: function (component, source) { 11 | var self = this; 12 | return $A.getCallback(function (builderUpdate) { 13 | if (builderUpdate.status == 'working') { 14 | Core.AuraUtils.execute(component, 'pollSource', { 15 | sourceType: component.get("v.sourceType"), 16 | // workaround SFDC bug that fails to deserialize custom classes 17 | source: JSON.stringify(source), 18 | prevUpdate: JSON.stringify(builderUpdate) 19 | }, 20 | function (builderUpdate) { 21 | var poll = self.pollSource(component, source); 22 | poll(builderUpdate); 23 | }); 24 | } else { 25 | // continue processing the source list 26 | self.nextSource(component); 27 | 28 | // fire the diagram mutation event to allow the ERD to update UI and persist 29 | var updateDiagramEvent = component.getEvent("onDiagramMutate"); 30 | updateDiagramEvent.setParams({ 31 | entitiesToAdd: builderUpdate.entitiesToAdd, 32 | fieldsMap: builderUpdate.fieldsMap, 33 | fieldsMode: 'MERGE' 34 | }); 35 | updateDiagramEvent.fire(); 36 | } 37 | }); 38 | }, 39 | nextSource: function (component) { 40 | var current = component.get("v.currentSourceIndex"); 41 | var sources = component.get("v.sources"); 42 | var isLastSource = current == sources.length - 1; 43 | if (isLastSource) { 44 | component.set("v.active", false); 45 | component.set("v.currentSource", ''); 46 | component.set("v.progress", 0); 47 | } else { 48 | current++; 49 | component.set("v.currentSourceIndex", current); 50 | component.set("v.currentSource", sources[current].source); 51 | component.set("v.progress", current * 100 / sources.length); 52 | this.analyzeSource(component, sources[current], $A.getCallback(this.nextSource)); 53 | } 54 | } 55 | }) -------------------------------------------------------------------------------- /js/pure/test/diagramViewSamples.js: -------------------------------------------------------------------------------- 1 | // this file contains the JSON schema for a view object i.e. the shape of the data expected by the Mustache template 2 | // and enough valid samples to visually check each feature of the diagram. 3 | // the schema is used to check samples in this file as well as translations from persisted diagram samples. 4 | 5 | var samples = 6 | { 7 | "account_contact": 8 | { 9 | "groups": [{ 10 | "name": "", 11 | "entities": [ 12 | { 13 | "name": "Account", 14 | "id": "Account", 15 | "color": "black", 16 | "fields": [ 17 | { 18 | "name": "Phone", 19 | "id": "Phone", 20 | "type": "String" 21 | }, 22 | { 23 | "name": "Email", 24 | "id": "Email", 25 | "type": "String" 26 | }, 27 | { 28 | "name": "Parent Account", 29 | "id": "Parent", 30 | "type": "Id" 31 | }] 32 | }, 33 | { 34 | "name": "Contact", 35 | "id": "Contact", 36 | "color": "lightgrey", 37 | "fields": [ 38 | { 39 | "name": "Phone", 40 | "id": "Phone", 41 | "type": "String" 42 | }, 43 | { 44 | "name": "Account", 45 | "id": "AccountId", 46 | "type": "Id" 47 | }] 48 | }] 49 | }], 50 | "relationships": [ 51 | { 52 | "from": "Contact", 53 | "field": "AccountId", 54 | "to": "Account", 55 | "style": "solid" 56 | }, 57 | { 58 | "from": "Account", 59 | "field": "Parent", 60 | "to": "Account", 61 | "style": "dashed" 62 | } 63 | 64 | ] 65 | } 66 | }; 67 | 68 | module.exports = { 69 | samples: samples 70 | }; 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /js/diagram-viewer/src/index.js: -------------------------------------------------------------------------------- 1 | import LCC from 'lightning-container'; 2 | import * as d3 from 'd3' 3 | import * as d3Graphviz from 'd3-graphviz'; 4 | 5 | // Register for messages sent by hosting component 6 | LCC.addMessageHandler(lightningMessageHandler); 7 | 8 | // add resize window handler 9 | d3.select(window).on("resize", resizeSVG); 10 | 11 | const transitionTime = 400; 12 | 13 | // global/module state 14 | var graphviz; 15 | 16 | function lightningMessageHandler(message) { 17 | switch (message.type) { 18 | case "render": 19 | if (graphviz) { 20 | render(message.content); 21 | } else { 22 | graphviz = d3.select("#graph").graphviz() 23 | .on("initEnd", function () { 24 | render(message.content); 25 | }); 26 | } 27 | break; 28 | case "reset": 29 | resetZoom(); 30 | break; 31 | default: 32 | throw new Error("Unknown message type: " + message.type); 33 | } 34 | } 35 | 36 | function render(content) { 37 | 38 | graphviz 39 | .attributer(attributer) 40 | .engine("dot") 41 | .transition(d3.transition("diagram-update").duration(transitionTime).ease(d3.easeLinear)) 42 | .dot(content) 43 | .render() 44 | .zoom(true) // must be before "end" handler or zoomBehaviour() is undefined 45 | .on("end", function () { 46 | // notify Lightning of SVG change 47 | LCC.sendMessage({ 48 | type: "svg-update", 49 | svg: d3.select("#graph").selectWithoutDataPropagation("svg").html() 50 | }); 51 | }) 52 | 53 | } 54 | 55 | function attributer(datum, index, nodes) { 56 | var margin = 50; // to avoid scrollbars 57 | var selection = d3.select(this); 58 | if (datum.tag == "svg") { 59 | var width = window.innerWidth; 60 | var height = window.innerHeight; 61 | selection 62 | .attr("width", width) 63 | .attr("height", height) 64 | datum.attributes.width = width - margin; 65 | datum.attributes.height = height - margin; 66 | } 67 | } 68 | 69 | function resetZoom() { 70 | graphviz.resetZoom(d3.transition("reset").duration(transitionTime)); 71 | } 72 | 73 | function resizeSVG() { 74 | var margin = 20; // to avoid scrollbars 75 | var width = window.innerWidth; 76 | var height = window.innerHeight; 77 | var svg = d3.select("#graph").selectWithoutDataPropagation("svg"); 78 | var borderSize = 40; 79 | svg 80 | .attr("width", width - borderSize) 81 | .attr("height", height - borderSize); 82 | var d = svg.datum(); 83 | d.attributes['width'] = width - margin; 84 | d.attributes['height'] = height - margin; 85 | } 86 | 87 | module.exports = 88 | { 89 | lightningMessageHandler: lightningMessageHandler, 90 | reset: resetZoom 91 | }; 92 | 93 | -------------------------------------------------------------------------------- /graphviz/main/default/layouts/Graphviz_AutoBuild__mdt-Graphviz AutoBuild Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | true 7 | 8 | 9 | 10 | Required 11 | MasterLabel 12 | 13 | 14 | Required 15 | DeveloperName 16 | 17 | 18 | Required 19 | Apex_Class__c 20 | 21 | 22 | Edit 23 | Description__c 24 | 25 | 26 | Edit 27 | Lightning_Config_Component__c 28 | 29 | 30 | 31 | 32 | Edit 33 | IsProtected 34 | 35 | 36 | Required 37 | NamespacePrefix 38 | 39 | 40 | 41 | 42 | 43 | false 44 | false 45 | true 46 | 47 | 48 | 49 | Readonly 50 | CreatedById 51 | 52 | 53 | 54 | 55 | Readonly 56 | LastModifiedById 57 | 58 | 59 | 60 | 61 | 62 | false 63 | false 64 | false 65 | 66 | 67 | 68 | false 69 | false 70 | false 71 | false 72 | false 73 | 74 | -------------------------------------------------------------------------------- /docs/index.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | body { 4 | font-family: "Open Sans"; 5 | } 6 | 7 | .header { 8 | padding: 2rem 2rem 0 2rem; 9 | } 10 | 11 | .icons a { 12 | margin-right: 1rem; 13 | } 14 | 15 | .marketing-site-features .row { 16 | background-color: #E6E8EB; 17 | padding: 2rem 1rem 2rem 1rem; 18 | } 19 | 20 | .marketing-site-features { 21 | text-align: center; 22 | padding: 1rem 0 0 0; 23 | } 24 | 25 | .marketing-site-features .fa { 26 | font-size: 2rem; 27 | margin-bottom: 1rem; 28 | color: #1779ba; 29 | } 30 | 31 | .marketing-site-features .marketing-site-features-title { 32 | font-size: 1.125rem; 33 | font-weight: bold; 34 | margin-bottom: 1rem; 35 | } 36 | 37 | @media screen and (min-width: 40em) { 38 | .marketing-site-features { 39 | text-align: left; 40 | } 41 | } 42 | 43 | .header h3 { 44 | text-align: center; 45 | font-family: "Archivo Black"; 46 | margin-bottom: 1rem; 47 | } 48 | 49 | .marketing-site-features-subheadline { 50 | margin-bottom: 2rem; 51 | text-align: center; 52 | } 53 | 54 | .feature { 55 | background-color: white; 56 | padding: 1rem 0 0 1rem; 57 | min-height: 175px; 58 | } 59 | 60 | .feature i { 61 | margin-right: 1rem; 62 | } 63 | 64 | .install.row { 65 | background-color: #d5d5d5; 66 | padding-top: 2rem; 67 | padding-bottom: 2rem; 68 | } 69 | 70 | .install a { 71 | width: 300px; 72 | } 73 | 74 | .content { 75 | background-color: #d5d5d5; 76 | padding-bottom: 5rem; 77 | } 78 | 79 | .button-rounded-hover { 80 | border: 0; 81 | border-radius: 5000px; 82 | padding: 1rem 2rem; 83 | text-transform: uppercase; 84 | position: relative; 85 | overflow: hidden; 86 | font-size: 1rem; 87 | letter-spacing: 2px; 88 | transition: all 0.35s ease; 89 | -webkit-transform: translateZ(0); 90 | transform: translateZ(0); 91 | } 92 | 93 | .button-rounded-hover:before { 94 | opacity: 0; 95 | content: ""; 96 | position: absolute; 97 | top: 0px; 98 | bottom: 0px; 99 | left: 0px; 100 | right: 0px; 101 | border-radius: inherit; 102 | background-color: #fefefe; 103 | transition: all 0.3s; 104 | -webkit-transform: translateX(-100%); 105 | -ms-transform: translateX(-100%); 106 | transform: translateX(-100%); 107 | } 108 | 109 | .button-rounded-hover:after { 110 | position: absolute; 111 | top: 0px; 112 | bottom: 0px; 113 | left: 0px; 114 | right: 0px; 115 | border: 5px solid #115b8d; 116 | content: ''; 117 | border-radius: inherit; 118 | } 119 | 120 | .button-rounded-hover:hover, 121 | .button-rounded-hover:focus { 122 | background-color: #115b8d; 123 | } 124 | 125 | .button-rounded-hover:hover:before, 126 | .button-rounded-hover:focus:before { 127 | -webkit-transform: translateX(0%); 128 | -ms-transform: translateX(0%); 129 | transform: translateX(0%); 130 | opacity: 0.25; 131 | } 132 | 133 | 134 | -------------------------------------------------------------------------------- /graphviz/main/default/permissionsets/Graphvizforce.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GraphvizForce_App 5 | true 6 | 7 | 8 | AutoBuild 9 | true 10 | 11 | 12 | AutoBuildApex 13 | true 14 | 15 | 16 | AutoBuildApexTests 17 | true 18 | 19 | 20 | AutoBuildTrigger 21 | true 22 | 23 | 24 | AutoBuildTriggerTests 25 | true 26 | 27 | 28 | BuilderSource 29 | true 30 | 31 | 32 | BuilderSourceType 33 | true 34 | 35 | 36 | GraphVizForceController 37 | true 38 | 39 | 40 | LightningUtility 41 | true 42 | 43 | 44 | GraphvizForceControllerTest 45 | true 46 | 47 | 48 | ToolingAPI 49 | true 50 | 51 | 52 | ToolingAPIMocks 53 | true 54 | 55 | 56 | ToolingAPITest 57 | true 58 | 59 | 60 | UmlService 61 | true 62 | 63 | 64 | true 65 | Graphviz_Diagram__c.Content__c 66 | true 67 | 68 | false 69 | 70 | 71 | true 72 | true 73 | true 74 | true 75 | true 76 | Graphviz_Diagram__c 77 | true 78 | 79 | 80 | AutoBuildBridge 81 | true 82 | 83 | 84 | Graphviz_Diagrams 85 | Visible 86 | 87 | 88 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/Panel/Panel.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | {!v.icon} 19 |
20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 | {!v.title} 28 |
29 |
30 |
31 | 32 |
33 | 34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 | 42 |
43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 | {!v.body} 51 |
52 |
53 | 54 |
-------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GraphvizForce_App 5 | CustomApplication 6 | 7 | 8 | ActionEvent 9 | AppHeader 10 | AttributePanel 11 | AutoBuildProgress 12 | AutoBuildSelector 13 | AutoBuildStart 14 | DiagramCard 15 | DiagramConfigurator 16 | DiagramMutateEvent 17 | DiagramOutput 18 | DiagramUpdatedEvent 19 | DiagramViewer 20 | DiagramViewerTests 21 | ERDContainer 22 | ERDLightningOut 23 | GroupPanel 24 | Modal 25 | ObjectPanel 26 | Panel 27 | Pill 28 | DiagramSettingsChangeEvent 29 | SOQLRenderer 30 | SOQLRenderTests 31 | TestApp 32 | lts_jasmineRunner 33 | AuraDefinitionBundle 34 | 35 | 36 | AutoBuild 37 | AutoBuildApex 38 | AutoBuildApexTests 39 | AutoBuildTrigger 40 | AutoBuildTriggerTests 41 | BuilderSource 42 | BuilderSourceType 43 | GraphVizForceController 44 | GraphvizForceControllerTest 45 | LightningUtility 46 | ToolingAPI 47 | ToolingAPIMocks 48 | ToolingAPITest 49 | UmlService 50 | ApexClass 51 | 52 | 53 | AutoBuildBridge 54 | ApexPage 55 | 56 | 57 | Graphviz_Diagram__c-Graphviz Diagram Layout 58 | Graphviz_AutoBuild__mdt-Graphviz AutoBuild Layout 59 | Layout 60 | 61 | 62 | Graphviz_Diagram__c 63 | Graphviz_AutoBuild__mdt 64 | GraphvizConfig__c 65 | CustomObject 66 | 67 | 68 | Graphviz_AutoBuild.All_Apex 69 | Graphviz_AutoBuild.Apex_Triggers 70 | CustomMetadata 71 | 72 | 73 | Graphvizforce 74 | PermissionSet 75 | 76 | 77 | GVF2TestsJasmine 78 | GraphvizForceUtils 79 | LightningUtils 80 | pure 81 | VizJS 82 | d3graphviz 83 | d3v4 84 | StaticResource 85 | 86 | 87 | Graphviz_Diagrams 88 | CustomTab 89 | 90 | 91 | GraphvizForce_App_UtilityBar 92 | FlexiPage 93 | 94 | 41.0 95 | -------------------------------------------------------------------------------- /js/diagram-viewer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DiagramViewer 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/SOQLRenderer/SOQLRendererController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | doInit: function (component, event, helper) { 3 | component.set("v.ready", true); 4 | }, 5 | fromChange: function (component, event, helper) { 6 | 7 | var newFrom = event.getParam("value"); 8 | 9 | var obscured = {}; 10 | 11 | try { 12 | // update the UI, generating new SOQL if required 13 | if ($A.util.isEmpty(component.get("v.from"))) { 14 | component.set("v.prompt", "Choose a SOQL FROM object:"); 15 | component.set("v.fromStyle", ""); 16 | } else { 17 | var rendered = window.pure.soql.v2.diagramAsSelects(component.get("v.diagram"), component.get("v.describes"), newFrom); 18 | var soql = window.pure.soql.v2.diagramSelectsAsSOQL(rendered.selectLists, newFrom, false); 19 | component.set("v.fromStyle", "border: solid 2px " + window.pure.graphviz.entityFrom + ";padding:3px;border-radius:7px;"); 20 | component.set("v.prompt", "FROM") 21 | component.set("v.soql", soql); 22 | component.set("v.selectLists", rendered.selectLists); 23 | 24 | // populate the obscured entities 25 | rendered.entities.forEach(function (entity) { 26 | obscured[entity] = true; 27 | }) 28 | for (var key in rendered.selectedFields) { 29 | delete obscured[key]; 30 | } 31 | } 32 | 33 | // translate obscured into a list 34 | var obscuredList = []; 35 | for (var e in obscured) { 36 | obscuredList.push(e); 37 | } 38 | 39 | // now fire the component event to notify parent components 40 | component.getEvent('onSettingsChange') 41 | .setParams({ 42 | from: newFrom, 43 | obscuredEntities: obscuredList 44 | }).fire(); 45 | 46 | } catch (error) { 47 | var wasUserError = helper.handleError(component, error); 48 | if (wasUserError) { 49 | component.set("v.from", undefined); 50 | } 51 | } 52 | }, 53 | diagramChange: function (component, event, helper) { 54 | try { 55 | var newDiagram = event.getParam("value"); 56 | component.set("v.from", newDiagram.settings.from); 57 | if ($A.util.isEmpty(component.get("v.from"))) { 58 | // load entities from diagram but don't generate SOQL 59 | var entities = window.pure.soql.v2.entities(event.getParam("value")); 60 | helper.updateEntities(component, entities); 61 | } else { 62 | var rendered = window.pure.soql.v2.diagramAsSelects(newDiagram, component.get("v.describes"), component.get("v.from")); 63 | helper.updateEntities(component, rendered.entities); 64 | var soql = window.pure.soql.v2.diagramSelectsAsSOQL(rendered.selectLists, component.get("v.from"), false); 65 | component.set("v.soql", soql); 66 | component.set("v.selectLists", rendered.selectLists); 67 | } 68 | } catch (error) { 69 | var wasUserError = helper.handleError(component, error); 70 | if (wasUserError) { 71 | component.set("v.from", undefined); 72 | } 73 | } 74 | }, 75 | handleClearClick: function (component, event, helper) { 76 | component.set("v.from", undefined); 77 | }, 78 | handleCopyClick: function (component, event, helper) { 79 | var success = Core.AuraUtils.copyToClipboard(component.get("v.soql")); 80 | var c = component.find("copied"); 81 | if (!success) { 82 | window.alert("Copy to clipboard failed. Try another browser?"); 83 | } 84 | 85 | c.getElement().setAttribute("style", "opacity: 1;") 86 | setTimeout($A.getCallback(function () { 87 | c.getElement().setAttribute("style", "opacity: 0;") 88 | }), 1000); 89 | }, 90 | handleSOQLClick: function (component, event, helper) { 91 | component.set("v.mode", "soql"); 92 | }, 93 | handlePreviewClick: function (component, event, helper) { 94 | component.set("v.mode", "preview"); 95 | } 96 | }) -------------------------------------------------------------------------------- /graphviz/main/default/staticresources/LightningUtils.resource: -------------------------------------------------------------------------------- 1 | window.Core; 2 | (function(Core){ 3 | 4 | var SystemUtils = (function(){ 5 | return { 6 | /* 7 | Returns a function, that, as long as it continues to be invoked, will not be triggered. 8 | The function will be called after it stops being called for N milliseconds. 9 | If 'immediate' is passed, trigger the function on the leading edge, instead of the trailing. 10 | Reference: https://davidwalsh.name/javascript-debounce-function 11 | */ 12 | debounce: function(func, wait, immediate) { 13 | var timeout; 14 | return function() { 15 | var context = this, args = arguments; 16 | var later = function() { 17 | timeout = null; 18 | if (!immediate) func.apply(context, args); 19 | }; 20 | var callNow = immediate && !timeout; 21 | clearTimeout(timeout); 22 | timeout = setTimeout(later, wait); 23 | if (callNow) func.apply(context, args); 24 | }; 25 | }, 26 | }; 27 | })(); 28 | Core.SystemUtils = SystemUtils; 29 | 30 | var AuraUtils = (function(){ 31 | return { 32 | execute: function(component, actionName, params, callback) { 33 | var action = component.get('c.' + actionName); 34 | if(params != null) action.setParams(params); 35 | action.setCallback(this, function(a) { 36 | if (a.getState() === "SUCCESS"){ 37 | var result = a.getReturnValue(); 38 | callback(result); 39 | } 40 | else if (a.getState() === "ERROR"){ 41 | var messages = []; 42 | Core.AuraUtils.findErrorMessages(a.getError(), messages); 43 | alert('Server error : ' + messages); 44 | } 45 | 46 | }); 47 | $A.enqueueAction(action); 48 | }, 49 | findErrorMessages : function (obj, result){ 50 | for (var prop in obj) { 51 | var value = obj[prop]; 52 | if (typeof value === 'object') { 53 | Core.AuraUtils.findErrorMessages(value, result); 54 | }else { 55 | if (prop === 'message') 56 | result.push(value); 57 | } 58 | } 59 | }, 60 | copyToClipboard(text) { 61 | // Copies a string to the clipboard. Must be called from within an 62 | // event handler such as click. May return false if it failed, but 63 | // this is not always possible. Browser support for Chrome 43+, 64 | // Firefox 42+, Safari 10+, Edge and IE 10+. 65 | // IE: The clipboard feature may be disabled by an administrator. By 66 | // default a prompt is shown the first time the clipboard is 67 | // used (per session). 68 | if (window.clipboardData && window.clipboardData.setData) { 69 | // IE specific code path to prevent textarea being shown while dialog is visible. 70 | return clipboardData.setData("Text", text); 71 | 72 | } else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { 73 | var textarea = document.createElement("textarea"); 74 | textarea.textContent = text; 75 | textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge. 76 | document.body.appendChild(textarea); 77 | textarea.select(); 78 | try { 79 | return document.execCommand("copy"); // Security exception may be thrown by some browsers. 80 | } catch (ex) { 81 | console.warn("Copy to clipboard failed.", ex); 82 | return false; 83 | } finally { 84 | document.body.removeChild(textarea); 85 | } 86 | } 87 | } 88 | }; 89 | })(); 90 | Core.AuraUtils = AuraUtils; 91 | 92 | })(window.Core || ( window.Core = {} )); -------------------------------------------------------------------------------- /graphviz/main/default/classes/AutoBuildTrigger.cls: -------------------------------------------------------------------------------- 1 | public without sharing class AutoBuildTrigger implements AutoBuild.AutoBuilder { 2 | 3 | public List getSources() { 4 | List results = new List(); 5 | 6 | try { 7 | // need to use Tooling API since Apex doesn't expose ApexTrigger in SOQL 8 | ToolingAPI.QueryResult queryResult = new ToolingAPI().query('select Id, Name, BodyCRC from ApexTrigger'); 9 | if (queryResult.records != NULL && !queryResult.records.isEmpty()) { 10 | addTriggers(results, queryResult.records); 11 | } 12 | while (!queryResult.done) { 13 | queryResult = new ToolingAPI().queryMore(queryResult.nextRecordsUrl); 14 | if (queryResult.records != NULL && !queryResult.records.isEmpty()) { 15 | addTriggers(results, queryResult.records); 16 | } 17 | } 18 | 19 | return results; 20 | } catch (ToolingApi.ToolingAPIAuthorizationException tapiae) { 21 | throw UmlService.makeException(tapiae); 22 | } 23 | } 24 | 25 | private void addTriggers(List sources, List triggers) { 26 | Double managedSize = -1; 27 | for (ToolingAPI.SObject_x aTrigger : triggers) { 28 | ToolingAPI.ApexTrigger theTrigger = (ToolingAPI.ApexTrigger) aTrigger; 29 | if (theTrigger.bodyCrc != managedSize) { 30 | sources.add(new BuilderSource(theTrigger.name, theTrigger.id)); 31 | } 32 | } 33 | } 34 | 35 | private Class Payload { 36 | String requestId; 37 | } 38 | 39 | public AutoBuild.BuilderUpdate startSource(BuilderSource source) { 40 | AutoBuild.BuilderUpdate result = new AutoBuild.BuilderUpdate(); 41 | // kick off the compile request 42 | ToolingApi.ContainerAsyncRequest request = UmlService.compileTrigger(source.id); 43 | 44 | Payload payload = new Payload(); 45 | payload.requestId = request.id; 46 | 47 | result.payload = JSON.serialize(payload); 48 | result.status = AutoBuild.STATUS_WORKING; 49 | return result; 50 | } 51 | 52 | public AutoBuild.BuilderUpdate pollSource(BuilderSource source, AutoBuild.BuilderUpdate prevUpdate) { 53 | AutoBuild.BuilderUpdate result = new AutoBuild.BuilderUpdate(); 54 | Payload payload = (Payload) JSON.deserialize(prevUpdate.payload, Payload.class); 55 | ToolingApi.ContainerAsyncRequest request = UmlService.containerAsyncRequest(payload.requestId); 56 | result.status = request.state; 57 | if (request.state == 'Completed') { 58 | 59 | ToolingApi.SymbolTable symbolTable = UmlService.symbolTableTrigger(request.metadataContainerId, source.source); 60 | 61 | AutoBuild.DescribeData describes = AutoBuild.getDataModelDescribe(); 62 | 63 | // now check every reference and filter only object/field refs. add them to the result 64 | for (ToolingAPI.ExternalReference reference : symbolTable.externalReferences) { 65 | 66 | Boolean isReferenceToSObject = describes.objectLabelsByAPIName.containsKey(reference.name); 67 | if (isReferenceToSObject) { // only object references are returned 68 | Map entity = new Map(); 69 | entity.put('apiName', reference.name); 70 | entity.put('label', describes.objectLabelsByAPIName.get(reference.name)); 71 | result.entitiesToAdd.add(entity); 72 | 73 | List> fields = new List>(); 74 | for (ToolingAPI.ExternalSymbol symbol : reference.variables) { 75 | fields.add(new Map{ 76 | 'apiName' => symbol.name, 77 | 'label' => describes.fieldTuplesByAPIName.get(reference.name).get(symbol.name) 78 | }); 79 | } 80 | result.fieldsMap.put(reference.name, fields); 81 | } 82 | } 83 | 84 | result.status = AutoBuild.STATUS_COMPLETE; 85 | } else { 86 | result.payload = prevUpdate.payload; // pass back to client for use in next poll/update request 87 | result.status = AutoBuild.STATUS_WORKING; 88 | } 89 | return result; 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /js/pure/src/schemas.js: -------------------------------------------------------------------------------- 1 | // this schema was generated (and manually adjusted) from the sample using https://www.liquid-technologies.com/online-json-to-schema-converter 2 | var persisted = 3 | { 4 | "$schema": "http://json-schema.org/draft-04/schema#", 5 | "type": "object", 6 | "properties": { 7 | "name": {"type": "string"}, 8 | "id": {"type": "string"}, 9 | "entities": { 10 | "type": "array", 11 | "items": { 12 | "type": "object", 13 | "properties": { 14 | "apiName": {"type": "string"}, 15 | "fields": { 16 | "type": "array", 17 | "items": { 18 | "type": "object", 19 | "properties": {"apiName": {"type": "string"}}, 20 | "required": ["apiName"] 21 | } 22 | } 23 | }, 24 | "required": ["apiName", "fields"] 25 | } 26 | }, 27 | "groups": { 28 | "type": "array", 29 | "items": { 30 | "type": "object", 31 | "properties": { 32 | "name": {"type": "string"}, 33 | "entities": { 34 | "type": "array", 35 | "items": {"type": "string"} 36 | } 37 | }, 38 | "required": ["name", "entities"] 39 | } 40 | }, 41 | "settings": { 42 | "type": "object", 43 | "properties": { 44 | "showSelfRelations": {"type": "boolean"}, 45 | "showStandardUserRelationships": {"type": "boolean"}, 46 | "from": {"type": "string"}, 47 | "layout": { 48 | "type": "string", 49 | "enum": ["TD", "LR"] 50 | }, 51 | "obscureEntities": { 52 | "type": "array", 53 | "items": {"type": "string"} 54 | } 55 | }, 56 | "required": [] 57 | } 58 | }, 59 | "required": ["name", "entities", "groups", "settings"] 60 | }; 61 | 62 | // this schema was manually built 63 | var view = { 64 | "type": "object", 65 | "properties": { 66 | "groups": 67 | { 68 | "type": "array", "required": true, 69 | "items": { 70 | "type": "object", 71 | "properties": { 72 | "name": {"type": "String", "required": true}, 73 | "entities": { 74 | "type": "array", 75 | "required": true, 76 | "items": { 77 | "type": "object", 78 | "properties": { 79 | "name": {"type": "String", "required": true}, 80 | "id": {"type": "String", "required": true}, 81 | "color": {"type": "String", "required": true}, 82 | "fields": { 83 | "type": "array", 84 | "required": true, 85 | "items": { 86 | "type": "object", 87 | "properties": { 88 | "name": {"type": "String", "required": true}, 89 | "id": {"type": "String", "required": true}, 90 | "type": {"type": "String", "required": true}, 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | }, 100 | "relationships": { 101 | "type": "array", "required": true, 102 | "items": { 103 | "type": "object", 104 | "properties": { 105 | "from": {"type": "string", "required": true}, 106 | "field": {"type": "string", "required": true}, 107 | "to": {"type": "string", "required": true}, 108 | "style": {"type": "string", "required": true}, 109 | } 110 | } 111 | } 112 | } 113 | }; 114 | 115 | module.exports = { 116 | persisted: persisted, 117 | view: view 118 | }; -------------------------------------------------------------------------------- /graphviz/main/default/classes/AutoBuild.cls: -------------------------------------------------------------------------------- 1 | public with sharing class AutoBuild { 2 | 3 | public static final String STATUS_WORKING = 'working'; 4 | public static final String STATUS_COMPLETE = 'complete'; 5 | 6 | @AuraEnabled 7 | public static List getSourceTypes() { 8 | return getTypes(); 9 | } 10 | 11 | @AuraEnabled 12 | public static List getSources(String sourceType) { 13 | try { 14 | return getBuilder(sourceType).getSources(); 15 | } catch (UmlService.UmlServiceException se) { 16 | throw handleToolingAPIException(se); 17 | } 18 | } 19 | 20 | @AuraEnabled 21 | public static BuilderUpdate startSource(String sourceType, String source) { 22 | try { 23 | BuilderSource builderSource = (BuilderSource) JSON.deserialize(source, BuilderSource.class); 24 | return getBuilder(sourceType).startSource(builderSource); 25 | } catch (UmlService.UmlServiceException se) { 26 | throw handleToolingAPIException(se); 27 | } 28 | } 29 | 30 | @AuraEnabled 31 | public static BuilderUpdate pollSource(String sourceType, String source, String prevUpdate) { 32 | try { 33 | BuilderSource builderSource = (BuilderSource) JSON.deserialize(source, BuilderSource.class); 34 | BuilderUpdate builderUpdate = (BuilderUpdate) JSON.deserialize(prevUpdate, BuilderUpdate.class); 35 | return getBuilder(sourceType).pollSource(builderSource, builderUpdate); 36 | } catch (UmlService.UmlServiceException se) { 37 | throw handleToolingAPIException(se); 38 | } 39 | } 40 | 41 | private static AuraHandledException handleToolingAPIException(UmlService.UmlServiceException e) { 42 | String userMessage = e.getMessage(); 43 | return new AuraHandledException(userMessage); 44 | } 45 | 46 | private static AutoBuilder getBuilder(String sourceType) { 47 | List types = getTypes(); 48 | for (BuilderSourceType t : types) { 49 | if (t.sourceType == sourceType) { 50 | AutoBuilder builder = (AutoBuilder) System.Type.forName(t.apexClass).newInstance(); 51 | return builder; 52 | } 53 | } 54 | throw new UnsupportedOperationException('No type matched: ' + sourceType); 55 | } 56 | 57 | private static List getTypes() { 58 | List results = new List(); 59 | for (Graphviz_AutoBuild__mdt autoBuild : [ 60 | SELECT Label, DeveloperName, Apex_Class__c, Lightning_Config_Component__c, Description__c 61 | FROM Graphviz_AutoBuild__mdt 62 | ]) { 63 | results.add(new BuilderSourceType(autoBuild)); 64 | } 65 | return results; 66 | } 67 | 68 | public interface AutoBuilder { 69 | List getSources(); 70 | BuilderUpdate startSource(BuilderSource source); 71 | BuilderUpdate pollSource(BuilderSource source, BuilderUpdate prevUpdate); 72 | } 73 | 74 | public Class BuilderUpdate { 75 | 76 | @AuraEnabled 77 | public String status; 78 | 79 | @AuraEnabled 80 | public String payload; 81 | 82 | @AuraEnabled 83 | public List> entitiesToAdd = new List>(); 84 | 85 | @AuraEnabled 86 | public Map>> fieldsMap = new Map>>(); 87 | 88 | public BuilderUpdate() { 89 | } 90 | public BuilderUpdate(String status, String payload) { 91 | this.status = status; 92 | this.payload = payload; 93 | } 94 | } 95 | 96 | // data returned to auto-builder impl classes when they need to compare results against the data model 97 | public Class DescribeData { 98 | public Map objectLabelsByAPIName = new Map(); 99 | public Map> fieldTuplesByAPIName = new Map>(); 100 | } 101 | 102 | public static DescribeData getDataModelDescribe() { 103 | DescribeData result = new DescribeData(); 104 | 105 | for (Schema.SObjectType sObjType : Schema.getGlobalDescribe().values()) { 106 | DescribeSObjectResult describe = sObjType.getDescribe(); 107 | // store object names/labels 108 | result.objectLabelsByAPIName.put(describe.name, describe.label); 109 | // store fields 110 | Map fields = describe.fields.getMap(); 111 | Map fieldLabelsByAPIName = new Map(); 112 | for (String field : fields.keySet()) { 113 | fieldLabelsByAPIName.put( 114 | fields.get(field).getDescribe().name, 115 | fields.get(field).getDescribe().label); 116 | } 117 | result.fieldTuplesByAPIName.put(describe.name, fieldLabelsByAPIName); 118 | } 119 | return result; 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /graphviz/main/default/classes/AutoBuildApex.cls: -------------------------------------------------------------------------------- 1 | public without sharing class AutoBuildApex implements AutoBuild.AutoBuilder { 2 | 3 | // a set of class names that are not used as field/object sources in normal operation 4 | private final Set excluded = new Set{ 5 | 'AutoBuild', 'AutoBuildApex', 'AutoBuildApexTests', 'AutoBuildDiagram', 'AutoBuildTrigger', 6 | 'AutoBuildTriggerTests', 'BuilderSource', 'BuilderSourceType', 'GraphVizForceController', 7 | 'GraphvizForceControllerTest', 'LightningUtility', 'ToolingAPI', 'ToolingAPITest', 'ToolingAPIMocks', 'UmlService', 8 | 'egServerSideActionController', 'egServerSideActionControllerTest' 9 | }; 10 | 11 | public List getSources() { 12 | 13 | List results = new List(); 14 | 15 | Map config = GraphVizForceController.getConfig(); 16 | System.debug('Config:' + config); 17 | 18 | Double managedSize = -1; 19 | 20 | if (config.get('development mode') == NULL) { 21 | for (List classes : [ 22 | SELECT Id, Name, BodyCrc 23 | FROM ApexClass 24 | WHERE Name <> :excluded 25 | ORDER BY NAME 26 | ]) { 27 | for (ApexClass clazz : classes) { 28 | if (clazz.BodyCrc != managedSize) { 29 | results.add(new BuilderSource(clazz.Name, clazz.Id)); 30 | } 31 | } 32 | } 33 | } else { 34 | for (List classes : [ 35 | SELECT Id, Name,BodyCrc 36 | FROM ApexClass 37 | ORDER BY NAME 38 | ]) { 39 | for (ApexClass clazz : classes) { 40 | if (clazz.BodyCrc != managedSize) { 41 | results.add(new BuilderSource(clazz.Name, clazz.Id)); 42 | } 43 | } 44 | } 45 | } 46 | return results; 47 | } 48 | 49 | private Class Payload { 50 | String requestId; 51 | } 52 | 53 | public AutoBuild.BuilderUpdate startSource(BuilderSource source) { 54 | AutoBuild.BuilderUpdate result = new AutoBuild.BuilderUpdate(); 55 | // kick off the compile request 56 | ToolingApi.ContainerAsyncRequest request = UmlService.compile(source.source); 57 | 58 | Payload payload = new Payload(); 59 | payload.requestId = request.id; 60 | 61 | result.payload = JSON.serialize(payload); 62 | result.status = AutoBuild.STATUS_WORKING; 63 | return result; 64 | } 65 | 66 | public AutoBuild.BuilderUpdate pollSource(BuilderSource source, AutoBuild.BuilderUpdate prevUpdate) { 67 | AutoBuild.BuilderUpdate result = new AutoBuild.BuilderUpdate(); 68 | Payload payload = (Payload) JSON.deserialize(prevUpdate.payload, Payload.class); 69 | ToolingApi.ContainerAsyncRequest request = UmlService.containerAsyncRequest(payload.requestId); 70 | 71 | if (request == NULL) { 72 | System.debug('Empty async request'); 73 | result.status = AutoBuild.STATUS_COMPLETE; 74 | } else { 75 | 76 | result.status = request.state; 77 | if (request.state == 'Completed') { 78 | 79 | System.debug('reading completed symbol table for class: ' + source.source); 80 | ToolingApi.SymbolTable symbolTable = UmlService.symbolTable(request.metadataContainerId, source.source); 81 | 82 | AutoBuild.DescribeData describes = AutoBuild.getDataModelDescribe(); 83 | 84 | // now check every reference and filter only object/field refs. add them to the result 85 | for (ToolingAPI.ExternalReference reference : symbolTable.externalReferences) { 86 | 87 | Boolean isReferenceToSObject = describes.objectLabelsByAPIName.containsKey(reference.name); 88 | if (isReferenceToSObject) { // only object references are returned 89 | Map entity = new Map(); 90 | entity.put('apiName', reference.name); 91 | entity.put('label', describes.objectLabelsByAPIName.get(reference.name)); 92 | result.entitiesToAdd.add(entity); 93 | 94 | List> fields = new List>(); 95 | for (ToolingAPI.ExternalSymbol symbol : reference.variables) { 96 | fields.add(new Map{ 97 | 'apiName' => symbol.name, 98 | 'label' => describes.fieldTuplesByAPIName.get(reference.name).get(symbol.name) 99 | }); 100 | } 101 | result.fieldsMap.put(reference.name, fields); 102 | } 103 | } 104 | 105 | result.status = AutoBuild.STATUS_COMPLETE; 106 | } else { 107 | result.payload = prevUpdate.payload; // pass back to client for use in next poll/update request 108 | result.status = AutoBuild.STATUS_WORKING; 109 | } 110 | } 111 | 112 | return result; 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /graphviz/main/default/aura/ERDContainer/ERDContainerController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * App/Container initialisation entry point, fires first server call to fetch full schema 4 | */ 5 | doInit: function (component, event, helper) { 6 | // Disable user guide 7 | window.showUserGuide = false; 8 | $A.util.toggleClass(component.find("mySpinner"), "slds-hide"); 9 | helper.loadSchema(component, event, helper); 10 | helper.loadDiagrams(component, event, helper); 11 | }, 12 | 13 | /** 14 | * Handler when diagram is being mutated 15 | */ 16 | onDiagramMutate: function (component, event, helper) { 17 | var entitiesToAdd = event.getParam('entitiesToAdd'); 18 | var entitiesToRemove = event.getParam('entitiesToRemove'); 19 | var fieldsMap = event.getParam('fieldsMap'); 20 | var fieldsMode = event.getParam('fieldsMode'); 21 | helper.handleDiagramMutate(component, helper, entitiesToAdd, entitiesToRemove, fieldsMap, fieldsMode); 22 | }, 23 | 24 | onSettingsChange: function (component, event, helper) { 25 | var selectedDiagram = component.get('v.selectedDiagram'); 26 | if (!$A.util.isEmpty(selectedDiagram)) { // protect against events firing during initial list/load time 27 | 28 | // TODO why is this required when other settings are persisted without code here 29 | var showUserRefs = event.getParam('showStandardUserRelations'); 30 | if (!$A.util.isUndefined(showUserRefs)) { 31 | selectedDiagram.settings.showStandardUserRelationships = showUserRefs; 32 | } 33 | 34 | // update diagram data 35 | component.set('v.selectedDiagram', selectedDiagram); 36 | // update diagrams list data 37 | component.set('v.diagrams', helper.propagateDiagramList(component.get('v.diagrams'), selectedDiagram)); 38 | 39 | // Persist the diagram to server 40 | helper.handlePersistDiagramData(component, selectedDiagram); 41 | 42 | // Dispatch DiagramUpdatedEvent to subscribers 43 | $A.get("e.gvf2:DiagramUpdatedEvent").setParams({type: 'MUTATION'}).fire(); 44 | } 45 | }, 46 | 47 | /** 48 | * Handler when user clicks on reload schema button 49 | */ 50 | onReloadSchema: function (component, event, helper) { 51 | $A.util.toggleClass(component.find("mySpinner"), "slds-hide"); 52 | helper.loadSchema(component, event, helper); 53 | }, 54 | 55 | /** List View Functions **/ 56 | /** 57 | * Handler when user changes the selection of current diagram 58 | */ 59 | onSelectDiagram: function (component, event, helper) { 60 | // Reset UI elements 61 | component.find('diagramConfigurator').find('objectPanel').set('v.searchTerm', ''); 62 | component.find('diagramConfigurator').set('v.selectedObject', null); 63 | 64 | var diagram = event.getParam('scope'); 65 | component.set('v.selectedDiagram', diagram); 66 | component.set('v.selectionMap', helper.getUpdatedSelectionMap(diagram)); 67 | 68 | // Dispatch DiagramUpdatedEvent to subscribers 69 | $A.get("e.gvf2:DiagramUpdatedEvent").setParams({type: 'SELECTION'}).fire(); 70 | 71 | component.set('v.currentState', 'DETAIL'); 72 | }, 73 | 74 | /** 75 | * Handler when user adds a new diagram 76 | */ 77 | onAddNewDiagram: function (component, event, helper) { 78 | helper.handleAddDiagram(component, event, helper); 79 | component.set('v.newDiagramName', ''); 80 | }, 81 | 82 | /** 83 | * Handler when user deletes a diagram 84 | */ 85 | onRemoveDiagram: function (component, event, helper) { 86 | var diagrams = component.get('v.diagrams'); 87 | var diagramToRemove = event.getParam('scope'); 88 | diagrams.forEach(function (diagram, index) { 89 | if (diagram.name === diagramToRemove.name) { 90 | diagrams.splice(index, 1); 91 | component.set('v.diagrams', diagrams); 92 | return; 93 | } 94 | }); 95 | 96 | // Delete diagram via apex controller 97 | helper.handleRemoveDiagram(component, diagramToRemove.id); 98 | }, 99 | 100 | /** 101 | * Handler when user switch current state back to list mode 102 | */ 103 | onBackToList: function (component, event, helper) { 104 | component.set('v.currentState', 'LIST'); 105 | }, 106 | 107 | /** 108 | * Handler when user clone a diagram 109 | */ 110 | onCloneDiagram: function (component, event, helper) { 111 | helper.handleCloneDiagram(component, event, helper); 112 | component.set('v.cloneDiagramName', ''); 113 | }, 114 | 115 | /** 116 | * Handler when user toggles the preview mode 117 | */ 118 | onTogglePreview: function (component, event, helper) { 119 | var isExpanded = event.getParam('scope'); 120 | component.set('v.isShowDiagramConfigurator', !isExpanded); 121 | }, 122 | 123 | /** 124 | * Handler when user starts auto build process 125 | */ 126 | onAutoBuildStart: function (component, event, helper) { 127 | var progress = component.find("autoBuildProgress"); 128 | progress.set("v.diagramId", component.get("v.selectedDiagram").recordId); 129 | progress.set("v.sourceType", event.getParam("type")); 130 | progress.start(); 131 | } 132 | 133 | }) -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramOutput/DiagramOutputController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | /** 3 | * Update initialised variable to indicate required static resources are loaded and ready 4 | */ 5 | doInit: function (component, event, helper) { 6 | component.set('v.initialised', true); 7 | }, 8 | 9 | /** 10 | * Handler when user toggles the current state of view 11 | */ 12 | onToggleState: function (component, event, helper) { 13 | var isExpanded = !component.get('v.isExpanded'); 14 | component.set('v.isExpanded', isExpanded); 15 | component.getEvent('onTogglePreview').setParams({scope: isExpanded}).fire(); 16 | }, 17 | 18 | /** 19 | * Handler when user toggles self relationships 20 | */ 21 | onToggleSelf: function (component, event, helper) { 22 | var diagram = component.get("v.diagram"); 23 | var oldValue = diagram.settings.showSelfRelations; 24 | var newValue = $A.util.isUndefined(oldValue) ? true : !oldValue; 25 | diagram.settings.showSelfRelations = newValue; 26 | component.set("v.diagram", diagram); 27 | component.getEvent('onSettingsChange').setParams({showSelfRelations: newValue}).fire(); 28 | }, 29 | 30 | /** 31 | * App event DiagramUpdatedEvent handler: when diagram data is updated, re-render the graphviz diagram 32 | * Handler when user toggles user relationship fields 33 | */ 34 | onToggleStdUser: function (component, event, helper) { 35 | var diagram = component.get("v.diagram"); 36 | var oldValue = diagram.settings.showStandardUserRelations; 37 | if (helper.isUserObjectPresent(component)) { 38 | var newValue = $A.util.isUndefined(oldValue) ? true : !oldValue; 39 | diagram.settings.showStandardUserRelations = newValue; 40 | component.set("v.diagram", diagram); 41 | component.getEvent('onSettingsChange').setParams({showStandardUserRelations: newValue}).fire(); 42 | } else { 43 | window.alert("User relationships require the User object in the diagram!") 44 | } 45 | }, 46 | 47 | onToggleAPINames: function (component, event, helper) { 48 | var diagram = component.get("v.diagram"); 49 | var oldValue = diagram.settings.showAPINames; 50 | var newValue = $A.util.isUndefined(oldValue) ? true : !oldValue; 51 | diagram.settings.showAPINames = newValue; 52 | component.set("v.diagram", diagram); 53 | component.getEvent('onSettingsChange').setParams({showAPINames: newValue}).fire(); 54 | }, 55 | 56 | onToggleLayout: function (component, event, helper) { 57 | var diagram = component.get("v.diagram"); 58 | var oldValue = diagram.settings.layout; 59 | var newValue; 60 | if ($A.util.isUndefined(oldValue)) { 61 | newValue = "TD"; 62 | } else { 63 | newValue = oldValue == "LR"? "TD" : "LR"; 64 | } 65 | diagram.settings.layout = newValue; 66 | component.set("v.diagram", diagram); 67 | component.getEvent('onSettingsChange').setParams({layout: newValue}).fire(); 68 | }, 69 | 70 | /** 71 | * Handler when user reset zoom level 72 | */ 73 | onResetZoom : function(component, event, helper){ 74 | component.find('diagramViewer').resetZoom(); 75 | }, 76 | 77 | /** 78 | * App event DiagramUpdatedEvent handler: when diagram data is updated, re-render the graphviz diagram 79 | */ 80 | onDiagramUpdated: function (component, event, helper) { 81 | helper.render(component); 82 | }, 83 | 84 | onSettingsChanged: function (component, event, helper) { 85 | if (event.getSource().getLocalId() != component.getLocalId()) { // avoid infinite loop 86 | var diagram = component.get("v.diagram"); 87 | if (!$A.util.isEmpty(diagram)) { 88 | diagram.settings.obscureEntities = event.getParam('obscuredEntities'); 89 | diagram.settings.from = event.getParam('from'); 90 | helper.render(component); 91 | } 92 | } 93 | }, 94 | 95 | /** 96 | * Set SVG content for user download after diagram is rendered 97 | */ 98 | onDiagramRendered: function (component, event, helper) { 99 | component.set('v.svgContent', event.getParam('scope')); 100 | }, 101 | 102 | /** 103 | * Handler when user press copy graphviz 104 | */ 105 | handleCopyGraphviz: function (component, event, helper) { 106 | helper.copyContent(component, helper, 'graphvizContent'); 107 | }, 108 | 109 | /** 110 | * Handler when user press copy svg 111 | */ 112 | handleCopySVG: function (component, event, helper) { 113 | helper.copyContent(component, helper, 'svgContent'); 114 | }, 115 | 116 | /** 117 | * Handler when user press download graphviz 118 | */ 119 | handleDownloadGraphviz: function (component, event, helper) { 120 | var diagram = component.get('v.diagram'); 121 | var content = component.get('v.graphvizContent'); 122 | helper.saveToFile(diagram.label + '.gv', content); 123 | }, 124 | 125 | /** 126 | * Handler when user press download SVG 127 | */ 128 | handleDownloadSVG: function (component, event, helper) { 129 | var diagram = component.get('v.diagram'); 130 | var content = component.get('v.svgContent'); 131 | helper.saveToFile(diagram.label + '.svg', content); 132 | }, 133 | }) -------------------------------------------------------------------------------- /graphviz/main/default/aura/SOQLRenderer/SOQLRenderer.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 11 | 13 | 16 | 19 | 21 | 22 | 24 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
{!v.soql}
39 |
40 | 41 |
42 |
43 | 45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 |
    53 | 54 |
  • SELECT
  • 55 | 56 |
  • {!sl}
  • 57 |
    58 |
    59 |
  • 60 |
    61 |
    {!v.prompt}
    62 |
    63 | 64 | 65 | 66 | 67 | 68 | 69 |
    70 | 71 |
    72 | 74 |
    75 |
    76 | Copied! 77 | 82 | 84 |
    85 |
    86 |
    87 |
  • 88 |
89 |
90 |
91 | 92 |
93 | 94 |
95 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/DiagramOutput/DiagramOutput.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 |
27 |
28 | 29 | 32 | 33 | 36 | 37 | 38 |
39 |
40 | 43 |
44 | 45 |
46 | 50 |
51 |
52 | 55 |
56 |
57 | 60 |
61 |
62 | 65 |
66 |
67 | 69 |
70 | 71 | 72 | 73 | 74 | 75 |
76 | 77 |
78 | 79 | 80 | 81 | 83 | 84 | 85 |

86 | 88 | 89 | 90 | 91 |
92 | 93 |
94 | 95 |
-------------------------------------------------------------------------------- /doc/development.md: -------------------------------------------------------------------------------- 1 | ## Getting started as a developer 2 | 3 | First, clone this project to your local filesystem. 4 | 5 | You will need access to a *Developer Hub* org. You can do by this enabling the dev hub feature in your production org 6 | or [signing up for a 30 trial org](https://developer.salesforce.com/promotions/orgs/dx-signup) with the dev hub enabled. 7 | 8 | Then you should install the SFDX CLI and 9 | 10 | Login to the dev hub org using this command: (a new browser window will open) 11 | 12 | Note : keep the hub org browser window open, it's needed in the next step. 13 | 14 | `sfdx force:auth:web:login --setdefaultdevhubusername --setalias my-hub-org` 15 | 16 | Link the packaging DE org: 17 | 18 | Contact us to get access to the packaging org so that you can add a *Namespace Registry* to your hub org. 19 | 20 | Then [follow these instructions](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_reg_namespace.htm) to link to the packaging DE org. 21 | 22 | You no longer need to keep the hub org open in the browser once linked. 23 | 24 | Now you are ready to create a new scratch org: 25 | 26 | `sfdx force:org:create --definitionfile config/project-scratch-def.json --durationdays 29 --setalias scratch1 --setdefaultusername` 27 | 28 | List all orgs to see which you have setup and connected to your SFDX CLI: 29 | 30 | `sfdx force:org:list` 31 | 32 | At any time you can open/login a window to new (default) scratch org: 33 | 34 | `sfdx force:org:open --path lightning/page/home` 35 | 36 | Now, install the Lightning Testing Service: 37 | 38 | `sfdx force:lightning:test:install` 39 | 40 | Deploy the code (ensure you are in graphvizforce-lightning dir): 41 | 42 | `sfdx force:source:push` 43 | 44 | Assign permission set to access the App and Tab: 45 | 46 | `sfdx force:user:permset:assign --permsetname graphvizforce` 47 | 48 | Now refresh Lightning Experience and you should be able to switch to the **Graphvizforce** app and 49 | see the single tab where the ERD component is available. 50 | 51 | If you use the *Developer Console* as your IDE, any changes made in your scratch org can be *pulled* back 52 | to the local dir using: 53 | 54 | `sfdx force:source:pull` 55 | 56 | ..and then committed to SCM as normal. 57 | 58 | ## Linting 59 | 60 | We use the Lightning Linter to check our code. To run this locally use: 61 | 62 | `sfdx force:lightning:lint graphviz` 63 | 64 | ## Testing 65 | 66 | #### Apex tests 67 | 68 | `sfdx force:apex:test:run` and follow the instruction returned. 69 | 70 | #### Lightning Tests 71 | 72 | Use the [Lightning Testing Service](https://forcedotcom.github.io/LightningTestingService/) (Jasmine flavoured) to test the components in this project. 73 | Use the following commands to run the tests: 74 | 75 | `sfdx force:lightning:test:run -a DiagramViewerTests.app` 76 | 77 | or you can run the testing app in the browser using: 78 | 79 | `sfdx force:org:open --path c/DiagramViewerTests.app` 80 | 81 | #### Jasmine / Node Tests 82 | 83 | `cd js/pure` 84 | 85 | `npm install` 86 | 87 | `./node_modules/jasmine-node/bin/jasmine-node spec` 88 | 89 | #### Development Mode 90 | 91 | If you are doing development on this tool, it is useful to enable *development mode* 92 | 93 | To enable it, add a new custom setting in *Graphviz Config* with Name = *development mode* 94 | and Value = *TRUE* 95 | 96 | This will: 97 | 98 | * cause the Apex auto-build to include Apex classes from this project when generating a diagram 99 | 100 | #### Lightning Development 101 | 102 | Although testing the full application is important, refreshing it after changes is slow. 103 | For this reason, there's also *TestApp.app* which refreshes much faster. It will not support tooling API features. 104 | 105 | `sfdx force:org:open --path c/TestApp.app` 106 | 107 | #### Javascript Development 108 | 109 | Functions that have complex logic are built using npm tooling to provide instant feedback when changing code. 110 | 111 | `cd js/pure` 112 | 113 | `npm install` 114 | 115 | TODO use npm run for this 116 | `./node_modules/jasmine-node/bin/jasmine-node --watch src test --autotest --color spec` 117 | 118 | Once you have made your changes and tested them, you will need to deploy the node.js code to the SFDC static resource. 119 | This is done in 2 steps: 120 | 121 | TODO use npm run for these two 122 | 1. `./node_modules/webpack-cli/bin/webpack.js` updates the static resource file on your local filesystem 123 | 2. `sfdx force:source:push` deploys the changed static resource to SFDC 124 | 125 | Now you can refresh the Test app or the full app to see your new code in action. 126 | 127 | You should also enable *Debug Mode* for Lightning Components to see the extra checks done for the components at runtime. 128 | 129 | ## Description of Files and Directories 130 | 131 | This project uses SFDX for all stages. 132 | 133 | ## Packaging for Managed Package 134 | 135 | 1. Add all components into `./package.xml` 136 | 2. In terminal, run `sfdx force:mdapi:retrieve -r ./mdapipkg -k ./package.xml` 137 | 3. Extract `./mdapipkg/unpackaged.zip` to directory './mdapipkg/unpackaged' 138 | 4. Create your own CI dir with sfdc jar, build.xml and/or build.properties. Add target DE org credentials. 139 | 5. In terminal, run `ant deployTarget` where *deployTarget* is the target in the build.xml 140 | 6. Test the application in the target DE org 141 | 7. Upload a new version a managed package in the target DE org 142 | * Ensure permission set is included 143 | * Must be *Managed* and not *Managed - Beta* or it cannot be installed in prod/DE orgs 144 | 8. Add a tag to github that matches the package version. good practice 145 | 9. Ask @steveb8n to update the tiny.cc urls 146 | 147 | ## CI Maintenance 148 | 149 | The docker image is built to support the Chrome headless browser, 150 | configured using the lts-config.json file. There are some important values in that file: 151 | 152 | * the chrome driver version. always use the latest from https://chromedriver.storage.googleapis.com/index.html 153 | * must use --headless and --no-sandbox 154 | 155 | The docker commands for updating: 156 | 157 | `docker build . -t steveb8n/circle-sfdx` 158 | `docker run -it steveb8n/circle-sfdx bash` for testing locally 159 | `docker login` 160 | `docker push steveb8n/circle-sfdx` 161 | -------------------------------------------------------------------------------- /js/pure/spec/SOQL2RenderSpec.js: -------------------------------------------------------------------------------- 1 | var gvfp = require('../src/pure.js'); 2 | var samples = require('../test/diagramPersistedSamples.js'); 3 | var wrappers = require('../test/describedObjectWrappers.js'); 4 | var fs = require('fs'); 5 | 6 | var renderAndSave = function (sample, from, name) { 7 | var result = gvfp.soql.v2.diagramAsSelects(sample, wrappers.wrappers, from); 8 | result.query = gvfp.soql.v2.diagramSelectsAsSOQL(result.selectLists, from, false); 9 | 10 | // save to file-system to support testing of each query using the DX api 11 | fs.mkdir("./generated/soql", function (error) { 12 | // ignore errors when dir already exists 13 | }); 14 | fs.writeFile("./generated/soql/" + name + ".soql", result.query, 15 | function (error) { 16 | // ignore write errors 17 | console.log(error); 18 | }); 19 | 20 | return result; 21 | } 22 | 23 | describe("query edge cases", function () { 24 | 25 | // some objects don't have child relationship describes and can't be used in SOQL 26 | // the example here is: SELECT Id, (Select Id from CaseTeamMembers) FROM CaseTeamRole 27 | // which will not run in SOQL. try it and see. 28 | describe("invalid join from CaseTeamRole to CaseTeamMember", function () { 29 | it("will throw an error to the user", function () { 30 | expect(function () { 31 | renderAndSave(samples.case_teams, "CaseTeamRole", "case-teams-from-role"); 32 | }).toThrow("The 'CaseTeamMember' object cannot be used as a child in a parent -> child relationship query with the 'CaseTeamRole' entity as the parent/from entity!"); 33 | }) 34 | }) 35 | // but querying from the other side works ok so SOQL generation should work with errors 36 | describe("valid join from CaseTeamMember to CaseTeamRole", function () { 37 | var result = renderAndSave(samples.case_teams, "CaseTeamMember", "case-teams-from-member"); 38 | it("generates a query", function () { 39 | expect(result.query).toEqual("SELECT Id,Parent.Id,TeamRole.Id FROM CaseTeamMember"); 40 | }); 41 | }) 42 | 43 | describe("No fields selected", function () { 44 | var result = renderAndSave(samples.account_contact_no_fields, "Account", "account-contact-without-fields"); 45 | it("Ids are added for all entities with no selected fields", function () { 46 | expect(result.selectLists).toEqual(['Id', '(SELECT Id FROM Contacts)']); 47 | }); 48 | }) 49 | 50 | describe("parent joins with no fields selected", function () { 51 | var result = renderAndSave(samples.timesheets, "TimeSheetEntry", "timesheetentry-to-timesheet"); 52 | it("auto-added Id fields have prefixes", function () { 53 | expect(result.query).toEqual("SELECT Id,TimeSheet.Id,WorkOrder.Id FROM TimeSheetEntry"); 54 | }); 55 | }) 56 | 57 | describe("Duplicate child joins", function () { 58 | var result = renderAndSave(samples.account_partners, "Account", "account-partners"); 59 | it("Child relationship joins use correct names when > 1 relationship from parent", function () { 60 | expect(result.selectLists).toEqual([ 61 | 'Id', 62 | '(SELECT Id FROM AccountPartnersFrom)', 63 | '(SELECT Id FROM AccountPartnersTo)']); 64 | }); 65 | }) 66 | 67 | describe("User joins", function () { 68 | var fromAccount = renderAndSave(samples.account_user, "Account", "account-user-from-account"); 69 | it("Account to user join returns only fields visible in the diagram", function () { 70 | expect(fromAccount.selectLists).toEqual(['Id', '(SELECT Id FROM Users)']); 71 | }); 72 | 73 | it("User to Account joins alerts user of invalid join", function () { 74 | expect(function () { 75 | renderAndSave(samples.account_user, "User", "account-user-from-user"); 76 | }).toThrow("The 'Account' object cannot be used as a child in a parent -> child relationship query with the 'User' entity as the parent/from entity!"); 77 | }); 78 | }) 79 | 80 | }); 81 | 82 | describe("account, contact and feed joins", function () { 83 | 84 | describe("contact to account parent and feed children", function () { 85 | var result = renderAndSave(samples.account_contact_feed2, "Contact", "contact-with-parent-and-children"); 86 | it("generates 3 fields and two joins", function () { 87 | expect(result.selectLists).toEqual( 88 | ['LastName,FirstName,AccountId', // from 89 | 'Account.Name,Account.Type', // parent 90 | '(SELECT Type,CreatedDate FROM Feeds)' // child 91 | ]); 92 | expect(result.query).toEqual("SELECT LastName,FirstName,AccountId,Account.Name,Account.Type,(SELECT Type,CreatedDate FROM Feeds) FROM Contact"); 93 | }); 94 | }); 95 | 96 | describe("account to contact children", function () { 97 | var result = renderAndSave(samples.account_contact_feed2, "Account", "account-with-contact-children"); 98 | it("generates 2 fields and one child join", function () { 99 | expect(result.selectLists).toEqual( 100 | ['Name,Type', // from 101 | '(SELECT LastName,FirstName,AccountId FROM Contacts)' // child 102 | ]); 103 | expect(result.query).toEqual("SELECT Name,Type,(SELECT LastName,FirstName,AccountId FROM Contacts) FROM Account"); 104 | }); 105 | }); 106 | 107 | describe("feed to contact and account ancestors", function () { 108 | var result = renderAndSave(samples.account_contact_feed2, "ContactFeed", "feed-with-contact-account-ancestors"); 109 | it("generates 2 fields and two ancestor joins", function () { 110 | expect(result.selectLists).toEqual( 111 | ['Type,CreatedDate', // from 112 | 'Parent.LastName,Parent.FirstName,Parent.AccountId', // parent 113 | 'Parent.Account.Name,Parent.Account.Type' // grand-parent 114 | ]); 115 | expect(result.query).toEqual("SELECT Type,CreatedDate,Parent.LastName,Parent.FirstName,Parent.AccountId,Parent.Account.Name,Parent.Account.Type FROM ContactFeed"); 116 | }); 117 | }); 118 | }) 119 | 120 | describe("entity extraction", function () { 121 | var result = gvfp.soql.v2.entities(samples.account_contact_feed2); 122 | it("finds 3 entities in the diagram", function () { 123 | expect(result).toEqual(['Contact', 'Account', 'ContactFeed']); 124 | }); 125 | }) 126 | 127 | // TODO a child join where the child has another parent,grandparent present in diagram 128 | // TODO a related entity with no fields selected in the diagram 129 | 130 | // TODO use DX to check if each SOQL file/query is valid using the API 131 | -------------------------------------------------------------------------------- /graphviz/main/default/aura/ERDContainer/ERDContainer.cmp: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 |

36 |      37 | {!v.selectedDiagram.name} 38 |

39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 62 | 63 |
64 |
65 |
66 | 67 |
68 |
69 |
    70 | 71 |
  • 72 | 74 |
  • 75 |
    76 | 77 | 78 | 79 |
    80 |
    81 |
    Start by creating a new diagram using the "Enter diagram name" above 82 |
    83 |
    Then click the "View" button on the new diagram. 84 |
    85 |
    86 |
    87 | 88 |
    89 |
90 |
91 | 92 |
93 | 94 | 95 |
96 | 97 |
98 |
99 |
100 | 101 | 102 |
103 |
104 |
105 | 106 |
107 | 108 | 109 | 110 |
-------------------------------------------------------------------------------- /graphviz/main/default/classes/GraphVizForceController.cls: -------------------------------------------------------------------------------- 1 | public without sharing class GraphVizForceController { 2 | 3 | @AuraEnabled 4 | public static String saveDiagram(String content, Id recordId){ 5 | Graphviz_Diagram__c diagram = new Graphviz_Diagram__c(Content__c = content); 6 | if(recordId != null){ 7 | diagram.Id = recordId; 8 | } 9 | upsert diagram; 10 | LightningUtility.ResultWrapper resultWrapper = new LightningUtility.ResultWrapper(); 11 | resultWrapper.result = diagram; 12 | return JSON.serialize(resultWrapper); 13 | } 14 | 15 | @AuraEnabled 16 | public static String deleteDiagram(Id recordId){ 17 | Graphviz_Diagram__c diagram = new Graphviz_Diagram__c(Id = recordId); 18 | delete diagram; 19 | LightningUtility.ResultWrapper resultWrapper = new LightningUtility.ResultWrapper(); 20 | resultWrapper.result = true; 21 | return JSON.serialize(resultWrapper); 22 | } 23 | 24 | @AuraEnabled 25 | public static Map getConfig() { 26 | Map result = new Map(); 27 | for (GraphvizConfig__c config : [select Name__c, Value__c from GraphvizConfig__c]) { 28 | result.put(config.Name__c, config.Value__c); 29 | } 30 | return result; 31 | } 32 | 33 | @AuraEnabled 34 | public static List loadDiagrams(){ 35 | return [SELECT Id, Content__c FROM Graphviz_Diagram__c WHERE OwnerId = :UserInfo.getUserId()]; 36 | } 37 | 38 | @AuraEnabled 39 | public static String loadSchema(){ 40 | String result = JSON.serialize(loadSchemaWrappers()); 41 | return result; 42 | } 43 | 44 | @TestVisible 45 | private static List getChildRelationships(DescribeSObjectResult describe) { 46 | 47 | List result = new List(); 48 | 49 | // start with child relationships described for top level objects 50 | List childRelationships = describe.getChildRelationships(); 51 | if (!childRelationships.isEmpty()) { 52 | for (ChildRelationship rel : childRelationships) { 53 | // only return relationships with names since they are used in diagram arrows 54 | // and SOQL joins and these features don't work without a name 55 | if (rel.getRelationshipName() != NULL) { 56 | ChildRelationshipWrapper wrapper = new ChildRelationshipWrapper(); 57 | 58 | wrapper.childAPIName = rel.getChildSObject().getDescribe().getName(); 59 | wrapper.childFieldAPIName = rel.getField().getDescribe().getName(); 60 | wrapper.isCascadeDelete = rel.isCascadeDelete(); 61 | wrapper.relationshipName = rel.getRelationshipName(); 62 | 63 | result.add(wrapper); 64 | } 65 | } 66 | } 67 | 68 | return result; 69 | } 70 | 71 | public static List loadSchemaWrappers(){ 72 | List objectWrappers = new List(); 73 | Map gd = Schema.getGlobalDescribe(); 74 | if(!gd.isEmpty()){ 75 | for(Schema.SObjectType sObjType : gd.values()){ 76 | 77 | // Describe SObject 78 | Schema.DescribeSObjectResult sObjResult = sObjType.getDescribe(); 79 | 80 | // Create Object Wrapper 81 | ObjectWrapper objWrapper = new ObjectWrapper(); 82 | objWrapper.label = sObjResult.getLabel(); 83 | objWrapper.apiName = sObjResult.getName(); 84 | objWrapper.isCustom = sObjResult.isCustom(); 85 | objWrapper.childRelationships = getChildRelationships(sObjResult); 86 | 87 | // Describe Fields 88 | Map fieldsMap = sObjResult.fields.getMap(); 89 | List fieldList = fieldsMap.values(); 90 | if(!fieldList.isEmpty()){ 91 | objWrapper.fields = new List(); 92 | for(Schema.SObjectField field : fieldList){ 93 | Schema.DescribeFieldResult fieldResult = field.getDescribe(); 94 | FieldWrapper fieldWrapper = new FieldWrapper(); 95 | fieldWrapper.label = fieldResult.getLabel(); 96 | fieldWrapper.apiName = fieldResult.getName(); 97 | fieldWrapper.type = fieldResult.getType().name(); 98 | fieldWrapper.isCustom = fieldResult.isCustom(); 99 | fieldWrapper.isMDOrCascadeDelete = fieldResult.isCascadeDelete(); 100 | 101 | // If the field is a reference field, get the list of reference field api names 102 | List parentSObjectTypes = fieldResult.getReferenceTo(); 103 | if(!parentSObjectTypes.isEmpty()){ 104 | fieldWrapper.referenceFields = new List(); 105 | for(Schema.SObjectType parentSObjectType : parentSObjectTypes){ 106 | Schema.DescribeSObjectResult parentSObjectResult = parentSObjectType.getDescribe(); 107 | RelationshipWrapper rWrapper = new RelationshipWrapper(); 108 | rWrapper.parentLabel = parentSObjectResult.getLabel(); 109 | rWrapper.parentAPIName = parentSObjectResult.getName(); 110 | rWrapper.referenceFieldAPIName = fieldResult.getName(); 111 | rWrapper.relationshipName = fieldResult.getRelationshipName(); 112 | fieldWrapper.referenceFields.add(rWrapper); 113 | } 114 | } 115 | 116 | objWrapper.fields.add(fieldWrapper); 117 | } 118 | } 119 | objectWrappers.add(objWrapper); 120 | } 121 | } 122 | return objectWrappers; 123 | } 124 | 125 | public class ObjectWrapper{ 126 | public String label; 127 | public String apiName; 128 | public Boolean isCustom; 129 | public List fields; 130 | public List childRelationships; 131 | } 132 | 133 | public class FieldWrapper{ 134 | public String label; 135 | public String apiName; 136 | public String type; 137 | public Boolean isCustom; 138 | public Boolean isMDOrCascadeDelete; 139 | public List referenceFields; 140 | } 141 | 142 | public class ChildRelationshipWrapper { 143 | public String relationshipName; 144 | public String childAPIName; 145 | public String childFieldAPIName; 146 | public Boolean isCascadeDelete; 147 | } 148 | 149 | public class RelationshipWrapper{ 150 | public String parentLabel; 151 | public String parentAPIName; 152 | public String referenceFieldAPIName; 153 | public String relationshipName; 154 | } 155 | } --------------------------------------------------------------------------------