diff --git a/CHANGES.md b/CHANGES.md index 489a26bc..f9d7660e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,35 @@ # Release Notes +## Version 1.20 + +- Move popup arrow icon in Flow Builder because of Winter 24 UI changes [feature 200](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/200) +- Add 'Login As' button for Experience users [feature 190](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/190) +- Add 'Delete Records' button from data export page [feature 134](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/134) (contribution by [Oscar Gomez Balaguer](https://github.com/ogomezba)) +- Update popup title to show "Salesforce Inspector Reloaded" [feature 188](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/188) (idea by [Nicolas Vuillamy](https://github.com/nvuillam)) +- Add "Query Record" link from data-export page [feature 111](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/111) (contribution by [Antoine Leleu](https://github.com/AntoineLeleu-Salesforce)) +- Fix "Edit page layout link" for from show all data and use "openLinksInNewTab" property for those links [issue 181](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/181) +- Update to Salesforce API v 59.0 (Winter '24) +- Add a parameter to activate summary view of pset / psetGroup from shortcut tab [feature 175](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/175) +- Display record name (and link) in popup [feature 165](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/165) +- Add documentation link to popup +- Add option to open extension pages in a new tab using keyboard shortcuts (contribution by [Joshua Yarmak](https://github.com/toly11)) +- Add customizable query templates to query export page (idea and co-develop with [Samuel Krissi](https://github.com/samuelkrissi)) +- Explore-api page restyling +- Ability to define csv-file separator [feature 144](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/144) (issue by [Reinier van den Assum](https://github.com/foxysolutions)) +- Fix SObject auto detect for JSON input in data import +- "Lightning Field Setup" (from show all data) link did not work for CustomMetadataType and CustomSettings [issue 154](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/154) (issue by [Camille Guillory](https://github.com/CamilleGuillory)) +- Add missing Date Literals [feature 155](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/155) +- Allow navigation to the extension tabs (Object, Users, Shortcuts) using keyboard [feature 135](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/135) (feature by [Sarath Addanki](https://github.com/asknet)) +- Update query on EntityDefinition to avoid missing objects for large orgs [issue 138](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/138) (issue by [AjitRajendran](https://github.com/AjitRajendran)) +- Add 'LIMIT 200' when selecting 'FIELDS(' in autocomplete [feature 146](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/146) ) +- Change method to get extension id to be compatible with firefox [issue 137](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/137) (issue by [kkorynta](https://github.com/kkorynta)) +- Fix hardcoded browser in Generate Token url [issue 137](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/137) (issue by [kkorynta](https://github.com/kkorynta)) +- Add "Create New Flow", "Create New Custom Object", "Create New Permission Set", "Create New Custom Permission" and "Recycle Bin" shortcuts +- Update pop-up release note link to github pages +- Detect SObject on list view page [feature 121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/121) (idea by [Mehdi Cherfaoui](https://github.com/mehdisfdc)) +- Automate test setup manual step of contact to multiple accounts [Aidan Majewski](https://github.com/aimaj) +- In Data export, set input focus in SOQL query text area. [feature 183](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/183) (contribution by [Sarath Addanki](https://github.com/asknet)) + ## Version 1.19 - Inspect Page Restyling (UI improvements, red background for PROD, display / hide table borders) [PR105](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/105) (contribution by [Pietro Martino](https://github.com/pietromartino)) @@ -42,7 +72,7 @@ ## Version 1.14 -- Add checkbox in flow builder to give the possibility to the user to scroll on the flow (by [Samuel Krissi](https://github.com/samuelkrissi) ) +- Add checkbox in flow builder to give the possibility to the user to scroll on the flow (by [Samuel Krissi](https://github.com/samuelkrissi)) ![image](https://user-images.githubusercontent.com/96471586/226161542-cbedec0a-8988-4559-9152-d067ea6f9cb6.png) diff --git a/README.md b/README.md index fc07606f..feae840a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,10 @@ We all know and love Salesforce Inspector: As the great Søren Krabbe did not ha ## Documentation -[Extension's doc](https://tprouvot.github.io/Salesforce-Inspector-reloaded/) +> User guide for using the extension. + +[![view - Documentation](https://img.shields.io/badge/view-Documentation-blue?style=for-the-badge)](https://tprouvot.github.io/Salesforce-Inspector-reloaded/ "Go to extension documentation") + ## New features compared to original SF Inspector @@ -61,7 +64,7 @@ To validate the accuracy of this description, inspect the source code, monitor t ## Use Salesforce Inspector with a Connected App -Follow steps described in [wiki](https://github.com/tprouvot/Salesforce-Inspector-reloaded/wiki/How-to#use-sf-inspector-with-a-connected-app) +Follow steps described in [how-to documentation](https://tprouvot.github.io/Salesforce-Inspector-reloaded/how-to/#use-sf-inspector-with-a-connected-app) ## Installation @@ -71,6 +74,15 @@ Follow steps described in [wiki](https://github.com/tprouvot/Salesforce-Inspecto - [Firefox Browser Add-ons](https://addons.mozilla.org/en-US/firefox/addon/salesforce-inspector-reloaded/) - [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/salesforce-inspector-relo/noclfopoifgfgnflgkakofglfeeambpd) +### Local Installation + +1. Download or clone the repo. +2. Checkout the releaseCandidate branch. +3. Open `chrome://extensions/`. +4. Enable `Developer mode`. +5. Click `Load unpacked extension...`. +6. Select the **`addon`** subdirectory of this repository. + ## Troubleshooting - If Salesforce Inspector is not available after installation, the most likely issue is that your browser is not up to date. See [instructions for Google Chrome](https://productforums.google.com/forum/#!topic/chrome/YK1-o4KoSjc). @@ -120,19 +132,6 @@ Linting : to assure indentation, formatting and best practices coherence, please 1. `npm run eslint` -## Release - -Version number must be manually incremented in [addon/manifest-template.json](addon/manifest-template.json) file - -### Chrome - -If the version number is greater than the version currently in Chrome Web Store, the revision will be packaged and uploaded to the store ready for manual release to the masses. - -### Firefox - -1. `npm run firefox-release-build` -2. Upload the file from `target/firefox/firefox-release-build.zip` to addons.mozilla.org - ## Design Principles (we don't live up to all of them. pull requests welcome) diff --git a/addon/background.js b/addon/background.js index 57bb6525..8b1bbfe1 100644 --- a/addon/background.js +++ b/addon/background.js @@ -27,7 +27,21 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (sessionCookie) { sendResponse(sessionCookie.domain); } else { - sendResponse(null); + chrome.cookies.getAll({name: "sid", domain: "salesforce.mil", secure: true, storeId: sender.tab.cookieStoreId}, cookies => { + sessionCookie = cookies.find(c => c.value.startsWith(orgId + "!")); + if (sessionCookie) { + sendResponse(sessionCookie.domain); + } else { + chrome.cookies.getAll({name: "sid", domain: "cloudforce.mil", secure: true, storeId: sender.tab.cookieStoreId}, cookies => { + sessionCookie = cookies.find(c => c.value.startsWith(orgId + "!")); + if (sessionCookie) { + sendResponse(sessionCookie.domain); + } else { + sendResponse(null); + } + }); + } + }); } }); } diff --git a/addon/button.js b/addon/button.js index a0714f2b..b5918ab5 100644 --- a/addon/button.js +++ b/addon/button.js @@ -5,10 +5,9 @@ // sfdcBody = normal Salesforce page // ApexCSIPage = Developer Console // auraLoadingBox = Lightning / Salesforce1 -// location.host.endsWith("visualforce.com") = Visualforce page if (document.querySelector("body.sfdcBody, body.ApexCSIPage, #auraLoadingBox") || location.host.endsWith("visualforce.com")) { // We are in a Salesforce org - chrome.runtime.sendMessage({ message: "getSfHost", url: location.href }, sfHost => { + chrome.runtime.sendMessage({message: "getSfHost", url: location.href}, sfHost => { if (sfHost) { initButton(sfHost, false); } @@ -16,7 +15,6 @@ if (document.querySelector("body.sfdcBody, body.ApexCSIPage, #auraLoadingBox") | } function initButton(sfHost, inInspector) { - addFlowScrollability(); let rootEl = document.createElement("div"); rootEl.id = "insext"; let btn = document.createElement("div"); @@ -34,11 +32,19 @@ function initButton(sfHost, inInspector) { loadPopup(); }); + addFlowScrollability(); + function addFlowScrollability() { const currentUrl = window.location.href; // Check the current URL for the string "builder_platform_interaction" if (currentUrl.includes("builder_platform_interaction")) { + //add marging for the popup arrow to prevent overlap with standard close button in flow builder (Winter 24) + //temporary workaround, will be removed in next release when the popupArrow position will be updatable by users + const popupArrow = document.querySelector("#insext"); + if (popupArrow){ + popupArrow.style = "margin-top: 50px;"; + } // Create a new checkbox element const headerFlow = document.querySelector("builder_platform_interaction-container-common"); const overflowCheckbox = document.createElement("input"); @@ -120,7 +126,7 @@ function initButton(sfHost, inInspector) { }); rootEl.appendChild(popupEl); function openPopup() { - popupEl.contentWindow.postMessage({ insextUpdateRecordId: true, locationHref: location.href }, "*"); + popupEl.contentWindow.postMessage({insextUpdateRecordId: true, locationHref: location.href}, "*"); rootEl.classList.add("insext-active"); // These event listeners are only enabled when the popup is active to avoid interfering with Salesforce when not using the inspector addEventListener("click", outsidePopupClick); diff --git a/addon/data-export-test.js b/addon/data-export-test.js index 9b178240..65c56322 100644 --- a/addon/data-export-test.js +++ b/addon/data-export-test.js @@ -2,12 +2,12 @@ export async function dataExportTest(test) { console.log("TEST data-export"); - let { assertEquals, assert, loadPage, anonApex } = test; + let {assertEquals, assert, loadPage, anonApex} = test; localStorage.removeItem("insextQueryHistory"); localStorage.removeItem("insextSavedQueryHistory"); - let { model, sfConn } = await loadPage("data-export.html"); + let {model, sfConn} = await loadPage("data-export.html"); let vm = model; let queryInput = model.queryInput; function queryAutocompleteEvent() { @@ -37,8 +37,8 @@ export async function dataExportTest(test) { return list.map(el => el.value); } - assertEquals("select Id from Account", queryInput.value); - queryInput.selectionStart = queryInput.selectionEnd = "select Id from Account".length; // When the cursor is placed after object name, we will try to autocomplete that once the global describe loads, and we will not try to load object field describes, so we can test loading those separately + assertEquals("SELECT Id FROM Account", queryInput.value); + queryInput.selectionStart = queryInput.selectionEnd = "SELECT Id FROM Account".length; // When the cursor is placed after object name, we will try to autocomplete that once the global describe loads, and we will not try to load object field describes, so we can test loading those separately vm.queryAutocompleteHandler(); // Load global describe and user info @@ -77,7 +77,7 @@ export async function dataExportTest(test) { assertEquals("select Id, shipp from Account", queryInput.value); assertEquals("Account fields suggestions:", vm.autocompleteResults.title); assertEquals(["ShippingAddress", "ShippingCity", "ShippingCountry", "ShippingGeocodeAccuracy", "ShippingLatitude", "ShippingLongitude", "ShippingPostalCode", "ShippingState", "ShippingStreet"], getValues(vm.autocompleteResults.results)); - vm.queryAutocompleteHandler({ ctrlSpace: true }); + vm.queryAutocompleteHandler({ctrlSpace: true}); assertEquals("select Id, ShippingStreet, ShippingCity, ShippingState, ShippingPostalCode, ShippingCountry, ShippingLatitude, ShippingLongitude, ShippingGeocodeAccuracy, ShippingAddress from Account", queryInput.value); // Autocomplete relationship field in SELECT @@ -140,7 +140,7 @@ export async function dataExportTest(test) { // Autocomplete datetime value setQuery("select Id from Account where LastModifiedDate < TOD", "", " and IsDeleted = false"); assertEquals("Account.LastModifiedDate values:", vm.autocompleteResults.title); - assertEquals(["TODAY"], getValues(vm.autocompleteResults.results)); + assertEquals(["TODAY", "N_DAYS_AGO:n"], getValues(vm.autocompleteResults.results)); vm.autocompleteClick(vm.autocompleteResults.results[0]); assertEquals("select Id from Account where LastModifiedDate < TODAY and IsDeleted = false", queryInput.value); @@ -187,7 +187,7 @@ export async function dataExportTest(test) { await waitForSpinner(); assertEquals("Profile.Name values (Press Ctrl+Space to load suggestions):", vm.autocompleteResults.title); assertEquals([], getValues(vm.autocompleteResults.results)); - vm.queryAutocompleteHandler({ ctrlSpace: true }); + vm.queryAutocompleteHandler({ctrlSpace: true}); assertEquals("Loading Profile.Name values...", vm.autocompleteResults.title); assertEquals([], getValues(vm.autocompleteResults.results)); await waitForSpinner(); @@ -200,7 +200,7 @@ export async function dataExportTest(test) { setQuery("select Id from Account where Id = foo", "", ""); // LIKE query not supported by Id field assertEquals("Account.Id values (Press Ctrl+Space to load suggestions):", vm.autocompleteResults.title); assertEquals([], getValues(vm.autocompleteResults.results)); - vm.queryAutocompleteHandler({ ctrlSpace: true }); + vm.queryAutocompleteHandler({ctrlSpace: true}); assertEquals("Loading Account.Id values...", vm.autocompleteResults.title); assertEquals([], getValues(vm.autocompleteResults.results)); await waitForSpinner(); @@ -283,11 +283,11 @@ export async function dataExportTest(test) { assertEquals("Exported 4 record(s)", vm.exportStatus); assertEquals([ ["_", "Name", "Checkbox__c", "Number__c"], - [{ type: "Inspector_Test__c" }, "test1", false, 100.01], - [{ type: "Inspector_Test__c" }, "test2", true, 200.02], - [{ type: "Inspector_Test__c" }, "test3", false, 300.03], - [{ type: "Inspector_Test__c" }, "test4", true, 400.04] - ], vm.exportedData.table.map(row => row.map(cell => cell && cell.attributes ? { type: cell.attributes.type } : cell))); + [{type: "Inspector_Test__c"}, "test1", false, 100.01], + [{type: "Inspector_Test__c"}, "test2", true, 200.02], + [{type: "Inspector_Test__c"}, "test3", false, 300.03], + [{type: "Inspector_Test__c"}, "test4", true, 400.04] + ], vm.exportedData.table.map(row => row.map(cell => cell && cell.attributes ? {type: cell.attributes.type} : cell))); assertEquals(null, vm.exportError); assertEquals([true, true, true, true, true], vm.exportedData.rowVisibilities); assertEquals([true, true, true, true], vm.exportedData.colVisibilities); @@ -311,11 +311,11 @@ export async function dataExportTest(test) { assertEquals("Exported 4 record(s)", vm.exportStatus); assertEquals([ ["_", "Name", "Checkbox__c", "Number__c"], - [{ type: "Inspector_Test__c" }, "test1", false, 100.01], - [{ type: "Inspector_Test__c" }, "test2", true, 200.02], - [{ type: "Inspector_Test__c" }, "test3", false, 300.03], - [{ type: "Inspector_Test__c" }, "test4", true, 400.04] - ], vm.exportedData.table.map(row => row.map(cell => cell && cell.attributes ? { type: cell.attributes.type } : cell))); + [{type: "Inspector_Test__c"}, "test1", false, 100.01], + [{type: "Inspector_Test__c"}, "test2", true, 200.02], + [{type: "Inspector_Test__c"}, "test3", false, 300.03], + [{type: "Inspector_Test__c"}, "test4", true, 400.04] + ], vm.exportedData.table.map(row => row.map(cell => cell && cell.attributes ? {type: cell.attributes.type} : cell))); assertEquals(null, vm.exportError); assertEquals([true, false, true, false, true], vm.exportedData.rowVisibilities); assertEquals([true, true, true, true], vm.exportedData.colVisibilities); @@ -326,11 +326,11 @@ export async function dataExportTest(test) { assertEquals("Exported 4 record(s)", vm.exportStatus); assertEquals([ ["_", "Name", "Checkbox__c", "Number__c"], - [{ type: "Inspector_Test__c" }, "test1", false, 100.01], - [{ type: "Inspector_Test__c" }, "test2", true, 200.02], - [{ type: "Inspector_Test__c" }, "test3", false, 300.03], - [{ type: "Inspector_Test__c" }, "test4", true, 400.04] - ], vm.exportedData.table.map(row => row.map(cell => cell && cell.attributes ? { type: cell.attributes.type } : cell))); + [{type: "Inspector_Test__c"}, "test1", false, 100.01], + [{type: "Inspector_Test__c"}, "test2", true, 200.02], + [{type: "Inspector_Test__c"}, "test3", false, 300.03], + [{type: "Inspector_Test__c"}, "test4", true, 400.04] + ], vm.exportedData.table.map(row => row.map(cell => cell && cell.attributes ? {type: cell.attributes.type} : cell))); assertEquals(null, vm.exportError); assertEquals([true, true, true, true, true], vm.exportedData.rowVisibilities); assertEquals([true, true, true, true], vm.exportedData.colVisibilities); @@ -353,11 +353,11 @@ export async function dataExportTest(test) { assertEquals("Exported 4 record(s)", vm.exportStatus); assertEquals([ ["_", "Name", "Lookup__r", "Lookup__r.Name"], - [{ type: "Inspector_Test__c" }, "test1", null, null], - [{ type: "Inspector_Test__c" }, "test2", { type: "Inspector_Test__c" }, "test1"], - [{ type: "Inspector_Test__c" }, "test3", null, null], - [{ type: "Inspector_Test__c" }, "test4", { type: "Inspector_Test__c" }, "test3"] - ], vm.exportedData.table.map(row => row.map(cell => cell && cell.attributes ? { type: cell.attributes.type } : cell))); + [{type: "Inspector_Test__c"}, "test1", null, null], + [{type: "Inspector_Test__c"}, "test2", {type: "Inspector_Test__c"}, "test1"], + [{type: "Inspector_Test__c"}, "test3", null, null], + [{type: "Inspector_Test__c"}, "test4", {type: "Inspector_Test__c"}, "test3"] + ], vm.exportedData.table.map(row => row.map(cell => cell && cell.attributes ? {type: cell.attributes.type} : cell))); assertEquals(null, vm.exportError); assertEquals([true, true, true, true, true], vm.exportedData.rowVisibilities); assertEquals([true, true, true, true], vm.exportedData.colVisibilities); @@ -521,12 +521,12 @@ export async function dataExportTest(test) { // Query history assertEquals([ - { query: "select Name from ApexClass", useToolingApi: true }, - { query: "select Id from Inspector_Test__c", useToolingApi: false }, - { query: "select count() from Inspector_Test__c", useToolingApi: false }, - { query: "select Id from Inspector_Test__c where name = 'no such name'", useToolingApi: false }, - { query: "select Name, Lookup__r.Name from Inspector_Test__c order by Name", useToolingApi: false }, - { query: "select Name, Checkbox__c, Number__c from Inspector_Test__c order by Name", useToolingApi: false } + {query: "select Name from ApexClass", useToolingApi: true}, + {query: "select Id from Inspector_Test__c", useToolingApi: false}, + {query: "select count() from Inspector_Test__c", useToolingApi: false}, + {query: "select Id from Inspector_Test__c where name = 'no such name'", useToolingApi: false}, + {query: "select Name, Lookup__r.Name from Inspector_Test__c order by Name", useToolingApi: false}, + {query: "select Name, Checkbox__c, Number__c from Inspector_Test__c order by Name", useToolingApi: false} ], vm.queryHistory.list); vm.selectedHistoryEntry = vm.queryHistory.list[2]; vm.selectHistoryEntry(); @@ -534,6 +534,30 @@ export async function dataExportTest(test) { vm.clearHistory(); assertEquals([], vm.queryHistory.list); + await anonApex(` + delete [select Id from Inspector_Test__c]; + insert new Inspector_Test__c(Name = 'test1', Checkbox__c = false, Number__c = 100.01); + insert new Inspector_Test__c(Name = 'test2', Checkbox__c = true, Number__c = 200.02, Lookup__r = new Inspector_Test__c(Name = 'test1')); + insert new Inspector_Test__c(Name = 'test3', Checkbox__c = false, Number__c = 300.03); + insert new Inspector_Test__c(Name = 'test4', Checkbox__c = true, Number__c = 400.04, Lookup__r = new Inspector_Test__c(Name = 'test3')); + `); + + // "Delete Records" button + queryInput.value = "select Name from Inspector_Test__c"; + vm.doExport(); + await waitForSpinner(); + assert(!vm.canDelete(), "Delete button should be disabled as there is no Id field included in the query"); + + queryInput.value = "select Id, Name from Inspector_Test__c where Name = 'no such name'"; + vm.doExport(); + await waitForSpinner(); + assert(!vm.canDelete(), "Delete button should be disabled as there are no records to delete"); //Id field is not included in the query + + queryInput.value = "select Id, Name from Inspector_Test__c"; + vm.doExport(); + await waitForSpinner(); + assert(vm.canDelete(), "The Delete button should be enabled"); + // Autocomplete load errors let restOrig = sfConn.rest; let restError = () => Promise.reject(); diff --git a/addon/data-export.css b/addon/data-export.css index db63d848..11988edb 100644 --- a/addon/data-export.css +++ b/addon/data-export.css @@ -224,7 +224,7 @@ select, input[type=search], input[type=save], input[type=default] { - width: 10rem; + width: 8.5rem; font-family: inherit; padding: 5px 13px; border: 1px solid #DDDBDA; @@ -382,6 +382,32 @@ textarea[readonly] { color: #a12b2b; } +.delete-btn { + background-color: #c23934; + border-color: #c23934; + color: white; + /* Allows to still show the title even when disabled as it contains useful information */ + pointer-events: auto; +} + +.delete-btn:not(:disabled):hover, +.delete-btn:not(:disabled):focus { + background-color: #a61a14; + border-color: #c23934; + color: white; +} + +.delete-btn:not(:disabled):active { + background-color: #870500; + border-color: #870500; +} + +.delete-btn:disabled, .delete-btn:disabled:hover { + background-color: #c9c7c5; + border-color: #c9c7c5; + color: white; +} + .char-btn { color: white; text-decoration: none; diff --git a/addon/data-export.js b/addon/data-export.js index 465d2a13..e0dc097f 100644 --- a/addon/data-export.js +++ b/addon/data-export.js @@ -1,7 +1,7 @@ /* global React ReactDOM */ -import { sfConn, apiVersion } from "./inspector.js"; +import {sfConn, apiVersion} from "./inspector.js"; /* global initButton */ -import { Enumerable, DescribeInfo, copyToClipboard, initScrollTable } from "./data-load.js"; +import {Enumerable, DescribeInfo, copyToClipboard, initScrollTable} from "./data-load.js"; class QueryHistory { constructor(storageKey, max) { @@ -65,12 +65,12 @@ class QueryHistory { } class Model { - constructor({ sfHost, args }) { + constructor({sfHost, args}) { this.sfHost = sfHost; this.queryInput = null; this.initialQuery = ""; this.describeInfo = new DescribeInfo(this.spinFor.bind(this), () => { - this.queryAutocompleteHandler({ newDescribe: true }); + this.queryAutocompleteHandler({newDescribe: true}); this.didUpdate(); }); @@ -81,7 +81,7 @@ class Model { this.winInnerHeight = 0; this.queryAll = false; this.queryTooling = false; - this.autocompleteResults = { sobjectName: "", title: "\u00A0", results: [] }; + this.autocompleteResults = {sobjectName: "", title: "\u00A0", results: []}; this.autocompleteClick = null; this.isWorking = false; this.exportStatus = "Ready"; @@ -98,7 +98,14 @@ class Model { this.autocompleteProgress = {}; this.exportProgress = {}; this.queryName = ""; - this.clientId = localStorage.getItem(sfHost + "_clientId"); + this.clientId = localStorage.getItem(sfHost + "_clientId") ? localStorage.getItem(sfHost + "_clientId") : ""; + this.queryTemplates = localStorage.getItem("queryTemplates") ? this.queryTemplates = localStorage.getItem("queryTemplates").split("//") : [ + "SELECT Id FROM ", + "SELECT Id FROM WHERE", + "SELECT Id FROM WHERE IN", + "SELECT Id FROM WHERE LIKE", + "SELECT Id FROM WHERE ORDER BY" + ]; this.spinFor(sfConn.soap(sfConn.wsdl(apiVersion, "Partner"), "getUserInfo", {}).then(res => { this.userInfo = res.userFullName + " / " + res.userName + " / " + res.organizationName; @@ -111,7 +118,7 @@ class Model { this.initialQuery = this.queryHistory.list[0].query; this.queryTooling = this.queryHistory.list[0].useToolingApi; } else { - this.initialQuery = "select Id from Account"; + this.initialQuery = "SELECT Id FROM Account"; this.queryTooling = false; } @@ -165,6 +172,14 @@ class Model { this.selectedHistoryEntry = null; } } + selectQueryTemplate() { + this.queryInput.value = this.selectedQueryTemplate.trimStart(); + this.queryInput.focus(); + let indexPos = this.queryInput.value.toLowerCase().indexOf("from "); + if (indexPos !== -1) { + this.queryInput.setRangeText("", indexPos + 5, indexPos + 5, "end"); + } + } clearHistory() { this.queryHistory.clear(); } @@ -189,13 +204,13 @@ class Model { this.savedHistory.clear(); } addToHistory() { - this.savedHistory.add({ query: this.getQueryToSave(), useToolingApi: this.queryTooling }); + this.savedHistory.add({query: this.getQueryToSave(), useToolingApi: this.queryTooling}); } saveClientId() { localStorage.setItem(this.sfHost + "_clientId", this.clientId); } removeFromHistory() { - this.savedHistory.remove({ query: this.getQueryToSave(), useToolingApi: this.queryTooling }); + this.savedHistory.remove({query: this.getQueryToSave(), useToolingApi: this.queryTooling}); } getQueryToSave() { return this.queryName != "" ? this.queryName + ":" + this.queryInput.value : this.queryInput.value; @@ -206,15 +221,33 @@ class Model { canCopy() { return this.exportedData != null; } + canDelete() { + //In order to allow deletion, we should have at least 1 element and the Id field should have been included in the query + return this.exportedData + && (this.exportedData.countOfVisibleRecords === null /* no filtering has been done yet*/ || this.exportedData.countOfVisibleRecords > 1) + && this.exportedData?.table?.at(0)?.find(header => header.toLowerCase() === "id"); + } copyAsExcel() { copyToClipboard(this.exportedData.csvSerialize("\t")); } copyAsCsv() { - copyToClipboard(this.exportedData.csvSerialize(",")); + let separator = getSeparator(); + copyToClipboard(this.exportedData.csvSerialize(separator)); } copyAsJson() { copyToClipboard(JSON.stringify(this.exportedData.records, null, " ")); } + deleteRecords(e) { + let separator = getSeparator(); + let data = this.exportedData.csvSerialize(separator); + let encodedData = btoa(data); + + let args = new URLSearchParams(); + args.set("host", this.sfHost); + args.set("data", encodedData); + + window.open("data-import.html?" + args, getLinkTarget(e)); + } /** * Notify React that we changed something, so it will rerender the view. * Should only be called once at the end of an event or asynchronous operation, since each call can take some time. @@ -281,7 +314,7 @@ class Model { vm.autocompleteProgress.abort(); } - vm.autocompleteClick = ({ value, suffix }) => { + vm.autocompleteClick = ({value, suffix}) => { vm.queryInput.focus(); //handle when selected field is the last one before "FROM" keyword, or if an existing comma is present after selection let indexFrom = query.toLowerCase().indexOf("from"); @@ -289,6 +322,10 @@ class Model { suffix = ""; } vm.queryInput.setRangeText(value + suffix, selStart, selEnd, "end"); + //add query suffix if needed + if (value.startsWith("FIELDS") && !query.toLowerCase().includes("limit")) { + vm.queryInput.value += " LIMIT 200"; + } vm.queryAutocompleteHandler(); }; @@ -298,7 +335,7 @@ class Model { : query.substring(0, selStart).match(/[a-zA-Z0-9_]*$/)[0]; selStart = selEnd - searchTerm.length; - function sortRank({ value, title }) { + function sortRank({value, title}) { let i = 0; if (value.toLowerCase() == searchTerm.toLowerCase()) { return i; @@ -336,7 +373,7 @@ class Model { // If we are just after the "from" keyword, autocomplete the sobject name if (query.substring(0, selStart).match(/(^|\s)from\s*$/i)) { - let { globalStatus, globalDescribe } = vm.describeInfo.describeGlobal(useToolingApi); + let {globalStatus, globalDescribe} = vm.describeInfo.describeGlobal(useToolingApi); if (!globalDescribe) { switch (globalStatus) { case "loading": @@ -350,7 +387,7 @@ class Model { vm.autocompleteResults = { sobjectName: "", title: "Loading metadata failed.", - results: [{ value: "Retry", title: "Retry" }] + results: [{value: "Retry", title: "Retry"}] }; vm.autocompleteClick = vm.autocompleteReload.bind(vm); return; @@ -368,7 +405,7 @@ class Model { title: "Objects suggestions:", results: new Enumerable(globalDescribe.sobjects) .filter(sobjectDescribe => sobjectDescribe.name.toLowerCase().includes(searchTerm.toLowerCase()) || sobjectDescribe.label.toLowerCase().includes(searchTerm.toLowerCase())) - .map(sobjectDescribe => ({ value: sobjectDescribe.name, title: sobjectDescribe.label, suffix: " ", rank: 1, autocompleteType: "object", dataType: "" })) + .map(sobjectDescribe => ({value: sobjectDescribe.name, title: sobjectDescribe.label, suffix: " ", rank: 1, autocompleteType: "object", dataType: ""})) .toArray() .sort(resultsSort) }; @@ -407,7 +444,7 @@ class Model { isAfterFrom = selStart > fromKeywordMatch.index + fromKeywordMatch[0].length; } } - let { sobjectStatus, sobjectDescribe } = vm.describeInfo.describeSobject(useToolingApi, sobjectName); + let {sobjectStatus, sobjectDescribe} = vm.describeInfo.describeSobject(useToolingApi, sobjectName); if (!sobjectDescribe) { switch (sobjectStatus) { case "loading": @@ -421,7 +458,7 @@ class Model { vm.autocompleteResults = { sobjectName, title: "Loading " + sobjectName + " metadata failed.", - results: [{ value: "Retry", title: "Retry" }] + results: [{value: "Retry", title: "Retry"}] }; vm.autocompleteClick = vm.autocompleteReload.bind(vm); return; @@ -484,7 +521,7 @@ class Model { .filter(field => field.relationshipName && field.relationshipName.toLowerCase() == referenceFieldName.toLowerCase()) .flatMap(field => field.referenceTo) ) { - let { sobjectStatus, sobjectDescribe } = vm.describeInfo.describeSobject(useToolingApi, referencedSobjectName); + let {sobjectStatus, sobjectDescribe} = vm.describeInfo.describeSobject(useToolingApi, referencedSobjectName); if (sobjectDescribe) { newContextSobjectDescribes.add(sobjectDescribe); } else { @@ -508,7 +545,7 @@ class Model { vm.autocompleteResults = { sobjectName, title: "Loading " + sobjectStatuses.get("loadfailed") + " metadata failed.", - results: [{ value: "Retry", title: "Retry" }] + results: [{value: "Retry", title: "Retry"}] }; vm.autocompleteClick = vm.autocompleteReload.bind(vm); return; @@ -542,7 +579,7 @@ class Model { let contextValueFields = contextSobjectDescribes .flatMap(sobjectDescribe => sobjectDescribe.fields .filter(field => field.name.toLowerCase() == fieldName.toLowerCase()) - .map(field => ({ sobjectDescribe, field })) + .map(field => ({sobjectDescribe, field})) ) .toArray(); if (contextValueFields.length == 0) { @@ -567,7 +604,7 @@ class Model { let contextValueField = contextValueFields[0]; let queryMethod = useToolingApi ? "tooling/query" : vm.queryAll ? "queryAll" : "query"; let acQuery = "select " + contextValueField.field.name + " from " + contextValueField.sobjectDescribe.name + " where " + contextValueField.field.name + " like '%" + searchTerm.replace(/'/g, "\\'") + "%' group by " + contextValueField.field.name + " limit 100"; - vm.spinFor(sfConn.rest("/services/data/v" + apiVersion + "/" + queryMethod + "/?q=" + encodeURIComponent(acQuery), { progressHandler: vm.autocompleteProgress }) + vm.spinFor(sfConn.rest("/services/data/v" + apiVersion + "/" + queryMethod + "/?q=" + encodeURIComponent(acQuery), {progressHandler: vm.autocompleteProgress}) .catch(err => { if (err.name != "AbortError") { vm.autocompleteResults = { @@ -589,7 +626,7 @@ class Model { results: new Enumerable(data.records) .map(record => record[contextValueField.field.name]) .filter(value => value) - .map(value => ({ value: "'" + value + "'", title: value, suffix: " ", rank: 1, autocompleteType: "fieldValue" })) + .map(value => ({value: "'" + value + "'", title: value, suffix: " ", rank: 1, autocompleteType: "fieldValue"})) .toArray() .sort(resultsSort) }; @@ -601,17 +638,17 @@ class Model { }; return; } - let ar = new Enumerable(contextValueFields).flatMap(function* ({ field }) { - yield* field.picklistValues.map(pickVal => ({ value: "'" + pickVal.value + "'", title: pickVal.label, suffix: " ", rank: 1, autocompleteType: "picklistValue", dataType: "" })); + let ar = new Enumerable(contextValueFields).flatMap(function* ({field}) { + yield* field.picklistValues.map(pickVal => ({value: "'" + pickVal.value + "'", title: pickVal.label, suffix: " ", rank: 1, autocompleteType: "picklistValue", dataType: ""})); if (field.type == "boolean") { - yield { value: "true", title: "true", suffix: " ", rank: 1 }; - yield { value: "false", title: "false", suffix: " ", rank: 1 }; + yield {value: "true", title: "true", suffix: " ", rank: 1}; + yield {value: "false", title: "false", suffix: " ", rank: 1}; } if (field.type == "date" || field.type == "datetime") { let pad = (n, d) => ("000" + n).slice(-d); let d = new Date(); if (field.type == "date") { - yield { value: pad(d.getFullYear(), 4) + "-" + pad(d.getMonth() + 1, 2) + "-" + pad(d.getDate(), 2), title: "Today", suffix: " ", rank: 1 }; + yield {value: pad(d.getFullYear(), 4) + "-" + pad(d.getMonth() + 1, 2) + "-" + pad(d.getDate(), 2), title: "Today", suffix: " ", rank: 1}; } if (field.type == "datetime") { yield { @@ -624,47 +661,54 @@ class Model { rank: 1 }; } - // from http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_dateformats.htm Spring 15 - yield { value: "YESTERDAY", title: "Starts 12:00:00 the day before and continues for 24 hours.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "TODAY", title: "Starts 12:00:00 of the current day and continues for 24 hours.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "TOMORROW", title: "Starts 12:00:00 after the current day and continues for 24 hours.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_WEEK", title: "Starts 12:00:00 on the first day of the week before the most recent first day of the week and continues for seven full days. First day of the week is determined by your locale.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "THIS_WEEK", title: "Starts 12:00:00 on the most recent first day of the week before the current day and continues for seven full days. First day of the week is determined by your locale.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_WEEK", title: "Starts 12:00:00 on the most recent first day of the week after the current day and continues for seven full days. First day of the week is determined by your locale.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_MONTH", title: "Starts 12:00:00 on the first day of the month before the current day and continues for all the days of that month.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "THIS_MONTH", title: "Starts 12:00:00 on the first day of the month that the current day is in and continues for all the days of that month.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_MONTH", title: "Starts 12:00:00 on the first day of the month after the month that the current day is in and continues for all the days of that month.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_90_DAYS", title: "Starts 12:00:00 of the current day and continues for the last 90 days.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_90_DAYS", title: "Starts 12:00:00 of the current day and continues for the next 90 days.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_N_DAYS:n", title: "For the number n provided, starts 12:00:00 of the current day and continues for the last n days.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_N_DAYS:n", title: "For the number n provided, starts 12:00:00 of the current day and continues for the next n days.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_N_WEEKS:n", title: "For the number n provided, starts 12:00:00 of the first day of the next week and continues for the next n weeks.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_N_WEEKS:n", title: "For the number n provided, starts 12:00:00 of the last day of the previous week and continues for the last n weeks.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_N_MONTHS:n", title: "For the number n provided, starts 12:00:00 of the first day of the next month and continues for the next n months.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_N_MONTHS:n", title: "For the number n provided, starts 12:00:00 of the last day of the previous month and continues for the last n months.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "THIS_QUARTER", title: "Starts 12:00:00 of the current quarter and continues to the end of the current quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_QUARTER", title: "Starts 12:00:00 of the previous quarter and continues to the end of that quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_QUARTER", title: "Starts 12:00:00 of the next quarter and continues to the end of that quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_N_QUARTERS:n", title: "Starts 12:00:00 of the next quarter and continues to the end of the nth quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_N_QUARTERS:n", title: "Starts 12:00:00 of the previous quarter and continues to the end of the previous nth quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "THIS_YEAR", title: "Starts 12:00:00 on January 1 of the current year and continues through the end of December 31 of the current year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_YEAR", title: "Starts 12:00:00 on January 1 of the previous year and continues through the end of December 31 of that year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_YEAR", title: "Starts 12:00:00 on January 1 of the following year and continues through the end of December 31 of that year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_N_YEARS:n", title: "Starts 12:00:00 on January 1 of the following year and continues through the end of December 31 of the nth year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_N_YEARS:n", title: "Starts 12:00:00 on January 1 of the previous year and continues through the end of December 31 of the previous nth year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "THIS_FISCAL_QUARTER", title: "Starts 12:00:00 on the first day of the current fiscal quarter and continues through the end of the last day of the fiscal quarter. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_FISCAL_QUARTER", title: "Starts 12:00:00 on the first day of the last fiscal quarter and continues through the end of the last day of that fiscal quarter. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_FISCAL_QUARTER", title: "Starts 12:00:00 on the first day of the next fiscal quarter and continues through the end of the last day of that fiscal quarter. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_N_FISCAL_QUARTERS:n", title: "Starts 12:00:00 on the first day of the next fiscal quarter and continues through the end of the last day of the nth fiscal quarter. The fiscal year is defined in the company profile under Setup atCompany Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_N_FISCAL_QUARTERS:n", title: "Starts 12:00:00 on the first day of the last fiscal quarter and continues through the end of the last day of the previous nth fiscal quarter. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "THIS_FISCAL_YEAR", title: "Starts 12:00:00 on the first day of the current fiscal year and continues through the end of the last day of the fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_FISCAL_YEAR", title: "Starts 12:00:00 on the first day of the last fiscal year and continues through the end of the last day of that fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_FISCAL_YEAR", title: "Starts 12:00:00 on the first day of the next fiscal year and continues through the end of the last day of that fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "NEXT_N_FISCAL_YEARS:n", title: "Starts 12:00:00 on the first day of the next fiscal year and continues through the end of the last day of the nth fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; - yield { value: "LAST_N_FISCAL_YEARS:n", title: "Starts 12:00:00 on the first day of the last fiscal year and continues through the end of the last day of the previous nth fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: "" }; + // from https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_dateformats.htm Winter 24 + yield {value: "YESTERDAY", title: "Starts 12:00:00 the day before and continues for 24 hours.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "TODAY", title: "Starts 12:00:00 of the current day and continues for 24 hours.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "TOMORROW", title: "Starts 12:00:00 after the current day and continues for 24 hours.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_WEEK", title: "Starts 12:00:00 on the first day of the week before the most recent first day of the week and continues for seven full days. First day of the week is determined by your locale.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "THIS_WEEK", title: "Starts 12:00:00 on the most recent first day of the week before the current day and continues for seven full days. First day of the week is determined by your locale.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_WEEK", title: "Starts 12:00:00 on the most recent first day of the week after the current day and continues for seven full days. First day of the week is determined by your locale.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_MONTH", title: "Starts 12:00:00 on the first day of the month before the current day and continues for all the days of that month.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "THIS_MONTH", title: "Starts 12:00:00 on the first day of the month that the current day is in and continues for all the days of that month.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_MONTH", title: "Starts 12:00:00 on the first day of the month after the month that the current day is in and continues for all the days of that month.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_90_DAYS", title: "Starts 12:00:00 of the current day and continues for the last 90 days.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_90_DAYS", title: "Starts 12:00:00 of the current day and continues for the next 90 days.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_N_DAYS:n", title: "For the number n provided, starts 12:00:00 of the current day and continues for the last n days.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_N_DAYS:n", title: "For the number n provided, starts 12:00:00 of the current day and continues for the next n days.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_N_WEEKS:n", title: "For the number n provided, starts 12:00:00 of the first day of the next week and continues for the next n weeks.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "N_DAYS_AGO:n", title: "Starts at 12:00:00 AM on the day n days before the current day and continues for 24 hours. (The range doesn’t include today.)", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_N_WEEKS:n", title: "For the number n provided, starts 12:00:00 of the last day of the previous week and continues for the last n weeks.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "N_WEEKS_AGO:n", title: "Starts at 12:00:00 AM on the first day of the month that started n months before the start of the current month and continues for all the days of that month.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_N_MONTHS:n", title: "For the number n provided, starts 12:00:00 of the first day of the next month and continues for the next n months.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_N_MONTHS:n", title: "For the number n provided, starts 12:00:00 of the last day of the previous month and continues for the last n months.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "N_MONTHS_AGO:n", title: "For the number n provided, starts 12:00:00 of the last day of the previous month and continues for the last n months.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "THIS_QUARTER", title: "Starts 12:00:00 of the current quarter and continues to the end of the current quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_QUARTER", title: "Starts 12:00:00 of the previous quarter and continues to the end of that quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_QUARTER", title: "Starts 12:00:00 of the next quarter and continues to the end of that quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_N_QUARTERS:n", title: "Starts 12:00:00 of the next quarter and continues to the end of the nth quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_N_QUARTERS:n", title: "Starts 12:00:00 of the previous quarter and continues to the end of the previous nth quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "N_QUARTERS_AGO:n", title: "Starts at 12:00:00 AM on the first day of the calendar quarter n quarters before the current calendar quarter and continues to the end of that quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "THIS_YEAR", title: "Starts 12:00:00 on January 1 of the current year and continues through the end of December 31 of the current year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_YEAR", title: "Starts 12:00:00 on January 1 of the previous year and continues through the end of December 31 of that year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_YEAR", title: "Starts 12:00:00 on January 1 of the following year and continues through the end of December 31 of that year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_N_YEARS:n", title: "Starts 12:00:00 on January 1 of the following year and continues through the end of December 31 of the nth year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_N_YEARS:n", title: "Starts 12:00:00 on January 1 of the previous year and continues through the end of December 31 of the previous nth year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "N_YEARS_AGO:n", title: "Starts at 12:00:00 AM on January 1 of the calendar year n years before the current calendar year and continues through the end of December 31 of that year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "THIS_FISCAL_QUARTER", title: "Starts 12:00:00 on the first day of the current fiscal quarter and continues through the end of the last day of the fiscal quarter. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_FISCAL_QUARTER", title: "Starts 12:00:00 on the first day of the last fiscal quarter and continues through the end of the last day of that fiscal quarter. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_FISCAL_QUARTER", title: "Starts 12:00:00 on the first day of the next fiscal quarter and continues through the end of the last day of that fiscal quarter. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_N_FISCAL_QUARTERS:n", title: "Starts 12:00:00 on the first day of the next fiscal quarter and continues through the end of the last day of the nth fiscal quarter. The fiscal year is defined in the company profile under Setup atCompany Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_N_FISCAL_QUARTERS:n", title: "Starts 12:00:00 on the first day of the last fiscal quarter and continues through the end of the last day of the previous nth fiscal quarter. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "N_FISCAL_QUARTERS_AGO:n", title: "Starts at 12:00:00 AM on the first day of the fiscal quarter n fiscal quarters before the current fiscal quarter and continues through the end of the last day of that fiscal quarter.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "THIS_FISCAL_YEAR", title: "Starts 12:00:00 on the first day of the current fiscal year and continues through the end of the last day of the fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_FISCAL_YEAR", title: "Starts 12:00:00 on the first day of the last fiscal year and continues through the end of the last day of that fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_FISCAL_YEAR", title: "Starts 12:00:00 on the first day of the next fiscal year and continues through the end of the last day of that fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "NEXT_N_FISCAL_YEARS:n", title: "Starts 12:00:00 on the first day of the next fiscal year and continues through the end of the last day of the nth fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "LAST_N_FISCAL_YEARS:n", title: "Starts 12:00:00 on the first day of the last fiscal year and continues through the end of the last day of the previous nth fiscal year. The fiscal year is defined in the company profile under Setup at Company Profile | Fiscal Year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; + yield {value: "N_FISCAL_YEARS_AGO:n", title: "Starts at 12:00:00 AM on the first day of the fiscal year n fiscal years ago and continues through the end of the last day of that fiscal year.", suffix: " ", rank: 1, autocompleteType: "variable", dataType: ""}; } if (field.nillable) { - yield { value: "null", title: "null", suffix: " ", rank: 1, autocompleteType: "null", dataType: "" }; + yield {value: "null", title: "null", suffix: " ", rank: 1, autocompleteType: "null", dataType: ""}; } }) .filter(res => res.value.toLowerCase().includes(searchTerm.toLowerCase()) || res.title.toLowerCase().includes(searchTerm.toLowerCase())) @@ -698,9 +742,9 @@ class Model { .flatMap(sobjectDescribe => sobjectDescribe.fields) .filter(field => field.name.toLowerCase().includes(searchTerm.toLowerCase()) || field.label.toLowerCase().includes(searchTerm.toLowerCase())) .flatMap(function* (field) { - yield { value: field.name, title: field.label, suffix: isAfterFrom ? " " : ", ", rank: 1, autocompleteType: "fieldName", dataType: field.type }; + yield {value: field.name, title: field.label, suffix: isAfterFrom ? " " : ", ", rank: 1, autocompleteType: "fieldName", dataType: field.type}; if (field.relationshipName) { - yield { value: field.relationshipName + ".", title: field.label, suffix: "", rank: 1, autocompleteType: "relationshipName", dataType: "" }; + yield {value: field.relationshipName + ".", title: field.label, suffix: "", rank: 1, autocompleteType: "relationshipName", dataType: ""}; } }) .concat( @@ -708,9 +752,9 @@ class Model { .filter(fn => fn.toLowerCase().startsWith(searchTerm.toLowerCase())) .map(fn => { if (fn.includes(")")) { //Exception to easily support functions with hardcoded parameter options - return { value: fn, title: fn, suffix: "", rank: 2, autocompleteType: "variable", dataType: "" }; + return {value: fn, title: fn, suffix: "", rank: 2, autocompleteType: "variable", dataType: ""}; } else { - return { value: fn, title: fn + "()", suffix: "(", rank: 2, autocompleteType: "variable", dataType: "" }; + return {value: fn, title: fn + "()", suffix: "(", rank: 2, autocompleteType: "variable", dataType: ""}; } }) ) @@ -731,7 +775,7 @@ class Model { function batchHandler(batch) { return batch.catch(err => { if (err.name == "AbortError") { - return { records: [], done: true, totalSize: -1 }; + return {records: [], done: true, totalSize: -1}; } throw err; }).then(data => { @@ -740,7 +784,7 @@ class Model { exportedData.totalSize = data.totalSize; } if (!data.done) { - let pr = batchHandler(sfConn.rest(data.nextRecordsUrl, { progressHandler: vm.exportProgress })); + let pr = batchHandler(sfConn.rest(data.nextRecordsUrl, {progressHandler: vm.exportProgress})); vm.isWorking = true; vm.exportStatus = "Exporting... Completed " + exportedData.records.length + " of " + exportedData.totalSize + " record(s)"; vm.exportError = null; @@ -749,7 +793,7 @@ class Model { vm.didUpdate(); return pr; } - vm.queryHistory.add({ query, useToolingApi: exportedData.isTooling }); + vm.queryHistory.add({query, useToolingApi: exportedData.isTooling}); if (exportedData.records.length == 0) { vm.isWorking = false; vm.exportStatus = data.totalSize > 0 ? "No data exported. " + data.totalSize + " record(s)." : "No data exported."; @@ -785,7 +829,7 @@ class Model { return null; }); } - vm.spinFor(batchHandler(sfConn.rest("/services/data/v" + apiVersion + "/" + queryMethod + "/?q=" + encodeURIComponent(query), { progressHandler: vm.exportProgress })) + vm.spinFor(batchHandler(sfConn.rest("/services/data/v" + apiVersion + "/" + queryMethod + "/?q=" + encodeURIComponent(query), {progressHandler: vm.exportProgress})) .catch(error => { console.error(error); vm.isWorking = false; @@ -853,6 +897,7 @@ function RecordTable(vm) { table: [], rowVisibilities: [], colVisibilities: [true], + countOfVisibleRecords: null, isTooling: false, totalSize: -1, addToTable(expRecords) { @@ -870,12 +915,26 @@ function RecordTable(vm) { discoverColumns(record, "", row); } }, - csvSerialize: separator => rt.table.map(row => row.map(cell => "\"" + cellToString(cell).split("\"").join("\"\"") + "\"").join(separator)).join("\r\n"), + csvSerialize: separator => rt.getVisibleTable().map(row => row.map(cell => "\"" + cellToString(cell).split("\"").join("\"\"") + "\"").join(separator)).join("\r\n"), updateVisibility() { let filter = vm.resultsFilter; + let countOfVisibleRecords = 0; for (let r = 1/* always show header */; r < rt.table.length; r++) { rt.rowVisibilities[r] = isVisible(rt.table[r], filter); + if (isVisible(rt.table[r], filter)) countOfVisibleRecords++; + } + this.countOfVisibleRecords = countOfVisibleRecords; + }, + getVisibleTable() { + if (vm.resultsFilter) { + let filteredTable = []; + for (let i = 0; i < rt.table.length; i++) { + if (rt.rowVisibilities[i]) + filteredTable.push(rt.table[i]); + } + return filteredTable; } + return rt.table; } }; return rt; @@ -889,6 +948,7 @@ class App extends React.Component { this.onQueryAllChange = this.onQueryAllChange.bind(this); this.onQueryToolingChange = this.onQueryToolingChange.bind(this); this.onSelectHistoryEntry = this.onSelectHistoryEntry.bind(this); + this.onSelectQueryTemplate = this.onSelectQueryTemplate.bind(this); this.onClearHistory = this.onClearHistory.bind(this); this.onSelectSavedEntry = this.onSelectSavedEntry.bind(this); this.onAddToHistory = this.onAddToHistory.bind(this); @@ -903,59 +963,66 @@ class App extends React.Component { this.onCopyAsExcel = this.onCopyAsExcel.bind(this); this.onCopyAsCsv = this.onCopyAsCsv.bind(this); this.onCopyAsJson = this.onCopyAsJson.bind(this); + this.onDeleteRecords = this.onDeleteRecords.bind(this); this.onResultsFilterInput = this.onResultsFilterInput.bind(this); this.onSetQueryName = this.onSetQueryName.bind(this); this.onSetClientId = this.onSetClientId.bind(this); this.onStopExport = this.onStopExport.bind(this); } onQueryAllChange(e) { - let { model } = this.props; + let {model} = this.props; model.queryAll = e.target.checked; model.didUpdate(); } onQueryToolingChange(e) { - let { model } = this.props; + let {model} = this.props; model.queryTooling = e.target.checked; model.queryAutocompleteHandler(); model.didUpdate(); } onSelectHistoryEntry(e) { - let { model } = this.props; + let {model} = this.props; model.selectedHistoryEntry = JSON.parse(e.target.value); model.selectHistoryEntry(); model.didUpdate(); } + onSelectQueryTemplate(e) { + let {model} = this.props; + model.selectedQueryTemplate = e.target.value; + model.selectQueryTemplate(); + model.didUpdate(); + } onClearHistory(e) { e.preventDefault(); let r = confirm("Are you sure you want to clear the query history?"); if (r == true) { - let { model } = this.props; + let {model} = this.props; model.clearHistory(); model.didUpdate(); } } onSelectSavedEntry(e) { - let { model } = this.props; + let {model} = this.props; model.selectedSavedEntry = JSON.parse(e.target.value); model.selectSavedEntry(); model.didUpdate(); } onAddToHistory(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.addToHistory(); model.didUpdate(); } onSaveClientId(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.saveClientId(); model.didUpdate(); } onRemoveFromHistory(e) { e.preventDefault(); let r = confirm("Are you sure you want to remove this saved query?"); - let { model } = this.props; + let {model} = this.props; if (r == true) { model.removeFromHistory(); } @@ -965,7 +1032,7 @@ class App extends React.Component { onClearSavedHistory(e) { e.preventDefault(); let r = confirm("Are you sure you want to remove all saved queries?"); - let { model } = this.props; + let {model} = this.props; if (r == true) { model.clearSavedHistory(); } @@ -974,29 +1041,29 @@ class App extends React.Component { } onToggleHelp(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.toggleHelp(); model.didUpdate(); } onToggleExpand(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.toggleExpand(); model.didUpdate(); } onToggleSavedOptions(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.toggleSavedOptions(); model.didUpdate(); } onExport() { - let { model } = this.props; + let {model} = this.props; model.doExport(); model.didUpdate(); } onCopyQuery() { - let { model } = this.props; + let {model} = this.props; let url = new URL(window.location.href); let searchParams = url.searchParams; searchParams.set("query", model.queryInput.value); @@ -1006,45 +1073,54 @@ class App extends React.Component { model.didUpdate(); } onCopyAsExcel() { - let { model } = this.props; + let {model} = this.props; model.copyAsExcel(); model.didUpdate(); } onCopyAsCsv() { - let { model } = this.props; + let {model} = this.props; model.copyAsCsv(); model.didUpdate(); } onCopyAsJson() { - let { model } = this.props; + let {model} = this.props; model.copyAsJson(); model.didUpdate(); } + onDeleteRecords(e) { + let {model} = this.props; + model.deleteRecords(e); + model.didUpdate(); + } onResultsFilterInput(e) { - let { model } = this.props; + let {model} = this.props; model.setResultsFilter(e.target.value); model.didUpdate(); } onSetQueryName(e) { - let { model } = this.props; + let {model} = this.props; model.setQueryName(e.target.value); model.didUpdate(); } onSetClientId(e) { - let { model } = this.props; + let {model} = this.props; model.setClientId(e.target.value); model.didUpdate(); } onStopExport() { - let { model } = this.props; + let {model} = this.props; model.stopExport(); model.didUpdate(); } componentDidMount() { - let { model } = this.props; + let {model} = this.props; let queryInput = this.refs.query; model.setQueryInput(queryInput); + //Set the cursor focus on query text area + if (localStorage.getItem("disableQueryInputAutoFocus") !== "true"){ + queryInput.focus(); + } function queryAutocompleteEvent() { model.queryAutocompleteHandler(); @@ -1061,7 +1137,7 @@ class App extends React.Component { queryInput.addEventListener("keydown", e => { if (e.ctrlKey && e.key == " ") { e.preventDefault(); - model.queryAutocompleteHandler({ ctrlSpace: true }); + model.queryAutocompleteHandler({ctrlSpace: true}); model.didUpdate(); } }); @@ -1080,7 +1156,7 @@ class App extends React.Component { if (!window.webkitURL) { // Firefox // Firefox does not fire a resize event. The next best thing is to listen to when the browser changes the style.height attribute. - new MutationObserver(recalculateHeight).observe(queryInput, { attributes: true }); + new MutationObserver(recalculateHeight).observe(queryInput, {attributes: true}); } else { // Chrome // Chrome does not fire a resize event and does not allow us to get notified when the browser changes the style.height attribute. @@ -1104,114 +1180,119 @@ class App extends React.Component { this.scrollTable.viewportChange(); } render() { - let { model } = this.props; + let {model} = this.props; return h("div", {}, - h("div", { id: "user-info" }, - h("a", { href: model.sfLink, className: "sf-link" }, - h("svg", { viewBox: "0 0 24 24" }, - h("path", { d: "M18.9 12.3h-1.5v6.6c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-5.1h-3.6v5.1c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-6.6H5.1c-.1 0-.3-.1-.3-.2s0-.2.1-.3l6.9-7c.1-.1.3-.1.4 0l7 7v.3c0 .1-.2.2-.3.2z" }) + h("div", {id: "user-info"}, + h("a", {href: model.sfLink, className: "sf-link"}, + h("svg", {viewBox: "0 0 24 24"}, + h("path", {d: "M18.9 12.3h-1.5v6.6c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-5.1h-3.6v5.1c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-6.6H5.1c-.1 0-.3-.1-.3-.2s0-.2.1-.3l6.9-7c.1-.1.3-.1.4 0l7 7v.3c0 .1-.2.2-.3.2z"}) ), " Salesforce Home" ), h("h1", {}, "Data Export"), h("span", {}, " / " + model.userInfo), - h("div", { className: "flex-right" }, - h("div", { id: "spinner", role: "status", className: "slds-spinner slds-spinner_small slds-spinner_inline", hidden: model.spinnerCount == 0 }, - h("span", { className: "slds-assistive-text" }), - h("div", { className: "slds-spinner__dot-a" }), - h("div", { className: "slds-spinner__dot-b" }), + h("div", {className: "flex-right"}, + h("div", {id: "spinner", role: "status", className: "slds-spinner slds-spinner_small slds-spinner_inline", hidden: model.spinnerCount == 0}, + h("span", {className: "slds-assistive-text"}), + h("div", {className: "slds-spinner__dot-a"}), + h("div", {className: "slds-spinner__dot-b"}), ), - h("a", { href: "#", id: "help-btn", title: "Export Help", onClick: this.onToggleHelp }, - h("div", { className: "icon" }) + h("a", {href: "#", id: "help-btn", title: "Export Help", onClick: this.onToggleHelp}, + h("div", {className: "icon"}) ), ), ), - h("div", { className: "area" }, - h("div", { className: "area-header" }, + h("div", {className: "area"}, + h("div", {className: "area-header"}, ), - h("div", { className: "query-controls" }, + h("div", {className: "query-controls"}, h("h1", {}, "Export Query"), - h("div", { className: "query-history-controls" }, - h("div", { className: "button-group" }, - h("select", { value: JSON.stringify(model.selectedHistoryEntry), onChange: this.onSelectHistoryEntry, className: "query-history" }, - h("option", { value: JSON.stringify(null), disabled: true }, "Query History"), - model.queryHistory.list.map(q => h("option", { key: JSON.stringify(q), value: JSON.stringify(q) }, q.query.substring(0, 300))) + h("div", {className: "query-history-controls"}, + h("select", {value: "", onChange: this.onSelectQueryTemplate, className: "query-history", title: "Check documentation to customize templates"}, + h("option", {value: null, disabled: true, defaultValue: true, hidden: true}, "Templates"), + model.queryTemplates.map(q => h("option", {key: q, value: q}, q)) + ), + h("div", {className: "button-group"}, + h("select", {value: JSON.stringify(model.selectedHistoryEntry), onChange: this.onSelectHistoryEntry, className: "query-history"}, + h("option", {value: JSON.stringify(null), disabled: true}, "Query History"), + model.queryHistory.list.map(q => h("option", {key: JSON.stringify(q), value: JSON.stringify(q)}, q.query.substring(0, 300))) ), - h("button", { onClick: this.onClearHistory, title: "Clear Query History" }, "Clear") + h("button", {onClick: this.onClearHistory, title: "Clear Query History"}, "Clear") ), - h("div", { className: "pop-menu saveOptions", hidden: !model.expandSavedOptions }, - h("a", { href: "#", onClick: this.onRemoveFromHistory, title: "Remove query from saved history" }, "Remove Saved Query"), - h("a", { href: "#", onClick: this.onClearSavedHistory, title: "Clear saved history" }, "Clear Saved Queries") + h("div", {className: "pop-menu saveOptions", hidden: !model.expandSavedOptions}, + h("a", {href: "#", onClick: this.onRemoveFromHistory, title: "Remove query from saved history"}, "Remove Saved Query"), + h("a", {href: "#", onClick: this.onClearSavedHistory, title: "Clear saved history"}, "Clear Saved Queries") ), - h("div", { className: "button-group" }, - h("select", { value: JSON.stringify(model.selectedSavedEntry), onChange: this.onSelectSavedEntry, className: "query-history" }, - h("option", { value: JSON.stringify(null), disabled: true }, "Saved Queries"), - model.savedHistory.list.map(q => h("option", { key: JSON.stringify(q), value: JSON.stringify(q) }, q.query.substring(0, 300))) + h("div", {className: "button-group"}, + h("select", {value: JSON.stringify(model.selectedSavedEntry), onChange: this.onSelectSavedEntry, className: "query-history"}, + h("option", {value: JSON.stringify(null), disabled: true}, "Saved Queries"), + model.savedHistory.list.map(q => h("option", {key: JSON.stringify(q), value: JSON.stringify(q)}, q.query.substring(0, 300))) ), - h("input", { placeholder: "Query Label", type: "save", value: model.queryName, onInput: this.onSetQueryName }), - h("button", { onClick: this.onAddToHistory, title: "Add query to saved history" }, "Save Query"), - h("button", { className: model.expandSavedOptions ? "toggle contract" : "toggle expand", title: "Show More Options", onClick: this.onToggleSavedOptions }, h("div", { className: "button-toggle-icon" })), - h("input", { placeholder: "Consumer Key", type: "default", value: model.clientId, onInput: this.onSetClientId }), - h("button", { onClick: this.onSaveClientId, title: "Save Consumer Key" }, "Save"), + h("input", {placeholder: "Query Label", type: "save", value: model.queryName, onInput: this.onSetQueryName}), + h("button", {onClick: this.onAddToHistory, title: "Add query to saved history"}, "Save Query"), + h("button", {className: model.expandSavedOptions ? "toggle contract" : "toggle expand", title: "Show More Options", onClick: this.onToggleSavedOptions}, h("div", {className: "button-toggle-icon"})), + h("input", {placeholder: "Consumer Key", type: "default", value: model.clientId, onInput: this.onSetClientId}), + h("button", {onClick: this.onSaveClientId, title: "Save Consumer Key"}, "Save"), ), ), - h("div", { className: "query-options" }, + h("div", {className: "query-options"}, h("label", {}, - h("input", { type: "checkbox", checked: model.queryAll, onChange: this.onQueryAllChange, disabled: model.queryTooling }), + h("input", {type: "checkbox", checked: model.queryAll, onChange: this.onQueryAllChange, disabled: model.queryTooling}), " ", h("span", {}, "Add deleted records?") ), - h("label", { title: "With the tooling API you can query more metadata, but you cannot query regular data" }, - h("input", { type: "checkbox", checked: model.queryTooling, onChange: this.onQueryToolingChange, disabled: model.queryAll }), + h("label", {title: "With the tooling API you can query more metadata, but you cannot query regular data"}, + h("input", {type: "checkbox", checked: model.queryTooling, onChange: this.onQueryToolingChange, disabled: model.queryAll}), " ", h("span", {}, "Tooling API?") ), ), ), - h("textarea", { id: "query", ref: "query", style: { maxHeight: (model.winInnerHeight - 200) + "px" } }), - h("div", { className: "autocomplete-box" + (model.expandAutocomplete ? " expanded" : "") }, - h("div", { className: "autocomplete-header" }, + h("textarea", {id: "query", ref: "query", style: {maxHeight: (model.winInnerHeight - 200) + "px"}}), + h("div", {className: "autocomplete-box" + (model.expandAutocomplete ? " expanded" : "")}, + h("div", {className: "autocomplete-header"}, h("span", {}, model.autocompleteResults.title), - h("div", { className: "flex-right" }, - h("button", { tabIndex: 1, disabled: model.isWorking, onClick: this.onExport, title: "Ctrl+Enter / F5", className: "highlighted" }, "Run Export"), - h("button", { tabIndex: 2, onClick: this.onCopyQuery, title: "Copy query url", className: "copy-id" }, "Export Query"), - h("a", { tabIndex: 3, className: "button", hidden: !model.autocompleteResults.sobjectName, href: model.showDescribeUrl(), target: "_blank", title: "Show field info for the " + model.autocompleteResults.sobjectName + " object" }, model.autocompleteResults.sobjectName + " Field Info"), - h("button", { tabIndex: 4, href: "#", className: model.expandAutocomplete ? "toggle contract" : "toggle expand", onClick: this.onToggleExpand, title: "Show all suggestions or only the first line" }, - h("div", { className: "button-icon" }), - h("div", { className: "button-toggle-icon" }) + h("div", {className: "flex-right"}, + h("button", {tabIndex: 1, disabled: model.isWorking, onClick: this.onExport, title: "Ctrl+Enter / F5", className: "highlighted"}, "Run Export"), + h("button", {tabIndex: 2, onClick: this.onCopyQuery, title: "Copy query url", className: "copy-id"}, "Export Query"), + h("a", {tabIndex: 3, className: "button", hidden: !model.autocompleteResults.sobjectName, href: model.showDescribeUrl(), target: "_blank", title: "Show field info for the " + model.autocompleteResults.sobjectName + " object"}, model.autocompleteResults.sobjectName + " Field Info"), + h("button", {tabIndex: 4, href: "#", className: model.expandAutocomplete ? "toggle contract" : "toggle expand", onClick: this.onToggleExpand, title: "Show all suggestions or only the first line"}, + h("div", {className: "button-icon"}), + h("div", {className: "button-toggle-icon"}) ) ), ), - h("div", { className: "autocomplete-results" }, + h("div", {className: "autocomplete-results"}, model.autocompleteResults.results.map(r => - h("div", { className: "autocomplete-result", key: r.value }, h("a", { tabIndex: 0, title: r.title, onClick: e => { e.preventDefault(); model.autocompleteClick(r); model.didUpdate(); }, href: "#", className: r.autocompleteType + ' ' + r.dataType }, h("div", { className: "autocomplete-icon" }), r.value), " ") + h("div", {className: "autocomplete-result", key: r.value}, h("a", {tabIndex: 0, title: r.title, onClick: e => { e.preventDefault(); model.autocompleteClick(r); model.didUpdate(); }, href: "#", className: r.autocompleteType + " " + r.dataType}, h("div", {className: "autocomplete-icon"}), r.value), " ") ) ), ), - h("div", { hidden: !model.showHelp, className: "help-text" }, + h("div", {hidden: !model.showHelp, className: "help-text"}, h("h3", {}, "Export Help"), - h("p", {}, "Use for quick one-off data exports. Enter a ", h("a", { href: "http://www.salesforce.com/us/developer/docs/soql_sosl/", target: "_blank" }, "SOQL query"), " in the box above and press Export."), + h("p", {}, "Use for quick one-off data exports. Enter a ", h("a", {href: "http://www.salesforce.com/us/developer/docs/soql_sosl/", target: "_blank"}, "SOQL query"), " in the box above and press Export."), h("p", {}, "Press Ctrl+Space to insert all field name autosuggestions or to load suggestions for field values."), h("p", {}, "Press Ctrl+Enter or F5 to execute the export."), h("p", {}, "Supports the full SOQL language. The columns in the CSV output depend on the returned data. Using subqueries may cause the output to grow rapidly. Bulk API is not supported. Large data volumes may freeze or crash your browser.") ) ), - h("div", { className: "area", id: "result-area" }, - h("div", { className: "result-bar" }, + h("div", {className: "area", id: "result-area"}, + h("div", {className: "result-bar"}, h("h1", {}, "Export Result"), - h("div", { className: "button-group" }, - h("button", { disabled: !model.canCopy(), onClick: this.onCopyAsExcel, title: "Copy exported data to clipboard for pasting into Excel or similar" }, "Copy (Excel format)"), - h("button", { disabled: !model.canCopy(), onClick: this.onCopyAsCsv, title: "Copy exported data to clipboard for saving as a CSV file" }, "Copy (CSV)"), - h("button", { disabled: !model.canCopy(), onClick: this.onCopyAsJson, title: "Copy raw API output to clipboard" }, "Copy (JSON)"), + h("div", {className: "button-group"}, + h("button", {disabled: !model.canCopy(), onClick: this.onCopyAsExcel, title: "Copy exported data to clipboard for pasting into Excel or similar"}, "Copy (Excel format)"), + h("button", {disabled: !model.canCopy(), onClick: this.onCopyAsCsv, title: "Copy exported data to clipboard for saving as a CSV file"}, "Copy (CSV)"), + h("button", {disabled: !model.canCopy(), onClick: this.onCopyAsJson, title: "Copy raw API output to clipboard"}, "Copy (JSON)"), + h("button", {disabled: !model.canDelete(), onClick: this.onDeleteRecords, title: "Open the 'Data Import' page with preloaded records to delete. 'Id' field needs to be queried", className: "delete-btn"}, "Delete Records"), ), - h("input", { placeholder: "Filter Results", type: "search", value: model.resultsFilter, onInput: this.onResultsFilterInput }), - h("span", { className: "result-status flex-right" }, + h("input", {placeholder: "Filter Results", type: "search", value: model.resultsFilter, onInput: this.onResultsFilterInput}), + h("span", {className: "result-status flex-right"}, h("span", {}, model.exportStatus), - h("button", { className: "cancel-btn", disabled: !model.isWorking, onClick: this.onStopExport }, "Stop"), + h("button", {className: "cancel-btn", disabled: !model.isWorking, onClick: this.onStopExport}, "Stop"), ) ), - h("textarea", { id: "result-text", readOnly: true, value: model.exportError || "", hidden: model.exportError == null }), - h("div", { id: "result-table", ref: "scroller", hidden: model.exportError != null } + h("textarea", {id: "result-text", readOnly: true, value: model.exportError || "", hidden: model.exportError == null}), + h("div", {id: "result-table", ref: "scroller", hidden: model.exportError != null} /* the scroll table goes here */ ) ) @@ -1227,16 +1308,32 @@ class App extends React.Component { sfConn.getSession(sfHost).then(() => { let root = document.getElementById("root"); - let model = new Model({ sfHost, args }); + let model = new Model({sfHost, args}); model.reactCallback = cb => { - ReactDOM.render(h(App, { model }), root, cb); + ReactDOM.render(h(App, {model}), root, cb); }; - ReactDOM.render(h(App, { model }), root); + ReactDOM.render(h(App, {model}), root); if (parent && parent.isUnitTest) { // for unit tests - parent.insextTestLoaded({ model, sfConn }); + parent.insextTestLoaded({model, sfConn}); } }); -} \ No newline at end of file +} + +function getLinkTarget(e) { + if (localStorage.getItem("openLinksInNewTab") == "true" || (e.ctrlKey || e.metaKey)) { + return "_blank"; + } else { + return "_top"; + } +} + +function getSeparator() { + let separator = ","; + if (localStorage.getItem("csvSeparator")) { + separator = localStorage.getItem("csvSeparator"); + } + return separator; +} diff --git a/addon/data-import-test.js b/addon/data-import-test.js index e74dd9f7..2ee27bed 100644 --- a/addon/data-import-test.js +++ b/addon/data-import-test.js @@ -361,6 +361,29 @@ export async function dataImportTest(test) { records = getRecords(await sfConn.rest("/services/data/v35.0/query/?q=" + encodeURIComponent("select Name, Checkbox__c, Number__c, Lookup__r.Name from Inspector_Test__c order by Name"))); assertEquals([], records); + // Delete from data-export + let separator = ","; + if (localStorage.getItem("csvSeparator")) { + separator = localStorage.getItem("csvSeparator"); + } + + let data = [ + ["_", "Id", "Name"], + ["[Account]", "0010Y00000kCUn3QAG", "GenePoint1111"], + ["[Account]", "0010Y00000kCUn1QAG", "United Oil & Gas UK2222"] + ]; + let encodedData = btoa(data.map(r => r.join(separator)).join("\r\n")); + let args = new URLSearchParams(); + args.set("data", encodedData); + + let result = await loadPage("data-import.html", args); + let importModel = result.model; + assertEquals([ + ["[Account]", "0010Y00000kCUn3QAG", "GenePoint1111"], + ["[Account]", "0010Y00000kCUn1QAG", "United Oil & Gas UK2222"] + ], importModel.importData.importTable.data); + assertEquals("delete", importModel.importAction); + // Big result // TODO Write test for clipboard copy // TODO Write test for showStatus diff --git a/addon/data-import.js b/addon/data-import.js index 82805e9f..92b28c08 100644 --- a/addon/data-import.js +++ b/addon/data-import.js @@ -6,7 +6,7 @@ import { DescribeInfo, copyToClipboard, initScrollTable } from "./data-load.js"; class Model { - constructor(sfHost) { + constructor(sfHost, args) { this.sfHost = sfHost; this.importData = undefined; this.consecutiveFailures = 0; @@ -47,6 +47,15 @@ class Model { this.userInfo = res.userFullName + " / " + res.userName + " / " + res.organizationName; })); + if (args.has("data")) { + let data = atob(args.get("data")); + this.dataFormat = "csv"; + this.setData(data); + this.importAction = "delete"; + this.importActionName = "Delete"; + this.skipAllUnknownFields(); + console.log(this.importData); + } } /** @@ -96,7 +105,11 @@ class Model { if (this.dataFormat == "json") { text = this.getDataFromJson(text); } - let separator = this.dataFormat == "excel" ? "\t" : ","; + let csvSeparator = ","; + if (localStorage.getItem("csvSeparator")) { + csvSeparator = localStorage.getItem("csvSeparator"); + } + let separator = this.dataFormat == "excel" ? "\t" : csvSeparator; let data; try { data = csvParse(text, separator); @@ -148,18 +161,23 @@ class Model { let fields = ["_"].concat(Object.keys(json[0])); fields = fields.filter(field => field != "attributes"); + let separator = ","; + if (localStorage.getItem("csvSeparator")) { + separator = localStorage.getItem("csvSeparator"); + } + let sobject = json[0]["attributes"]["type"]; if (sobject) { csv = json.map(function (row) { return fields.map(function (fieldName) { - let value = row[fieldName]; - if (value && typeof value === "string") { + let value = fieldName == "_" ? sobject : row[fieldName]; + if (typeof value == "boolean" || (value && typeof value !== "object")) { return fieldName == "_" ? '"[' + sobject + ']"' : JSON.stringify(value) } - }).join(",") + }).join(separator) }) fields = fields.map(str => '"' + str + '"'); - csv.unshift(fields.join(",")); + csv.unshift(fields.join(separator)); csv = csv.join("\r\n"); } return csv; @@ -822,7 +840,11 @@ class App extends React.Component { onCopyAsCsvClick(e) { e.preventDefault(); let { model } = this.props; - model.copyResult(","); + let separator = ","; + if (localStorage.getItem("csvSeparator")) { + separator = localStorage.getItem("csvSeparator"); + } + model.copyResult(separator); } onCopyOptionsClick(e) { e.preventDefault(); @@ -1122,7 +1144,7 @@ class StatusBox extends React.Component { sfConn.getSession(sfHost).then(() => { let root = document.getElementById("root"); - let model = new Model(sfHost); + let model = new Model(sfHost, args); model.reactCallback = cb => { ReactDOM.render(h(App, { model }), root, cb); }; diff --git a/addon/data-load.css b/addon/data-load.css index 9445c534..8e669dc2 100644 --- a/addon/data-load.css +++ b/addon/data-load.css @@ -140,6 +140,14 @@ -webkit-mask-position: center; } +.pop-menu a.query-record .icon { + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: 1rem; + -webkit-mask-image: url('chrome-extension://__MSG_@@extension_id__/images/record_lookup.svg'); + -webkit-mask-position: center; + background-color: #706E6B; +} + .pop-menu a.copy-id .icon { -webkit-mask-repeat: no-repeat; -webkit-mask-size: 1rem; diff --git a/addon/data-load.js b/addon/data-load.js index 4ebd4051..cd4c2281 100644 --- a/addon/data-load.js +++ b/addon/data-load.js @@ -179,9 +179,24 @@ function renderCell(rt, cell, td) { aShow.textContent = "Show all data"; aShow.className = "view-inspector"; let aShowIcon = document.createElement("div"); - aShowIcon.className = "icon" + aShowIcon.className = "icon"; pop.appendChild(aShow); aShow.prepend(aShowIcon); + + //Query Record + let aQuery = document.createElement("a"); + let query = "SELECT Id FROM " + objectTypes + " WHERE Id = '" + recordId + "'"; + let queryArgs = new URLSearchParams(); + queryArgs.set("host", rt.sfHost); + queryArgs.set("query", query); + aQuery.href = "data-export.html?" + queryArgs; + aQuery.target = "_blank"; + aQuery.textContent = "Query Record"; + aQuery.className = "query-record"; + let aqueryIcon = document.createElement("div"); + aqueryIcon.className = "icon"; + pop.appendChild(aQuery); + aQuery.prepend(aqueryIcon); } // If the recordId ends with 0000000000AAA it is a dummy ID such as the ID for the master record type 012000000000000AAA if (recordId && isRecordId(recordId) && !recordId.endsWith("0000000000AAA")) { @@ -195,6 +210,7 @@ function renderCell(rt, cell, td) { pop.appendChild(aView); aView.prepend(aviewIcon); } + //copy to clipboard let aCopy = document.createElement("a"); aCopy.className = "copy-id"; diff --git a/addon/explore-api.css b/addon/explore-api.css index d7b223f8..694062eb 100644 --- a/addon/explore-api.css +++ b/addon/explore-api.css @@ -12,7 +12,7 @@ select { textarea { display:block; width: 100%; - height: 15em; + height: 40em; resize: vertical; word-wrap: normal; font-size: 11px; @@ -47,3 +47,17 @@ td,th { font-weight: bold; color: red; } + +#result-table table tr:first-child { + font-weight: 700; + background-color: #FAFAF9; + border-top: none; + padding-top: 7px; + padding-bottom: 7px; +} +pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; } +.string { color: green; } +.number { color: darkorange; } +.boolean { color: blue; } +.null { color: magenta; } +.key { color: red; } \ No newline at end of file diff --git a/addon/explore-api.html b/addon/explore-api.html index f552e18a..f558ccc7 100644 --- a/addon/explore-api.html +++ b/addon/explore-api.html @@ -1,16 +1,22 @@ - - - ... - - - - -
- - - - - + + + + ... + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/addon/explore-api.js b/addon/explore-api.js index a56b31fb..12fa740a 100644 --- a/addon/explore-api.js +++ b/addon/explore-api.js @@ -257,61 +257,81 @@ class App extends React.Component { let {model} = this.props; document.title = model.title; return h("div", {}, - h("img", {id: "spinner", src: "data:image/gif;base64,R0lGODlhIAAgAPUmANnZ2fX19efn5+/v7/Ly8vPz8/j4+Orq6vz8/Pr6+uzs7OPj4/f39/+0r/8gENvb2/9NQM/Pz/+ln/Hx8fDw8P/Dv/n5+f/Sz//w7+Dg4N/f39bW1v+If/9rYP96cP8+MP/h3+Li4v8RAOXl5f39/czMzNHR0fVhVt+GgN7e3u3t7fzAvPLU0ufY1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCAAmACwAAAAAIAAgAAAG/0CTcEhMEBSjpGgJ4VyI0OgwcEhaR8us6CORShHIq1WrhYC8Q4ZAfCVrHQ10gC12k7tRBr1u18aJCGt7Y31ZDmdDYYNKhVkQU4sCFAwGFQ0eDo14VXsDJFEYHYUfJgmDAWgmEoUXBJ2pQqJ2HIpXAp+wGJluEHsUsEMefXsMwEINw3QGxiYVfQDQ0dCoxgQl19jX0tIFzAPZ2dvRB8wh4NgL4gAPuKkIEeclAArqAALAGvElIwb1ABOpFOgrgSqDv1tREOTTt0FIAX/rDhQIQGBACHgDFQxJBxHawHBFHnQE8PFaBAtQHnYsWWKAlAkrP2r0UkBkvYERXKZKwFGcPhcAKI1NMLjt3IaZzIQYUNATG4AR1LwEAQAh+QQFCAAtACwAAAAAIAAgAAAG3MCWcEgstkZIBSFhbDqLyOjoEHhaodKoAnG9ZqUCxpPwLZtHq2YBkDq7R6dm4gFgv8vx5qJeb9+jeUYTfHwpTQYMFAKATxmEhU8kA3BPBo+EBFZpTwqXdQJdVnuXD6FWngAHpk+oBatOqFWvs10VIre4t7RFDbm5u0QevrjAQhgOwyIQxS0dySIcVipWLM8iF08mJRpcTijJH0ITRtolJREhA5lG374STuXm8iXeuctN8fPmT+0OIPj69Fn51qCJioACqT0ZEAHhvmIWADhkJkTBhoAUhwQYIfGhqSAAIfkEBQgAJgAsAAAAACAAIAAABshAk3BINCgWgCRxyWwKC5mkFOCsLhPIqdTKLTy0U251AtZyA9XydMRuu9mMtBrwro8ECHnZXldYpw8HBWhMdoROSQJWfAdcE1YBfCMJYlYDfASVVSQCdn6aThR8oE4Mo6RMBnwlrK2smahLrq4DsbKzrCG2RAC4JRF5uyYjviUawiYBxSWfThJcG8VVGB0iIlYKvk0VDR4O1tZ/s07g5eFOFhGtVebmVQOsVu3uTs3k8+DPtvgiDg3C+CCAQNbugz6C1iBwuGAlCAAh+QQFCAAtACwAAAAAIAAgAAAG28CWcEgstgDIhcJgbBYnTaQUkIE6r8bpdJHAeo9a6aNwVYXPaAChOSiZ0nBAqmmJlNzx8zx6v7/zUntGCn19Jk0BBQcPgVcbhYZYAnJXAZCFKlhrVyOXdxpfWACeEQihV54lIaeongOsTqmbsLReBiO4ubi1RQy6urxEFL+5wUIkAsQjCsYtA8ojs00sWCvQI11OKCIdGFcnygdX2yIiDh4NFU3gvwHa5fDx8uXsuMxN5PP68OwCpkb59gkEx2CawIPwVlxp4EBgMxAQ9jUTIuHDvIlDLnCIWA5WEAAh+QQFCAAmACwAAAAAIAAgAAAGyUCTcEgMjAClJHHJbAoVm6S05KwuLcip1ModRLRTblUB1nIn1fIUwG672YW0uvSuAx4JedleX1inESEDBE12cXIaCFV8GVwKVhN8AAZiVgJ8j5VVD3Z+mk4HfJ9OBaKjTAF8IqusqxWnTK2tDbBLsqwetUQQtyIOGLpCHL0iHcEmF8QiElYBXB/EVSQDIyNWEr1NBgwUAtXVVrytTt/l4E4gDqxV5uZVDatW7e5OzPLz3861+CMCDMH4FCgCaO6AvmMtqikgkKdKEAAh+QQFCAAtACwAAAAAIAAgAAAG28CWcEgstkpIwChgbDqLyGhpo3haodIowHK9ZqWRwZP1LZtLqmZDhDq7S6YmyCFiv8vxJqReb9+jeUYSfHwoTQQDIRGARhNCH4SFTwgacE8XkYQsVmlPHJl1HV1We5kOGKNPoCIeqaqgDa5OqxWytqMBALq7urdFBby8vkQHwbvDQw/GAAvILQLLAFVPK1YE0QAGTycjAyRPKcsZ2yPlAhQM2kbhwY5N3OXx5U7sus3v8vngug8J+PnyrIQr0GQFQH3WnjAQcHAeMgQKGjoTEuAAwIlDEhCIGM9VEAAh+QQFCAAmACwAAAAAIAAgAAAGx0CTcEi8cCCiJHHJbAoln6RU5KwuQcip1MptOLRTblUC1nIV1fK0xG672YO0WvSulyIWedleB1inDh4NFU12aHIdGFV8G1wSVgp8JQFiVhp8I5VVCBF2fppOIXygTgOjpEwEmCOsrSMGqEyurgyxS7OtFLZECrgjAiS7QgS+I3HCCcUjlFUTXAfFVgIAn04Bvk0BBQcP1NSQs07e499OCAKtVeTkVQysVuvs1lzx48629QAPBcL1CwnCTKzLwC+gQGoLFMCqEgQAIfkEBQgALQAsAAAAACAAIAAABtvAlnBILLZESAjnYmw6i8io6CN5WqHSKAR0vWaljsZz9S2bRawmY3Q6u0WoJkIwYr/L8aaiXm/fo3lGAXx8J00VDR4OgE8HhIVPGB1wTwmPhCtWaU8El3UDXVZ7lwIkoU+eIxSnqJ4MrE6pBrC0oQQluLm4tUUDurq8RCG/ucFCCBHEJQDGLRrKJSNWBFYq0CUBTykAAlYmyhvaAOMPBwXZRt+/Ck7b4+/jTuq4zE3u8O9P6hEW9vj43kqAMkLgH8BqTwo8MBjPWIIFDJsJmZDhX5MJtQwogNjwVBAAOw==", hidden: model.spinnerCount == 0}), - h("a", {href: model.sfLink}, "Salesforce Home"), - " \xa0 ", - h("span", {}, model.userInfo), - model.apiResponse && h("div", {}, - h("ul", {}, - h("li", {className: model.apiResponse.status == "Error" ? "status-error" : "status-success"}, "Status: " + model.apiResponse.status), - model.apiResponse.textViews.map(textView => - h("li", {key: textView.name}, - h("label", {}, - h("input", {type: "radio", name: "textView", checked: model.selectedTextView == textView, onChange: () => { model.selectedTextView = textView; model.didUpdate(); }}), - " " + textView.name - ) - ) - ) + h("div", {id: "user-info"}, + h("a", {href: model.sfLink, className: "sf-link"}, + h("svg", {viewBox: "0 0 24 24"}, + h("path", {d: "M18.9 12.3h-1.5v6.6c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-5.1h-3.6v5.1c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-6.6H5.1c-.1 0-.3-.1-.3-.2s0-.2.1-.3l6.9-7c.1-.1.3-.1.4 0l7 7v.3c0 .1-.2.2-.3.2z"}) + ), + " Salesforce Home" ), - model.selectedTextView && !model.selectedTextView.table && h("div", {}, - h("textarea", {readOnly: true, value: model.selectedTextView.value}) + h("h1", {}, "Explore API"), + h("span", {}, " / " + model.userInfo), + h("div", {className: "flex-right"}, + h("div", {id: "spinner", role: "status", className: "slds-spinner slds-spinner_small slds-spinner_inline", hidden: model.spinnerCount == 0}, + h("span", {className: "slds-assistive-text"}), + h("div", {className: "slds-spinner__dot-a"}), + h("div", {className: "slds-spinner__dot-b"}), + ), + ), + ), + h("div", {className: "area", id: "result-area"}, + h("div", {className: "result-bar"}, + h("h1", {}, "Request Result") ), - model.selectedTextView && model.selectedTextView.table && h("div", {}, - h("table", {}, - h("tbody", {}, - model.selectedTextView.table.map((row, key) => - h("tr", {key}, - row.map((cell, key) => - h("td", {key}, "" + cell) + h("div", {id: "result-table", ref: "scroller"}, + model.apiResponse && h("div", {}, + h("ul", {}, + h("li", {className: model.apiResponse.status == "Error" ? "status-error" : "status-success"}, "Status: " + model.apiResponse.status), + model.apiResponse.textViews.map(textView => + h("li", {key: textView.name}, + h("label", {}, + h("input", {type: "radio", name: "textView", checked: model.selectedTextView == textView, onChange: () => { model.selectedTextView = textView; model.didUpdate(); }}), + " " + textView.name ) ) ) + ), + model.selectedTextView && !model.selectedTextView.table && h("div", {}, + h("textarea", {readOnly: true, value: model.selectedTextView.value}) + ), + model.selectedTextView && model.selectedTextView.table && h("div", {}, + h("table", {className: "scrolltable-scrolled"}, + h("tbody", {}, + model.selectedTextView.table.map((row, key) => + h("tr", {key}, + row.map((cell, key) => + h("td", {key, className: "scrolltable-cell"}, "" + cell) + ) + ) + ) + ) + ) + ), + model.apiResponse.apiGroupUrls && h("ul", {}, + model.apiResponse.apiGroupUrls.map((apiGroupUrl, key) => + h("li", {key}, + h("a", {href: model.openGroupUrl(apiGroupUrl)}, apiGroupUrl.jsonPath), + " - " + apiGroupUrl.label + ) + ) + ), + model.apiResponse.apiSubUrls && h("ul", {}, + model.apiResponse.apiSubUrls.map((apiSubUrl, key) => + h("li", {key}, + h("a", {href: model.openSubUrl(apiSubUrl)}, apiSubUrl.jsonPath), + " - " + apiSubUrl.label + ) + ) ) - ) - ), - model.apiResponse.apiGroupUrls && h("ul", {}, - model.apiResponse.apiGroupUrls.map((apiGroupUrl, key) => - h("li", {key}, - h("a", {href: model.openGroupUrl(apiGroupUrl)}, apiGroupUrl.jsonPath), - " - " + apiGroupUrl.label - ) - ) + ), + h("a", {href: "https://www.salesforce.com/us/developer/docs/api_rest/", target: "_blank"}, "REST API documentation"), + " Open your browser's ", + h("b", {}, "F12 Developer Tools"), + " and select the ", + h("b", {}, "Console"), + " tab to make your own API calls." ), - model.apiResponse.apiSubUrls && h("ul", {}, - model.apiResponse.apiSubUrls.map((apiSubUrl, key) => - h("li", {key}, - h("a", {href: model.openSubUrl(apiSubUrl)}, apiSubUrl.jsonPath), - " - " + apiSubUrl.label - ) - ) - ) - ), - h("a", {href: "https://www.salesforce.com/us/developer/docs/api_rest/", target: "_blank"}, "REST API documentation"), - " Open your browser's ", - h("b", {}, "F12 Developer Tools"), - " and select the ", - h("b", {}, "Console"), - " tab to make your own API calls." + ) ); } diff --git a/addon/inspect.js b/addon/inspect.js index 77918d52..72d18490 100644 --- a/addon/inspect.js +++ b/addon/inspect.js @@ -1,7 +1,7 @@ /* global React ReactDOM */ -import { sfConn, apiVersion } from "./inspector.js"; +import {sfConn, apiVersion} from "./inspector.js"; /* global initButton */ -import { getObjectSetupLinks, getFieldSetupLinks } from "./setup-links.js"; +import {getObjectSetupLinks, getFieldSetupLinks} from "./setup-links.js"; class Model { constructor(sfHost) { @@ -13,6 +13,7 @@ class Model { this.objectData = null; this.recordData = null; this.layoutInfo = null; + this.entityDefinitionDurableId = null; // URL parameters this.sobjectName = null; @@ -110,13 +111,13 @@ class Model { } }); } - this.detailsBox = { rows: fieldDetails, name, detailsFilterList }; + this.detailsBox = {rows: fieldDetails, name, detailsFilterList}; } showObjectMetadata() { let objectDescribe = this.objectData; let props = {}; - addProperties(props, objectDescribe, "desc.", { fields: true, childRelationships: true }); - addProperties(props, this.layoutInfo, "layout.", { detailLayoutSections: true, editLayoutSections: true, relatedLists: true }); + addProperties(props, objectDescribe, "desc.", {fields: true, childRelationships: true}); + addProperties(props, this.layoutInfo, "layout.", {detailLayoutSections: true, editLayoutSections: true, relatedLists: true}); this.showDetailsBox(this.objectName(), props, null); } canUpdate() { @@ -147,7 +148,7 @@ class Model { let recordUrl = this.objectData.urls.rowTemplate.replace("{ID}", this.recordData.Id); this.spinFor( "saving record", - sfConn.rest(recordUrl, { method: "PATCH", body: record }).then(() => { + sfConn.rest(recordUrl, {method: "PATCH", body: record}).then(() => { this.endEdit(); this.clearRecordData(); this.setRecordData(sfConn.rest(recordUrl)); @@ -157,7 +158,7 @@ class Model { let recordUrl = this.objectData.urls.rowTemplate.replace("{ID}", this.recordData.Id); this.spinFor( "deleting record", - sfConn.rest(recordUrl, { method: "DELETE" }).then(() => { + sfConn.rest(recordUrl, {method: "DELETE"}).then(() => { this.endEdit(); let args = new URLSearchParams(); args.set("host", this.sfHost); @@ -174,7 +175,7 @@ class Model { let recordUrl = this.objectData.urls.sobject; this.spinFor( "creating record", - sfConn.rest(recordUrl, { method: "POST", body: record }).then(result => { + sfConn.rest(recordUrl, {method: "POST", body: record}).then(result => { this.endEdit(); let args = new URLSearchParams(); args.set("host", this.sfHost); @@ -223,7 +224,7 @@ class Model { } editLayoutLink() { if (this.layoutInfo && this.layoutInfo.id) { - return "https://" + this.sfHost + "//layouteditor/layoutEditor.apexp?type=" + this.sobjectName + "&lid=" + this.layoutInfo.id; + return "https://" + this.sfHost + "/lightning/setup/ObjectManager/" + this.entityDefinitionDurableId + "/PageLayouts/" + this.layoutInfo.id + "/view"; } return undefined; } @@ -280,7 +281,7 @@ class Model { return undefined; }).then(layoutDescribe => { if (layoutDescribe) { - for (let layoutType of [{ sections: "detailLayoutSections", property: "detailLayoutInfo" }, { sections: "editLayoutSections", property: "editLayoutInfo" }]) { + for (let layoutType of [{sections: "detailLayoutSections", property: "detailLayoutInfo"}, {sections: "editLayoutSections", property: "editLayoutInfo"}]) { layoutDescribe[layoutType.sections].forEach((section, sectionIndex) => { section.layoutRows.forEach((row, rowIndex) => { row.layoutItems.forEach((item, itemIndex) => { @@ -366,9 +367,12 @@ class Model { // Therefore qe query the minimum set of meta-fields needed by our main UI. this.spinFor( "querying tooling particles", - sfConn.rest("/services/data/v" + apiVersion + "/tooling/query/?q=" + encodeURIComponent("select QualifiedApiName, Label, DataType, ReferenceTo, Length, Precision, Scale, IsAutonumber, IsCaseSensitive, IsDependentPicklist, IsEncrypted, IsIdLookup, IsHtmlFormatted, IsNillable, IsUnique, IsCalculated, InlineHelpText, FieldDefinition.DurableId from EntityParticle where EntityDefinition.QualifiedApiName = '" + this.sobjectName + "'")).then(res => { + sfConn.rest("/services/data/v" + apiVersion + "/tooling/query/?q=" + encodeURIComponent("SELECT QualifiedApiName, Label, DataType, ReferenceTo, Length, Precision, Scale, IsAutonumber, IsCaseSensitive, IsDependentPicklist, IsEncrypted, IsIdLookup, IsHtmlFormatted, IsNillable, IsUnique, IsCalculated, InlineHelpText, FieldDefinition.DurableId, EntityDefinition.DurableId FROM EntityParticle WHERE EntityDefinition.QualifiedApiName = '" + this.sobjectName + "'")).then(res => { for (let entityParticle of res.records) { this.fieldRows.getRow(entityParticle.QualifiedApiName).entityParticle = entityParticle; + if (!this.entityDefinitionDurableId){ + this.entityDefinitionDurableId = entityParticle.EntityDefinition.DurableId; + } } this.hasEntityParticles = true; this.fieldRows.resortRows(); @@ -422,8 +426,8 @@ class RowList { resortRows() { let s = v => v === undefined ? "\uFFFD" - : v == null ? "" - : String(v).trim(); + : v == null ? "" + : String(v).trim(); this.rows.sort((a, b) => this._sortDir * s(a.sortKey(this._sortCol)).localeCompare(s(b.sortKey(this._sortCol)))); } initColumns(cols) { @@ -488,17 +492,17 @@ class FieldRowList extends RowList { name: col, label: col == "name" ? "Field API Name" - : col == "label" ? "Label" - : col == "type" ? "Type" - : col == "value" ? "Value" - : col == "helptext" ? "Help text" - : col == "desc" ? "Description" - : col, + : col == "label" ? "Label" + : col == "type" ? "Type" + : col == "value" ? "Value" + : col == "helptext" ? "Help text" + : col == "desc" ? "Description" + : col, className: this.getColumnClassName(col), reactElement: col == "value" ? FieldValueCell - : col == "type" ? FieldTypeCell - : DefaultCell, + : col == "type" ? FieldTypeCell + : DefaultCell, columnFilter: "" }; } @@ -530,10 +534,10 @@ class ChildRowList extends RowList { name: col, label: col == "name" ? "Relationship Name" - : col == "object" ? "Child Object" - : col == "field" ? "Field" - : col == "label" ? "Label" - : col, + : col == "object" ? "Child Object" + : col == "field" ? "Field" + : col == "label" ? "Label" + : col, className: "child-column" + (this.model.showTableBorder ? " border-cell" : ""), reactElement: col == "object" ? ChildObjectCell : DefaultCell, columnFilter: "" @@ -582,7 +586,7 @@ class FieldRow extends TableRow { rowProperties() { let props = {}; if (typeof this.dataTypedValue != "undefined") { - addProperties(props, { dataValue: this.dataTypedValue }, "", {}); + addProperties(props, {dataValue: this.dataTypedValue}, "", {}); } if (this.fieldDescribe) { addProperties(props, this.fieldDescribe, "desc.", {}); @@ -595,21 +599,21 @@ class FieldRow extends TableRow { } if (this.detailLayoutInfo) { addProperties(props, this.detailLayoutInfo.indexes, "layout.", {}); - addProperties(props, this.detailLayoutInfo.section, "layoutSection.", { layoutRows: true }); - addProperties(props, this.detailLayoutInfo.row, "layoutRow.", { layoutItems: true }); - addProperties(props, this.detailLayoutInfo.item, "layoutItem.", { layoutComponents: true }); - addProperties(props, this.detailLayoutInfo.component, "layoutComponent.", { details: true, components: true }); + addProperties(props, this.detailLayoutInfo.section, "layoutSection.", {layoutRows: true}); + addProperties(props, this.detailLayoutInfo.row, "layoutRow.", {layoutItems: true}); + addProperties(props, this.detailLayoutInfo.item, "layoutItem.", {layoutComponents: true}); + addProperties(props, this.detailLayoutInfo.component, "layoutComponent.", {details: true, components: true}); } else if (this.rowList.model.layoutInfo) { - addProperties(props, { shownOnLayout: false }, "layout.", {}); + addProperties(props, {shownOnLayout: false}, "layout.", {}); } if (this.editLayoutInfo) { addProperties(props, this.editLayoutInfo.indexes, "editLayout.", {}); - addProperties(props, this.editLayoutInfo.section, "editLayoutSection.", { layoutRows: true }); - addProperties(props, this.editLayoutInfo.row, "editLayoutRow.", { layoutItems: true }); - addProperties(props, this.editLayoutInfo.item, "editLayoutItem.", { layoutComponents: true }); - addProperties(props, this.editLayoutInfo.component, "editLayoutComponent.", { details: true, components: true }); + addProperties(props, this.editLayoutInfo.section, "editLayoutSection.", {layoutRows: true}); + addProperties(props, this.editLayoutInfo.row, "editLayoutRow.", {layoutItems: true}); + addProperties(props, this.editLayoutInfo.item, "editLayoutItem.", {layoutComponents: true}); + addProperties(props, this.editLayoutInfo.component, "editLayoutComponent.", {details: true, components: true}); } else if (this.rowList.model.layoutInfo) { - addProperties(props, { shownOnLayout: false }, "editLayout.", {}); + addProperties(props, {shownOnLayout: false}, "editLayout.", {}); } return props; } @@ -702,9 +706,10 @@ class FieldRow extends TableRow { this.fieldActionsOpen = !this.fieldActionsOpen; if (this.fieldActionsOpen && !this.fieldSetupLinksRequested) { this.fieldSetupLinksRequested = true; + let isCustomSetting = this.rowList.model.objectData?.customSetting; this.rowList.model.spinFor( "getting field setup links for" + this.fieldName, - getFieldSetupLinks(this.rowList.model.sfHost, this.rowList.model.objectName(), this.fieldName) + getFieldSetupLinks(this.rowList.model.sfHost, this.rowList.model.objectName(), this.fieldName, isCustomSetting) .then(setupLinks => this.fieldSetupLinks = setupLinks) ); } @@ -719,7 +724,7 @@ class FieldRow extends TableRow { + (fieldDescribe.calculatedFormula ? "Formula: " + fieldDescribe.calculatedFormula + "\n" : "") + (fieldDescribe.inlineHelpText ? "Help text: " + fieldDescribe.inlineHelpText + "\n" : "") + (fieldDescribe.picklistValues && fieldDescribe.picklistValues.length > 0 ? "Picklist values: " + fieldDescribe.picklistValues.map(pickval => pickval.value).join(", ") + "\n" : "") - ; + ; } // Entity particle does not contain any of this information return this.fieldName + "\n(Details not available)"; @@ -790,13 +795,13 @@ class FieldRow extends TableRow { args.set("useToolingApi", "1"); } args.set("recordId", recordId); - return { href: "inspect.html?" + args, text: "Show all data (" + sobject.name + ")" }; + return {href: "inspect.html?" + args, text: "Show all data (" + sobject.name + ")"}; }); } else { links = []; } - links.push({ href: this.idLink(), text: "View in Salesforce" }); - links.push({ href: "#", text: "Copy Id", className: "copy-id", id: this.dataTypedValue }); + links.push({href: this.idLink(), text: "View in Salesforce"}); + links.push({href: "#", text: "Copy Id", className: "copy-id", id: this.dataTypedValue}); this.recordIdPop = links; } showReferenceUrl(type) { @@ -852,7 +857,7 @@ class ChildRow extends TableRow { if (this.relatedListInfo) { addProperties(props, this.relatedListInfo, "layout.", {}); } else if (this.rowList.model.layoutInfo) { - addProperties(props, { shownOnLayout: false }, "layout.", {}); + addProperties(props, {shownOnLayout: false}, "layout.", {}); } return props; } @@ -908,9 +913,10 @@ class ChildRow extends TableRow { this.childSetupLinksRequested = true; let sobjectName = (this.childDescribe && this.childDescribe.childSObject) || (this.relatedListInfo && this.relatedListInfo.relatedList.sobject); let fieldName = (this.childDescribe && this.childDescribe.field) || (this.relatedListInfo && this.relatedListInfo.relatedList.field); + let isCustomSetting = this.rowList.model.objectData?.customSetting; this.rowList.model.spinFor( "getting relationship setup links for " + this.childName, - getFieldSetupLinks(this.rowList.model.sfHost, sobjectName, fieldName) + getFieldSetupLinks(this.rowList.model.sfHost, sobjectName, fieldName, isCustomSetting) .then(setupLinks => this.childSetupLinks = setupLinks) ); } @@ -974,154 +980,156 @@ class App extends React.Component { this.refs.rowsFilter.focus(); } onUseAllTab(e) { - let { model } = this.props; + let {model} = this.props; e.preventDefault(); model.useTab = "all"; model.didUpdate(); } onUseFieldsTab(e) { - let { model } = this.props; + let {model} = this.props; e.preventDefault(); model.useTab = "fields"; model.didUpdate(); } onUseChildsTab(e) { - let { model } = this.props; + let {model} = this.props; e.preventDefault(); model.useTab = "childs"; model.didUpdate(); } onRowsFilterInput(e) { - let { model } = this.props; + let {model} = this.props; model.rowsFilter = e.target.value; model.didUpdate(); } onClearAndFocusFilter(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.rowsFilter = ""; this.refs.rowsFilter.focus(); model.didUpdate(); } onShowObjectMetadata(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.showObjectMetadata(); model.didUpdate(); } onToggleObjectActions() { - let { model } = this.props; + let {model} = this.props; model.toggleObjectActions(); model.didUpdate(); } onDoUpdate(e) { e.currentTarget.disabled = true; - let { model } = this.props; + let {model} = this.props; model.doUpdate(); model.didUpdate(); e.currentTarget.disabled = false; } onDoDelete(e) { e.currentTarget.disabled = true; - let { model } = this.props; + let {model} = this.props; model.doDelete(); model.didUpdate(); e.currentTarget.disabled = false; } onDoCreate(e) { e.currentTarget.disabled = true; - let { model } = this.props; + let {model} = this.props; model.doCreate(); model.didUpdate(); e.currentTarget.disabled = false; } onDoSave(e) { e.currentTarget.disabled = true; - let { model } = this.props; + let {model} = this.props; model.doSave(); model.didUpdate(); e.currentTarget.disabled = false; } onCancelEdit() { - let { model } = this.props; + let {model} = this.props; model.cancelEdit(); model.didUpdate(); } onUpdateTableBorderSettings() { - let { model } = this.props; + let {model} = this.props; model.updateShowTableBorder(); model.reloadTables(); model.didUpdate(); // Save to local storage } render() { - let { model } = this.props; + let {model} = this.props; document.title = model.title(); + let linkInNewTab = localStorage.getItem("openLinksInNewTab"); + let linkTarget = linkInNewTab ? "_blank" : "_top"; return ( h("div", {}, - h("div", { className: "object-bar" }, - h("div", { className: "flex-right" }, - h("div", { id: "spinner", role: "status", className: "slds-spinner slds-spinner_large", hidden: model.spinnerCount == 0 }, - h("span", { className: "slds-assistive-text" }), - h("div", { className: "slds-spinner__dot-a" }), - h("div", { className: "slds-spinner__dot-b" }), + h("div", {className: "object-bar"}, + h("div", {className: "flex-right"}, + h("div", {id: "spinner", role: "status", className: "slds-spinner slds-spinner_large", hidden: model.spinnerCount == 0}, + h("span", {className: "slds-assistive-text"}), + h("div", {className: "slds-spinner__dot-a"}), + h("div", {className: "slds-spinner__dot-b"}), ) ), - h("a", { href: model.sfLink, className: "sf-link" }, - h("svg", { viewBox: "0 0 24 24" }, - h("path", { d: "M18.9 12.3h-1.5v6.6c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-5.1h-3.6v5.1c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-6.6H5.1c-.1 0-.3-.1-.3-.2s0-.2.1-.3l6.9-7c.1-.1.3-.1.4 0l7 7v.3c0 .1-.2.2-.3.2z" }) + h("a", {href: model.sfLink, target: linkTarget, className: "sf-link"}, + h("svg", {viewBox: "0 0 24 24"}, + h("path", {d: "M18.9 12.3h-1.5v6.6c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-5.1h-3.6v5.1c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-6.6H5.1c-.1 0-.3-.1-.3-.2s0-.2.1-.3l6.9-7c.1-.1.3-.1.4 0l7 7v.3c0 .1-.2.2-.3.2z"}) ), " Salesforce Home" ), - h("span", { className: "object-tab" + (model.useTab == "all" ? " active-tab" : "") }, - h("a", { href: "about:blank", onClick: this.onUseAllTab }, "All") + h("span", {className: "object-tab" + (model.useTab == "all" ? " active-tab" : "")}, + h("a", {href: "about:blank", onClick: this.onUseAllTab}, "All") ), - h("span", { className: "object-tab" + (model.useTab == "fields" ? " active-tab" : "") }, - h("a", { href: "about:blank", className: "tab-with-icon", onClick: this.onUseFieldsTab }, "Fields"), + h("span", {className: "object-tab" + (model.useTab == "fields" ? " active-tab" : "")}, + h("a", {href: "about:blank", className: "tab-with-icon", onClick: this.onUseFieldsTab}, "Fields"), h(ColumnsVisibiltyBox, { rowList: model.fieldRows, label: "Field columns", content: () => [ - h(ColumnVisibiltyToggle, { rowList: model.fieldRows, key: "name", name: "name", disabled: true }), - h(ColumnVisibiltyToggle, { rowList: model.fieldRows, key: "label", name: "label" }), - h(ColumnVisibiltyToggle, { rowList: model.fieldRows, key: "type", name: "type" }), - h(ColumnVisibiltyToggle, { rowList: model.fieldRows, key: "value", name: "value", disabled: !model.canView() }), - h(ColumnVisibiltyToggle, { rowList: model.fieldRows, key: "helptext", name: "helptext" }), - h(ColumnVisibiltyToggle, { rowList: model.fieldRows, key: "desc", name: "desc", disabled: !model.hasEntityParticles }), - h("hr", { key: "---" }), - model.fieldRows.availableColumns.map(col => h(ColumnVisibiltyToggle, { key: col, name: col, label: col, rowList: model.fieldRows })) + h(ColumnVisibiltyToggle, {rowList: model.fieldRows, key: "name", name: "name", disabled: true}), + h(ColumnVisibiltyToggle, {rowList: model.fieldRows, key: "label", name: "label"}), + h(ColumnVisibiltyToggle, {rowList: model.fieldRows, key: "type", name: "type"}), + h(ColumnVisibiltyToggle, {rowList: model.fieldRows, key: "value", name: "value", disabled: !model.canView()}), + h(ColumnVisibiltyToggle, {rowList: model.fieldRows, key: "helptext", name: "helptext"}), + h(ColumnVisibiltyToggle, {rowList: model.fieldRows, key: "desc", name: "desc", disabled: !model.hasEntityParticles}), + h("hr", {key: "---"}), + model.fieldRows.availableColumns.map(col => h(ColumnVisibiltyToggle, {key: col, name: col, label: col, rowList: model.fieldRows})) ] }) ), - h("span", { className: "object-tab" + (model.useTab == "childs" ? " active-tab" : "") }, - h("a", { href: "about:blank", className: "tab-with-icon", onClick: this.onUseChildsTab }, "Relations"), + h("span", {className: "object-tab" + (model.useTab == "childs" ? " active-tab" : "")}, + h("a", {href: "about:blank", className: "tab-with-icon", onClick: this.onUseChildsTab}, "Relations"), h(ColumnsVisibiltyBox, { rowList: model.childRows, label: "Relationship columns", content: () => [ - ["name", "object", "field", "label"].map(col => h(ColumnVisibiltyToggle, { key: col, rowList: model.childRows, name: col })), - h("hr", { key: "---" }), - model.childRows.availableColumns.map(col => h(ColumnVisibiltyToggle, { key: col, rowList: model.childRows, name: col })) + ["name", "object", "field", "label"].map(col => h(ColumnVisibiltyToggle, {key: col, rowList: model.childRows, name: col})), + h("hr", {key: "---"}), + model.childRows.availableColumns.map(col => h(ColumnVisibiltyToggle, {key: col, rowList: model.childRows, name: col})) ] }) ), - h("div", { className: "object-name" }, - h("span", { className: "quick-select" }, model.objectName()), + h("div", {className: "object-name"}, + h("span", {className: "quick-select"}, model.objectName()), " ", model.recordHeading() ), - model.useTab != "all" ? null : h("div", { className: "filter-box" }, - h("svg", { className: "filter-icon" }, - h("use", { xlinkHref: "symbols.svg#search" }) + model.useTab != "all" ? null : h("div", {className: "filter-box"}, + h("svg", {className: "filter-icon"}, + h("use", {xlinkHref: "symbols.svg#search"}) ), - h("input", { className: "filter-input", placeholder: "Filter", value: model.rowsFilter, onChange: this.onRowsFilterInput, ref: "rowsFilter" }), - h("a", { href: "about:blank", className: "filter-clear", onClick: this.onClearAndFocusFilter }, - h("svg", { className: "filter-clear-icon" }, - h("use", { xlinkHref: "symbols.svg#clear" }) + h("input", {className: "filter-input", placeholder: "Filter", value: model.rowsFilter, onChange: this.onRowsFilterInput, ref: "rowsFilter"}), + h("a", {href: "about:blank", className: "filter-clear", onClick: this.onClearAndFocusFilter}, + h("svg", {className: "filter-clear-icon"}, + h("use", {xlinkHref: "symbols.svg#clear"}) ) ) ), - h("span", { className: "object-actions" }, + h("span", {className: "object-actions"}, model.editMode == null && model.recordData && (model.useTab == "all" || model.useTab == "fields") ? h("button", { title: "Inline edit the values of this record", className: "button", @@ -1140,45 +1148,45 @@ class App extends React.Component { disabled: !model.canCreate(), onClick: this.onDoCreate }, model.recordData ? "Clone" : "New") : null, - model.exportLink() ? h("a", { href: model.exportLink(), title: "Export data from this object", className: "button" }, "Export") : null, - model.objectName() ? h("a", { href: "about:blank", onClick: this.onShowObjectMetadata, className: "button" }, "More") : null, - h("button", { className: "button", onClick: this.onToggleObjectActions }, - h("svg", { className: "button-icon" }, - h("use", { xlinkHref: "symbols.svg#down" }) + model.exportLink() ? h("a", {href: model.exportLink(), target: linkTarget, title: "Export data from this object", className: "button"}, "Export") : null, + model.objectName() ? h("a", {href: "about:blank", onClick: this.onShowObjectMetadata, className: "button"}, "More") : null, + h("button", {className: "button", onClick: this.onToggleObjectActions}, + h("svg", {className: "button-icon"}, + h("use", {xlinkHref: "symbols.svg#down"}) ) ), - model.objectActionsOpen && h("div", { className: "pop-menu" }, - model.viewLink() ? h("a", { href: model.viewLink() }, "View record in Salesforce") : null, - model.editLayoutLink() ? h("a", { href: model.editLayoutLink() }, "Edit page layout") : null, - model.objectSetupLinks && h("a", { href: model.objectSetupLinks.lightningSetupLink }, "Object setup (Lightning)"), - model.objectSetupLinks && h("a", { href: model.objectSetupLinks.classicSetupLink }, "Object setup (Classic)") + model.objectActionsOpen && h("div", {className: "pop-menu"}, + model.viewLink() ? h("a", {href: model.viewLink()}, "View record in Salesforce") : null, + model.editLayoutLink() ? h("a", {href: model.editLayoutLink(), target: linkTarget}, "Edit page layout") : null, + model.objectSetupLinks && h("a", {href: model.objectSetupLinks.lightningSetupLink, target: linkTarget}, "Object setup (Lightning)"), + model.objectSetupLinks && h("a", {href: model.objectSetupLinks.classicSetupLink, target: linkTarget}, "Object setup (Classic)") ) ) ), - h("div", { className: "table-container " + (model.fieldRows.selectedColumnMap.size < 2 && model.childRows.selectedColumnMap.size < 2 ? "empty " : "") }, - h("div", { hidden: model.errorMessages.length == 0, className: "error-message" }, model.errorMessages.map((data, index) => h("div", { key: index }, data))), + h("div", {className: "table-container " + (model.fieldRows.selectedColumnMap.size < 2 && model.childRows.selectedColumnMap.size < 2 ? "empty " : "")}, + h("div", {hidden: model.errorMessages.length == 0, className: "error-message"}, model.errorMessages.map((data, index) => h("div", {key: index}, data))), model.useTab == "all" || model.useTab == "fields" ? h(RowTable, { model, rowList: model.fieldRows, - actionsColumn: { className: "field-actions" + (model.showTableBorder ? " border-cell" : ""), reactElement: FieldActionsCell }, + actionsColumn: {className: "field-actions" + (model.showTableBorder ? " border-cell" : ""), reactElement: FieldActionsCell}, classNameForRow: row => (row.fieldIsCalculated() ? "fieldCalculated " : "") + (row.fieldIsHidden() ? "fieldHidden " : ""), onUpdateTableBorderSettings: this.onUpdateTableBorderSettings }) : null, model.useTab == "all" || model.useTab == "childs" ? h(RowTable, { model, rowList: model.childRows, - actionsColumn: { className: "child-actions" + (model.showTableBorder ? " border-cell" : ""), reactElement: ChildActionsCell }, + actionsColumn: {className: "child-actions" + (model.showTableBorder ? " border-cell" : ""), reactElement: ChildActionsCell}, classNameForRow: () => "", onUpdateTableBorderSettings: this.onUpdateTableBorderSettings }) : null ), - model.editMode != null && (model.useTab == "all" || model.useTab == "fields") ? h("div", { className: "footer-edit-bar" }, h("span", { className: "edit-bar" }, + model.editMode != null && (model.useTab == "all" || model.useTab == "fields") ? h("div", {className: "footer-edit-bar"}, h("span", {className: "edit-bar"}, h("button", { title: model.editMode == "update" ? "Cancel editing this record" - : model.editMode == "delete" ? "Cancel deleting this record" - : model.editMode == "create" ? "Cancel creating this record" - : null, + : model.editMode == "delete" ? "Cancel deleting this record" + : model.editMode == "create" ? "Cancel creating this record" + : null, className: "button", onClick: this.onCancelEdit }, "Cancel"), @@ -1186,18 +1194,18 @@ class App extends React.Component { name: "saveBtn", title: model.editMode == "update" ? "Save the values of this record" - : model.editMode == "delete" ? "Delete this record" - : model.editMode == "create" ? "Save the values as a new record" - : null, + : model.editMode == "delete" ? "Delete this record" + : model.editMode == "create" ? "Save the values as a new record" + : null, className: "button " + (model.editMode == "delete" ? "button-destructive" : "button-brand"), disabled: model.spinnerCount != 0 ? true : false, onClick: this.onDoSave }, model.editMode == "update" ? "Save" - : model.editMode == "delete" ? "Confirm delete" - : model.editMode == "create" ? "Save new" - : "???") + : model.editMode == "delete" ? "Confirm delete" + : model.editMode == "create" ? "Save new" + : "???") )) : null, - model.detailsBox ? h(DetailsBox, { model }) : null + model.detailsBox ? h(DetailsBox, {model}) : null ) ); } @@ -1210,21 +1218,21 @@ class ColumnsVisibiltyBox extends React.Component { } onAvailableColumnsClick(e) { e.preventDefault(); - let { rowList } = this.props; + let {rowList} = this.props; rowList.toggleAvailableColumns(); rowList.model.didUpdate(); } render() { - let { rowList, label, content } = this.props; - return h("span", { className: "column-button-outer" }, - h("a", { href: "about:blank", onClick: this.onAvailableColumnsClick, className: "button-icon-link" }, - h("svg", { className: "button-icon" }, - h("use", { xlinkHref: "symbols.svg#chevrondown" }) + let {rowList, label, content} = this.props; + return h("span", {className: "column-button-outer"}, + h("a", {href: "about:blank", onClick: this.onAvailableColumnsClick, className: "button-icon-link"}, + h("svg", {className: "button-icon"}, + h("use", {xlinkHref: "symbols.svg#chevrondown"}) ) ), - rowList.availableColumns ? h("div", { className: "column-popup" }, - h("div", { className: "column-popup-inner" }, - h("span", { className: "menu-item" }, label), + rowList.availableColumns ? h("div", {className: "column-popup"}, + h("div", {className: "column-popup-inner"}, + h("span", {className: "menu-item"}, label), content() ) ) : null @@ -1238,13 +1246,13 @@ class ColumnVisibiltyToggle extends React.Component { this.onShowColumnChange = this.onShowColumnChange.bind(this); } onShowColumnChange(e) { - let { rowList, name } = this.props; + let {rowList, name} = this.props; rowList.showHideColumn(e.target.checked, name); rowList.model.didUpdate(); } render() { - let { rowList, name, disabled } = this.props; - return h("label", { className: "menu-item" }, + let {rowList, name, disabled} = this.props; + return h("label", {className: "menu-item"}, h("input", { type: "checkbox", checked: rowList.selectedColumnMap.has(name), @@ -1273,38 +1281,38 @@ class RowTable extends React.Component { this.tableSettingsOpen = false; } render() { - let { rowList, actionsColumn, classNameForRow } = this.props; + let {rowList, actionsColumn, classNameForRow} = this.props; let selectedColumns = Array.from(rowList.selectedColumnMap.values()); return h("table", {}, h("thead", {}, h("tr", {}, selectedColumns.map(col => - h(HeaderCell, { key: col.name, col, rowList }) + h(HeaderCell, {key: col.name, col, rowList}) ), - h("th", { className: actionsColumn.className, tabIndex: 0 }, - h("button", { className: "table-settings-button", onClick: this.onToggleTableSettings }, - h("div", { className: "table-settings-icon" }) + h("th", {className: actionsColumn.className, tabIndex: 0}, + h("button", {className: "table-settings-button", onClick: this.onToggleTableSettings}, + h("div", {className: "table-settings-icon"}) ), - this.tableSettingsOpen && h("div", { className: "pop-menu-container" }, - h("div", { className: "pop-menu" }, - h("a", { className: "table-settings-link", onClick: this.onClickTableBorderSettings }, "Show / Hide table borders"), + this.tableSettingsOpen && h("div", {className: "pop-menu-container"}, + h("div", {className: "pop-menu"}, + h("a", {className: "table-settings-link", onClick: this.onClickTableBorderSettings}, "Show / Hide table borders"), ) ), ), ), rowList.model.useTab != "all" ? h("tr", {}, selectedColumns.map(col => - h(FilterCell, { key: col.name, col, rowList }) + h(FilterCell, {key: col.name, col, rowList}) ), - h("th", { className: actionsColumn.className + " " + "th-filter-row"}) + h("th", {className: actionsColumn.className + " " + "th-filter-row"}) ) : null ), h("tbody", {}, rowList.rows.map(row => - h("tr", { className: classNameForRow(row), hidden: !row.visible(), title: row.summary(), key: row.reactKey }, + h("tr", {className: classNameForRow(row), hidden: !row.visible(), title: row.summary(), key: row.reactKey}, selectedColumns.map(col => - h(col.reactElement, { key: col.name, row, col }) + h(col.reactElement, {key: col.name, row, col}) ), - h(actionsColumn.reactElement, { className: actionsColumn.className, row }) + h(actionsColumn.reactElement, {className: actionsColumn.className, row}) ) )) ); @@ -1317,12 +1325,12 @@ class HeaderCell extends React.Component { this.onSortRowsBy = this.onSortRowsBy.bind(this); } onSortRowsBy() { - let { rowList, col } = this.props; + let {rowList, col} = this.props; rowList.sortRowsBy(col.name); rowList.model.didUpdate(); } render() { - let { col } = this.props; + let {col} = this.props; return h("th", { className: col.className, @@ -1340,13 +1348,13 @@ class FilterCell extends React.Component { this.onColumnFilterInput = this.onColumnFilterInput.bind(this); } onColumnFilterInput(e) { - let { rowList, col } = this.props; + let {rowList, col} = this.props; col.columnFilter = e.target.value; rowList.model.didUpdate(); } render() { - let { col } = this.props; - return h("th", { className: col.className + " " + "th-filter-row" }, + let {col} = this.props; + return h("th", {className: col.className + " " + "th-filter-row"}, h("input", { placeholder: "Filter", className: "column-filter-box", @@ -1359,9 +1367,9 @@ class FilterCell extends React.Component { class DefaultCell extends React.Component { render() { - let { row, col } = this.props; - return h("td", { className: col.className }, - h(TypedValue, { value: row.sortKey(col.name) }) + let {row, col} = this.props; + return h("td", {className: col.className}, + h(TypedValue, {value: row.sortKey(col.name)}) ); } } @@ -1376,26 +1384,26 @@ class FieldValueCell extends React.Component { this.onLinkClick = this.onLinkClick.bind(this); } onTryEdit(e) { - let { row } = this.props; + let {row} = this.props; if (row.tryEdit()) { let td = e.currentTarget; row.rowList.model.didUpdate(() => td.querySelector("textarea").focus()); } } onDataEditValueInput(e) { - let { row } = this.props; + let {row} = this.props; row.dataEditValue = e.target.value; row.rowList.model.didUpdate(); } onCancelEdit(e) { e.preventDefault(); - let { row } = this.props; + let {row} = this.props; row.dataEditValue = null; row.rowList.model.didUpdate(); } onRecordIdClick(e) { e.preventDefault(); - let { row } = this.props; + let {row} = this.props; row.toggleRecordIdPop(); row.rowList.model.didUpdate(); } @@ -1406,22 +1414,22 @@ class FieldValueCell extends React.Component { } } render() { - let { row, col } = this.props; + let {row, col} = this.props; if (row.isEditing()) { - return h("td", { className: col.className }, - h("textarea", { value: row.dataEditValue, onChange: this.onDataEditValueInput }), - h("a", { href: "about:blank", onClick: this.onCancelEdit, className: "undo-button" }, "\u21B6") + return h("td", {className: col.className}, + h("textarea", {value: row.dataEditValue, onChange: this.onDataEditValueInput}), + h("a", {href: "about:blank", onClick: this.onCancelEdit, className: "undo-button"}, "\u21B6") ); } else if (row.isId()) { - return h("td", { className: col.className, onDoubleClick: this.onTryEdit }, - h("div", { className: "pop-menu-container" }, - h("div", { className: "value-text quick-select" }, h("a", { href: row.idLink() /*used to show visited color*/, onClick: this.onRecordIdClick }, row.dataStringValue())), - row.recordIdPop == null ? null : h("div", { className: "pop-menu" }, row.recordIdPop.map(link => h("a", { key: link.href, href: link.href, className: link.className, id: link.id, onClick: this.onLinkClick }, link.text))) + return h("td", {className: col.className, onDoubleClick: this.onTryEdit}, + h("div", {className: "pop-menu-container"}, + h("div", {className: "value-text quick-select"}, h("a", {href: row.idLink() /*used to show visited color*/, onClick: this.onRecordIdClick}, row.dataStringValue())), + row.recordIdPop == null ? null : h("div", {className: "pop-menu"}, row.recordIdPop.map(link => h("a", {key: link.href, href: link.href, className: link.className, id: link.id, onClick: this.onLinkClick}, link.text))) ) ); } else { - return h("td", { className: col.className, onDoubleClick: this.onTryEdit }, - h(TypedValue, { value: row.sortKey(col.name) }) + return h("td", {className: col.className, onDoubleClick: this.onTryEdit}, + h(TypedValue, {value: row.sortKey(col.name)}) ); } } @@ -1429,21 +1437,21 @@ class FieldValueCell extends React.Component { class FieldTypeCell extends React.Component { render() { - let { row, col } = this.props; - return h("td", { className: col.className + " quick-select" }, + let {row, col} = this.props; + return h("td", {className: col.className + " quick-select"}, row.referenceTypes() ? row.referenceTypes().map(data => - h("span", { key: data }, h("a", { href: row.showReferenceUrl(data) }, data), " ") + h("span", {key: data}, h("a", {href: row.showReferenceUrl(data)}, data), " ") ) : null, - !row.referenceTypes() ? h(TypedValue, { value: row.sortKey(col.name) }) : null + !row.referenceTypes() ? h(TypedValue, {value: row.sortKey(col.name)}) : null ); } } class ChildObjectCell extends React.Component { render() { - let { row, col } = this.props; - return h("td", { className: col.className + " quick-select", key: col.name }, - h("a", { href: row.showChildObjectUrl() }, row.childObject()) + let {row, col} = this.props; + return h("td", {className: col.className + " quick-select", key: col.name}, + h("a", {href: row.showChildObjectUrl()}, row.childObject()) ); } } @@ -1461,10 +1469,10 @@ let TypedValue = props => + (props.value === true ? "value-is-boolean-true " : "") + (props.value === undefined || props.value === null ? "" : "quick-select ") }, - props.value === undefined ? "(Unknown)" - : props.value === null ? "(Blank)" - : typeof props.value == "object" ? JSON.stringify(props.value, null, " ") - : "" + props.value + props.value === undefined ? "(Unknown)" + : props.value === null ? "(Blank)" + : typeof props.value == "object" ? JSON.stringify(props.value, null, " ") + : "" + props.value ); class FieldActionsCell extends React.Component { @@ -1475,28 +1483,28 @@ class FieldActionsCell extends React.Component { } onOpenDetails(e) { e.preventDefault(); - let { row } = this.props; + let {row} = this.props; row.showFieldMetadata(); row.rowList.model.didUpdate(); } onToggleFieldActions() { - let { row } = this.props; + let {row} = this.props; row.toggleFieldActions(); row.rowList.model.didUpdate(); } render() { - let { row, className } = this.props; - return h("td", { className }, - h("div", { className: "pop-menu-container" }, - h("button", { className: "actions-button", onClick: this.onToggleFieldActions }, - h("svg", { className: "actions-icon" }, - h("use", { xlinkHref: "symbols.svg#down" }) + let {row, className} = this.props; + return h("td", {className}, + h("div", {className: "pop-menu-container"}, + h("button", {className: "actions-button", onClick: this.onToggleFieldActions}, + h("svg", {className: "actions-icon"}, + h("use", {xlinkHref: "symbols.svg#down"}) ), ), - row.fieldActionsOpen && h("div", { className: "pop-menu" }, - h("a", { href: "about:blank", onClick: this.onOpenDetails }, "All field metadata"), - row.fieldSetupLinks && h("a", { href: row.fieldSetupLinks.lightningSetupLink }, "Field setup (Lightning)"), - row.fieldSetupLinks && h("a", { href: row.fieldSetupLinks.classicSetupLink }, "Field setup (Classic)") + row.fieldActionsOpen && h("div", {className: "pop-menu"}, + h("a", {href: "about:blank", onClick: this.onOpenDetails}, "All field metadata"), + row.fieldSetupLinks && h("a", {href: row.fieldSetupLinks.lightningSetupLink}, "Field setup (Lightning)"), + row.fieldSetupLinks && h("a", {href: row.fieldSetupLinks.classicSetupLink}, "Field setup (Classic)") ) ) ); @@ -1511,29 +1519,29 @@ class ChildActionsCell extends React.Component { } onOpenDetails(e) { e.preventDefault(); - let { row } = this.props; + let {row} = this.props; row.showChildMetadata(); row.rowList.model.didUpdate(); } onToggleChildActions() { - let { row } = this.props; + let {row} = this.props; row.toggleChildActions(); row.rowList.model.didUpdate(); } render() { - let { row, className } = this.props; - return h("td", { className }, - h("div", { className: "pop-menu-container" }, - h("button", { className: "actions-button", onClick: this.onToggleChildActions }, - h("svg", { className: "actions-icon" }, - h("use", { xlinkHref: "symbols.svg#down" }) + let {row, className} = this.props; + return h("td", {className}, + h("div", {className: "pop-menu-container"}, + h("button", {className: "actions-button", onClick: this.onToggleChildActions}, + h("svg", {className: "actions-icon"}, + h("use", {xlinkHref: "symbols.svg#down"}) ), ), - row.childActionsOpen && h("div", { className: "pop-menu" }, - h("a", { href: "about:blank", onClick: this.onOpenDetails }, "All relationship metadata"), - row.queryListUrl() ? h("a", { href: row.queryListUrl(), title: "Export records in this related list" }, "Export related records") : null, - row.childSetupLinks && h("a", { href: row.childSetupLinks.lightningSetupLink }, "Setup (Lightning)"), - row.childSetupLinks && h("a", { href: row.childSetupLinks.classicSetupLink }, "Setup (Classic)") + row.childActionsOpen && h("div", {className: "pop-menu"}, + h("a", {href: "about:blank", onClick: this.onOpenDetails}, "All relationship metadata"), + row.queryListUrl() ? h("a", {href: row.queryListUrl(), title: "Export records in this related list"}, "Export related records") : null, + row.childSetupLinks && h("a", {href: row.childSetupLinks.lightningSetupLink}, "Setup (Lightning)"), + row.childSetupLinks && h("a", {href: row.childSetupLinks.classicSetupLink}, "Setup (Classic)") ) ) ); @@ -1552,41 +1560,41 @@ class DetailsBox extends React.Component { } onCloseDetailsBox(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.detailsBox = null; model.didUpdate(); } onDetailsFilterInput(e) { - let { model } = this.props; + let {model} = this.props; model.detailsFilter = e.target.value; model.didUpdate(); } onDetailsFilterClick(e, row, detailsFilterList) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.detailsBox = null; detailsFilterList.showColumn(row.key, row.value); model.didUpdate(); } render() { - let { model } = this.props; + let {model} = this.props; return h("div", {}, - h("div", { id: "fieldDetailsView" }, - h("div", { className: "container" }, - h("a", { href: "about:blank", className: "closeLnk", onClick: this.onCloseDetailsBox }, "X"), - h("div", { className: "mainContent" }, + h("div", {id: "fieldDetailsView"}, + h("div", {className: "container"}, + h("a", {href: "about:blank", className: "closeLnk", onClick: this.onCloseDetailsBox}, "X"), + h("div", {className: "mainContent"}, h("h3", {}, "All available metadata for \"" + model.detailsBox.name + "\""), - h("input", { placeholder: "Filter", value: model.detailsFilter, onChange: this.onDetailsFilterInput, ref: "detailsFilter" }), + h("input", {placeholder: "Filter", value: model.detailsFilter, onChange: this.onDetailsFilterInput, ref: "detailsFilter"}), h("table", {}, h("thead", {}, h("tr", {}, h("th", {}, "Key"), h("th", {}, "Value"))), h("tbody", {}, model.detailsBox.rows.map(row => - h("tr", { hidden: !row.visible(), key: row.key }, + h("tr", {hidden: !row.visible(), key: row.key}, h("td", {}, - h("a", { href: "about:blank", onClick: e => this.onDetailsFilterClick(e, row, model.detailsBox.detailsFilterList), hidden: !model.detailsBox.detailsFilterList, title: "Show fields with this property" }, "🔍"), + h("a", {href: "about:blank", onClick: e => this.onDetailsFilterClick(e, row, model.detailsBox.detailsFilterList), hidden: !model.detailsBox.detailsFilterList, title: "Show fields with this property"}, "🔍"), " ", - h("span", { className: "quick-select" }, row.key) + h("span", {className: "quick-select"}, row.key) ), - h("td", {}, h(TypedValue, { value: row.value })) + h("td", {}, h(TypedValue, {value: row.value})) ) )) ) @@ -1611,9 +1619,9 @@ class DetailsBox extends React.Component { model.recordId = args.get("recordId"); model.startLoading(); model.reactCallback = cb => { - ReactDOM.render(h(App, { model }), root, cb); + ReactDOM.render(h(App, {model}), root, cb); }; - ReactDOM.render(h(App, { model }), root); + ReactDOM.render(h(App, {model}), root); }); diff --git a/addon/inspector.js b/addon/inspector.js index 7c002695..96fc5003 100644 --- a/addon/inspector.js +++ b/addon/inspector.js @@ -1,10 +1,10 @@ -export let apiVersion = localStorage.getItem("apiVersion") == null ? "57.0" : localStorage.getItem("apiVersion"); +export let apiVersion = localStorage.getItem("apiVersion") == null ? "59.0" : localStorage.getItem("apiVersion"); export let sfConn = { async getSession(sfHost) { let paramKey = "access_token"; let message = await new Promise(resolve => - chrome.runtime.sendMessage({ message: "getSession", sfHost }, resolve)); + chrome.runtime.sendMessage({message: "getSession", sfHost}, resolve)); if (message) { this.instanceHostname = message.hostname; this.sessionId = message.key; @@ -31,7 +31,7 @@ export let sfConn = { } }, - async rest(url, { logErrors = true, method = "GET", api = "normal", body = undefined, bodyType = "json", headers = {}, progressHandler = null } = {}) { + async rest(url, {logErrors = true, method = "GET", api = "normal", body = undefined, bodyType = "json", headers = {}, progressHandler = null} = {}) { if (!this.instanceHostname || !this.sessionId) { throw new Error("Session not found"); } @@ -137,7 +137,7 @@ export let sfConn = { return wsdl; }, - async soap(wsdl, method, args, { headers } = {}) { + async soap(wsdl, method, args, {headers} = {}) { if (!this.instanceHostname || !this.sessionId) { throw new Error("Session not found"); } @@ -147,13 +147,13 @@ export let sfConn = { xhr.setRequestHeader("Content-Type", "text/xml"); xhr.setRequestHeader("SOAPAction", '""'); - let sessionHeader = { SessionHeader: { sessionId: this.sessionId } }; + let sessionHeader = {SessionHeader: {sessionId: this.sessionId}}; let requestBody = XML.stringify({ name: "soapenv:Envelope", attributes: ` xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"${wsdl.targetNamespaces}`, value: { "soapenv:Header": Object.assign({}, sessionHeader, headers), - "soapenv:Body": { [method]: args } + "soapenv:Body": {[method]: args} } }); @@ -193,7 +193,7 @@ export let sfConn = { }; class XML { - static stringify({ name, attributes, value }) { + static stringify({name, attributes, value}) { function buildRequest(el, params) { if (params == null) { el.setAttribute("xsi:nil", "true"); diff --git a/addon/links.js b/addon/links.js index 5dab8ae7..e05bf473 100644 --- a/addon/links.js +++ b/addon/links.js @@ -1,350 +1,357 @@ export let setupLinks = [ - //Setup - { label: "Setup Home", link: "/lightning/setup/SetupOneHome/home", section: "Setup", prod: false }, - { label: "Service Setup Assistant", link: "/lightning/setup/ServiceHome/home", section: "Setup", prod: false }, - { label: "Multi-Factor Authentication Assistant", link: "/lightning/setup/MfaAssistant/home", section: "Setup", prod: false }, - { label: "Release Updates", link: "/lightning/setup/ReleaseUpdates/home", section: "Setup", prod: false }, - { label: "Salesforce Mobile App", link: "/lightning/setup/SalesforceMobileAppQuickStart/home", section: "Setup", prod: false }, - { label: "Optimizer", link: "/lightning/setup/SalesforceOptimizer/home", section: "Setup", prod: false }, + //Setup + {label: "Setup Home", link: "/lightning/setup/SetupOneHome/home", section: "Setup", prod: false}, + {label: "Service Setup Assistant", link: "/lightning/setup/ServiceHome/home", section: "Setup", prod: false}, + {label: "Multi-Factor Authentication Assistant", link: "/lightning/setup/MfaAssistant/home", section: "Setup", prod: false}, + {label: "Release Updates", link: "/lightning/setup/ReleaseUpdates/home", section: "Setup", prod: false}, + {label: "Salesforce Mobile App", link: "/lightning/setup/SalesforceMobileAppQuickStart/home", section: "Setup", prod: false}, + {label: "Optimizer", link: "/lightning/setup/SalesforceOptimizer/home", section: "Setup", prod: false}, - //Administration > Users - { label: "Permission Sets Groups", link: "/lightning/setup/PermSetGroups/home", section: "Administration > Users", prod: false }, - { label: "Permission Sets", link: "/lightning/setup/PermSets/home", section: "Administration > Users", prod: false }, - { label: "Profiles", link: "/lightning/setup/Profiles/home", section: "Administration > Users", prod: false }, - { label: "Profiles (Enhanced)", link: "/lightning/setup/EnhancedProfiles/home", section: "Administration > Users", prod: false }, - { label: "Public Groups", link: "/lightning/setup/PublicGroups/home", section: "Administration > Users", prod: false }, - { label: "Queues", link: "/lightning/setup/Queues/home", section: "Administration > Users", prod: false }, - { label: "Roles", link: "/lightning/setup/Roles/home", section: "Administration > Users", prod: false }, - { label: "User Management Settings", link: "/lightning/setup/UserManagementSettings/home", section: "Administration > Users", prod: false }, - { label: "Users", link: "/lightning/setup/ManageUsers/home", section: "Administration > Users", prod: false }, - //Administration > Data - { label: "Big Objects", link: "/lightning/setup/BigObjects/home", section: "Administration > Data", prod: false }, - { label: "Data Export", link: "/lightning/setup/DataManagementExport/home", section: "Administration > Data", prod: false }, - { label: "Data Integration Metrics", link: "/lightning/setup/XCleanVitalsUi/home", section: "Administration > Data", prod: false }, - { label: "Data Integration Rules", link: "/lightning/setup/CleanRules/home", section: "Administration > Data", prod: false }, - //Administration > Data > Duplicate Management - { label: "Duplicate Error Logs", link: "/lightning/setup/DuplicateErrorLog/home", section: "Administration > Data > Duplicate Management", prod: false }, - { label: "Duplicate Rules", link: "/lightning/setup/DuplicateRules/home", section: "Administration > Data > Duplicate Management", prod: false }, - { label: "Matching Rules", link: "/lightning/setup/MatchingRules/home", section: "Administration > Data > Duplicate Management", prod: false }, - //Administration > Data - { label: "Mass Delete Records", link: "/lightning/setup/DataManagementDelete/home", section: "Administration > Data", prod: false }, - { label: "Mass Transfer Approval Requests", link: "/lightning/setup/DataManagementManageApprovals/home", section: "Administration > Data", prod: false }, - { label: "Mass Transfer Records", link: "/lightning/setup/DataManagementTransfer/home", section: "Administration > Data", prod: false }, - { label: "Mass Update Addresses", link: "/lightning/setup/DataManagementMassUpdateAddresses/home", section: "Administration > Data", prod: false }, - { label: "Picklist Settings", link: "/lightning/setup/PicklistSettings/home", section: "Administration > Data", prod: false }, - { label: "Schema Settings", link: "/lightning/setup/SchemaSettings/home", section: "Administration > Data", prod: false }, - { label: "State and Country/Territory Picklists", link: "/lightning/setup/AddressCleanerOverview/home", section: "Administration > Data", prod: false }, - { label: "Storage Usage", link: "/lightning/setup/CompanyResourceDisk/home", section: "Administration > Data", prod: false }, - //Administration > Email - { label: "Apex Exception Email", link: "/lightning/setup/ApexExceptionEmail/home", section: "Administration > Email", prod: false }, - { label: "Classic Email Templates", link: "/lightning/setup/CommunicationTemplatesEmail/home", section: "Administration > Email", prod: false }, - { label: "Compliance BCC Email", link: "/lightning/setup/SecurityComplianceBcc/home", section: "Administration > Email", prod: false }, - { label: "DKIM Keys", link: "/lightning/setup/EmailDKIMList/home", section: "Administration > Email", prod: false }, - { label: "Deliverability", link: "/lightning/setup/OrgEmailSettings/home", section: "Administration > Email", prod: false }, - { label: "Email Attachments", link: "/lightning/setup/EmailAttachmentSettings/home", section: "Administration > Email", prod: false }, - //Administration > Email > Delivery Settings - { label: "Email Domain Filters", link: "/lightning/setup/EmailDomainFilter/home", section: "Administration > Email > Delivery Settings", prod: false }, - { label: "Email Relays", link: "/lightning/setup/EmailRelay/home", section: "Administration > Email > Delivery Settings", prod: false }, - //Administration >Email - { label: "Email Footers", link: "/lightning/setup/EmailDisclaimers/home", section: "Administration > Email", prod: false }, - { label: "Email to Salesforce", link: "/lightning/setup/EmailToSalesforce/home", section: "Administration > Email", prod: false }, - { label: "Enhanced Email", link: "/lightning/setup/EnhancedEmail/home", section: "Administration > Email", prod: false }, - { label: "Gmail Integration and Sync", link: "/lightning/setup/LightningForGmailAndSyncSettings/home", section: "Administration > Email", prod: false }, - { label: "Letterheads", link: "/lightning/setup/CommunicationTemplatesLetterheads/home", section: "Administration > Email", prod: false }, - { label: "Lightning Email Templates", link: "/lightning/setup/LightningEmailTemplateSetup/home", section: "Administration > Email", prod: false }, - { label: "Mail Merge Templates", link: "/lightning/setup/CommunicationTemplatesWord/home", section: "Administration > Email", prod: false }, - { label: "Organization-Wide Addresses", link: "/lightning/setup/OrgWideEmailAddresses/home", section: "Administration > Email", prod: false }, - { label: "Outlook Configurations", link: "/lightning/setup/EmailConfigurations/home", section: "Administration > Email", prod: false }, - { label: "Outlook Integration and Sync", link: "/lightning/setup/LightningForOutlookAndSyncSettings/home", section: "Administration > Email", prod: false }, - { label: "Send through External Email Services", link: "/lightning/setup/EmailTransportServiceSetupPage/home", section: "Administration > Email", prod: false }, - { label: "Test Deliverability", link: "/lightning/setup/TestEmailDeliverability/home", section: "Administration > Email", prod: false }, + //Administration > Users + {label: "Permission Sets Groups", link: "/lightning/setup/PermSetGroups/home", section: "Administration > Users", prod: false}, + {label: "Permission Sets", link: "/lightning/setup/PermSets/home", section: "Administration > Users", prod: false}, + {label: "Profiles", link: "/lightning/setup/Profiles/home", section: "Administration > Users", prod: false}, + {label: "Profiles (Enhanced)", link: "/lightning/setup/EnhancedProfiles/home", section: "Administration > Users", prod: false}, + {label: "Public Groups", link: "/lightning/setup/PublicGroups/home", section: "Administration > Users", prod: false}, + {label: "Queues", link: "/lightning/setup/Queues/home", section: "Administration > Users", prod: false}, + {label: "Roles", link: "/lightning/setup/Roles/home", section: "Administration > Users", prod: false}, + {label: "User Management Settings", link: "/lightning/setup/UserManagementSettings/home", section: "Administration > Users", prod: false}, + {label: "Users", link: "/lightning/setup/ManageUsers/home", section: "Administration > Users", prod: false}, + //Administration > Data + {label: "Big Objects", link: "/lightning/setup/BigObjects/home", section: "Administration > Data", prod: false}, + {label: "Data Export", link: "/lightning/setup/DataManagementExport/home", section: "Administration > Data", prod: false}, + {label: "Data Integration Metrics", link: "/lightning/setup/XCleanVitalsUi/home", section: "Administration > Data", prod: false}, + {label: "Data Integration Rules", link: "/lightning/setup/CleanRules/home", section: "Administration > Data", prod: false}, + //Administration > Data > Duplicate Management + {label: "Duplicate Error Logs", link: "/lightning/setup/DuplicateErrorLog/home", section: "Administration > Data > Duplicate Management", prod: false}, + {label: "Duplicate Rules", link: "/lightning/setup/DuplicateRules/home", section: "Administration > Data > Duplicate Management", prod: false}, + {label: "Matching Rules", link: "/lightning/setup/MatchingRules/home", section: "Administration > Data > Duplicate Management", prod: false}, + //Administration > Data + {label: "Mass Delete Records", link: "/lightning/setup/DataManagementDelete/home", section: "Administration > Data", prod: false}, + {label: "Mass Transfer Approval Requests", link: "/lightning/setup/DataManagementManageApprovals/home", section: "Administration > Data", prod: false}, + {label: "Mass Transfer Records", link: "/lightning/setup/DataManagementTransfer/home", section: "Administration > Data", prod: false}, + {label: "Mass Update Addresses", link: "/lightning/setup/DataManagementMassUpdateAddresses/home", section: "Administration > Data", prod: false}, + {label: "Picklist Settings", link: "/lightning/setup/PicklistSettings/home", section: "Administration > Data", prod: false}, + {label: "Schema Settings", link: "/lightning/setup/SchemaSettings/home", section: "Administration > Data", prod: false}, + {label: "State and Country/Territory Picklists", link: "/lightning/setup/AddressCleanerOverview/home", section: "Administration > Data", prod: false}, + {label: "Storage Usage", link: "/lightning/setup/CompanyResourceDisk/home", section: "Administration > Data", prod: false}, + //Administration > Email + {label: "Apex Exception Email", link: "/lightning/setup/ApexExceptionEmail/home", section: "Administration > Email", prod: false}, + {label: "Classic Email Templates", link: "/lightning/setup/CommunicationTemplatesEmail/home", section: "Administration > Email", prod: false}, + {label: "Compliance BCC Email", link: "/lightning/setup/SecurityComplianceBcc/home", section: "Administration > Email", prod: false}, + {label: "DKIM Keys", link: "/lightning/setup/EmailDKIMList/home", section: "Administration > Email", prod: false}, + {label: "Deliverability", link: "/lightning/setup/OrgEmailSettings/home", section: "Administration > Email", prod: false}, + {label: "Email Attachments", link: "/lightning/setup/EmailAttachmentSettings/home", section: "Administration > Email", prod: false}, + //Administration > Email > Delivery Settings + {label: "Email Domain Filters", link: "/lightning/setup/EmailDomainFilter/home", section: "Administration > Email > Delivery Settings", prod: false}, + {label: "Email Relays", link: "/lightning/setup/EmailRelay/home", section: "Administration > Email > Delivery Settings", prod: false}, + //Administration >Email + {label: "Email Footers", link: "/lightning/setup/EmailDisclaimers/home", section: "Administration > Email", prod: false}, + {label: "Email to Salesforce", link: "/lightning/setup/EmailToSalesforce/home", section: "Administration > Email", prod: false}, + {label: "Enhanced Email", link: "/lightning/setup/EnhancedEmail/home", section: "Administration > Email", prod: false}, + {label: "Gmail Integration and Sync", link: "/lightning/setup/LightningForGmailAndSyncSettings/home", section: "Administration > Email", prod: false}, + {label: "Letterheads", link: "/lightning/setup/CommunicationTemplatesLetterheads/home", section: "Administration > Email", prod: false}, + {label: "Lightning Email Templates", link: "/lightning/setup/LightningEmailTemplateSetup/home", section: "Administration > Email", prod: false}, + {label: "Mail Merge Templates", link: "/lightning/setup/CommunicationTemplatesWord/home", section: "Administration > Email", prod: false}, + {label: "Organization-Wide Addresses", link: "/lightning/setup/OrgWideEmailAddresses/home", section: "Administration > Email", prod: false}, + {label: "Outlook Configurations", link: "/lightning/setup/EmailConfigurations/home", section: "Administration > Email", prod: false}, + {label: "Outlook Integration and Sync", link: "/lightning/setup/LightningForOutlookAndSyncSettings/home", section: "Administration > Email", prod: false}, + {label: "Send through External Email Services", link: "/lightning/setup/EmailTransportServiceSetupPage/home", section: "Administration > Email", prod: false}, + {label: "Test Deliverability", link: "/lightning/setup/TestEmailDeliverability/home", section: "Administration > Email", prod: false}, - //Platform Tools > Apps - { label: "App Manager", link: "/lightning/setup/NavigationMenus/home", section: "Platform Tools > Apps", prod: false }, - { label: "AppExchange Marketplace", link: "/lightning/setup/AppExchangeMarketplace/home", section: "Platform Tools > Apps", prod: false }, - //Platform Tools > Apps > Connected Apps - { label: "Connected Apps OAuth Usage", link: "/lightning/setup/ConnectedAppsUsage/home", section: "Platform Tools > Apps > Connected Apps", prod: false }, - { label: "Manage Connected Apps", link: "/lightning/setup/ConnectedApplication/home", section: "Platform Tools > Apps > Connected Apps", prod: false }, - //Platform Tools > Apps > Lightning Bolt - { label: "Flow Category", link: "/lightning/setup/FlowCategory/home", section: "Platform Tools > Apps > Lightning Bolt", prod: false }, - { label: "Lightning Bolt Solutions", link: "/lightning/setup/LightningBolt/home", section: "Platform Tools > Apps > Lightning Bolt", prod: false }, - //Platform Tools > Apps > Mobile Apps > Salesforce - { label: "Salesforce Branding", link: "/lightning/setup/Salesforce1Branding/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false }, - { label: "Salesforce Mobile Quick Start", link: "/lightning/setup/Salesforce1SetupSection/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false }, - { label: "Salesforce Navigation", link: "/lightning/setup/ProjectOneAppMenu/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false }, - { label: "Salesforce Notifications", link: "/lightning/setup/NotificationsSettings/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false }, - { label: "Salesforce Offline", link: "/lightning/setup/MobileOfflineStorageAdmin/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false }, - { label: "Salesforce Settings", link: "/lightning/setup/Salesforce1Settings/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false }, - //Platform Tools > Apps > Packaging - { label: "Installed Packages", link: "/lightning/setup/ImportedPackage/home", section: "Platform Tools > Apps > Packaging", prod: false }, - { label: "Package Manager", link: "/lightning/setup/Package/home", section: "Platform Tools > Apps > Packaging", prod: false }, - { label: "Package Usage", link: "/lightning/setup/PackageUsageSummary/home", section: "Platform Tools > Apps > Packaging", prod: false }, + //Platform Tools > Apps + {label: "App Manager", link: "/lightning/setup/NavigationMenus/home", section: "Platform Tools > Apps", prod: false}, + {label: "AppExchange Marketplace", link: "/lightning/setup/AppExchangeMarketplace/home", section: "Platform Tools > Apps", prod: false}, + //Platform Tools > Apps > Connected Apps + {label: "Connected Apps OAuth Usage", link: "/lightning/setup/ConnectedAppsUsage/home", section: "Platform Tools > Apps > Connected Apps", prod: false}, + {label: "Manage Connected Apps", link: "/lightning/setup/ConnectedApplication/home", section: "Platform Tools > Apps > Connected Apps", prod: false}, + //Platform Tools > Apps > Lightning Bolt + {label: "Flow Category", link: "/lightning/setup/FlowCategory/home", section: "Platform Tools > Apps > Lightning Bolt", prod: false}, + {label: "Lightning Bolt Solutions", link: "/lightning/setup/LightningBolt/home", section: "Platform Tools > Apps > Lightning Bolt", prod: false}, + //Platform Tools > Apps > Mobile Apps > Salesforce + {label: "Salesforce Branding", link: "/lightning/setup/Salesforce1Branding/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false}, + {label: "Salesforce Mobile Quick Start", link: "/lightning/setup/Salesforce1SetupSection/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false}, + {label: "Salesforce Navigation", link: "/lightning/setup/ProjectOneAppMenu/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false}, + {label: "Salesforce Notifications", link: "/lightning/setup/NotificationsSettings/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false}, + {label: "Salesforce Offline", link: "/lightning/setup/MobileOfflineStorageAdmin/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false}, + {label: "Salesforce Settings", link: "/lightning/setup/Salesforce1Settings/home", section: "Platform Tools > Apps > Mobile Apps > Salesforce", prod: false}, + //Platform Tools > Apps > Packaging + {label: "Installed Packages", link: "/lightning/setup/ImportedPackage/home", section: "Platform Tools > Apps > Packaging", prod: false}, + {label: "Package Manager", link: "/lightning/setup/Package/home", section: "Platform Tools > Apps > Packaging", prod: false}, + {label: "Package Usage", link: "/lightning/setup/PackageUsageSummary/home", section: "Platform Tools > Apps > Packaging", prod: false}, - //Platform Tools > Feature Settings > Digital Experiences - { label: "All Sites", link: "/lightning/setup/SetupNetworks/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false }, - { label: "Pages", link: "/lightning/setup/CommunityFlexiPageList/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false }, - { label: "Settings", link: "/lightning/setup/NetworkSettings/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false }, - { label: "Templates", link: "/lightning/setup/CommunityTemplateDefinitionList/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false }, - { label: "Themes", link: "/lightning/setup/CommunityThemeDefinitionList/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false }, + //Platform Tools > Feature Settings > Digital Experiences + {label: "All Sites", link: "/lightning/setup/SetupNetworks/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false}, + {label: "Pages", link: "/lightning/setup/CommunityFlexiPageList/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false}, + {label: "Settings", link: "/lightning/setup/NetworkSettings/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false}, + {label: "Templates", link: "/lightning/setup/CommunityTemplateDefinitionList/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false}, + {label: "Themes", link: "/lightning/setup/CommunityThemeDefinitionList/home", section: "Platform Tools > Feature Settings > Digital Experiences", prod: false}, - //Platform Tools > Feature Settings - { label: "Functions", link: "/lightning/setup/Functions/home", section: "Platform Tools > Feature Settings", prod: false }, - { label: "Home", link: "/lightning/setup/Home/home", section: "Platform Tools > Feature Settings", prod: false }, - { label: "Quip (Salesforce Anywhere)", link: "/lightning/setup/SalesforceAnywhereSetupPage/home", section: "Platform Tools > Feature Settings", prod: false }, - //Platform Tools > Einstein > Einstein Assessors - { label: "Einstein Bots Assessor", link: "/lightning/setup/EinsteinBotsReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false }, - { label: "Einstein Conversation Insights Assessor", link: "/lightning/setup/EinsteinCIReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false }, - { label: "Revenue Intelligence Assessor", link: "/lightning/setup/EinsteinRevIntlReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false }, - { label: "Sales Cloud Einstein Assessor", link: "/lightning/setup/SalesCloudEinsteinReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false }, - { label: "Service Cloud Einstein Assessor", link: "lightning/setup/ServiceCloudEinsteinReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false }, - //Platform Tools > Einstein > Einstein Platform - { label: "Einstein Prediction Builder", link: "/lightning/setup/EinsteinBuilder/home", section: "Platform Tools > Einstein > Einstein Platform", prod: false }, - { label: "Einstein Recommendation Builder", link: "/lightning/setup/EinsteinRecommendation/home", section: "Platform Tools > Einstein > Einstein Platform", prod: false }, - { label: "Einstein.ai", link: "/lightning/setup/EinsteinKeyManagement/home", section: "Platform Tools > Einstein > Einstein Platform", prod: false }, - //Platform Tools > Einstein > Einstein Search - { label: "Objects to Always Search", link: "/lightning/setup/SearchScope/home", section: "Platform Tools > Einstein > Einstein Search", prod: false }, - { label: "Search Layouts", link: "/lightning/setup/EinsteinSearchLayouts/home", section: "Platform Tools > Einstein > Einstein Search", prod: false }, - { label: "Search Manager", link: "/lightning/setup/SearchConfiguration/home", section: "Platform Tools > Einstein > Einstein Search", prod: false }, - { label: "Settings", link: "/lightning/setup/EinsteinSearchSettings/home", section: "Platform Tools > Einstein > Einstein Search", prod: false }, - { label: "Synonyms", link: "/lightning/setup/ManageSynonyms/home", section: "Platform Tools > Einstein > Einstein Search", prod: false }, + //Platform Tools > Feature Settings + {label: "Functions", link: "/lightning/setup/Functions/home", section: "Platform Tools > Feature Settings", prod: false}, + {label: "Home", link: "/lightning/setup/Home/home", section: "Platform Tools > Feature Settings", prod: false}, + {label: "Quip (Salesforce Anywhere)", link: "/lightning/setup/SalesforceAnywhereSetupPage/home", section: "Platform Tools > Feature Settings", prod: false}, + //Platform Tools > Einstein > Einstein Assessors + {label: "Einstein Bots Assessor", link: "/lightning/setup/EinsteinBotsReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false}, + {label: "Einstein Conversation Insights Assessor", link: "/lightning/setup/EinsteinCIReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false}, + {label: "Revenue Intelligence Assessor", link: "/lightning/setup/EinsteinRevIntlReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false}, + {label: "Sales Cloud Einstein Assessor", link: "/lightning/setup/SalesCloudEinsteinReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false}, + {label: "Service Cloud Einstein Assessor", link: "lightning/setup/ServiceCloudEinsteinReadinessCheck/home", section: "Platform Tools > Einstein > Einstein Assessors", prod: false}, + //Platform Tools > Einstein > Einstein Platform + {label: "Einstein Prediction Builder", link: "/lightning/setup/EinsteinBuilder/home", section: "Platform Tools > Einstein > Einstein Platform", prod: false}, + {label: "Einstein Recommendation Builder", link: "/lightning/setup/EinsteinRecommendation/home", section: "Platform Tools > Einstein > Einstein Platform", prod: false}, + {label: "Einstein.ai", link: "/lightning/setup/EinsteinKeyManagement/home", section: "Platform Tools > Einstein > Einstein Platform", prod: false}, + //Platform Tools > Einstein > Einstein Search + {label: "Objects to Always Search", link: "/lightning/setup/SearchScope/home", section: "Platform Tools > Einstein > Einstein Search", prod: false}, + {label: "Search Layouts", link: "/lightning/setup/EinsteinSearchLayouts/home", section: "Platform Tools > Einstein > Einstein Search", prod: false}, + {label: "Search Manager", link: "/lightning/setup/SearchConfiguration/home", section: "Platform Tools > Einstein > Einstein Search", prod: false}, + {label: "Settings", link: "/lightning/setup/EinsteinSearchSettings/home", section: "Platform Tools > Einstein > Einstein Search", prod: false}, + {label: "Synonyms", link: "/lightning/setup/ManageSynonyms/home", section: "Platform Tools > Einstein > Einstein Search", prod: false}, - //Platform Tools > Feature Settings > Salesforce Files - { label: "Asset Files", link: "/lightning/setup/ContentAssets/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false }, - { label: "Content Deliveries and Public Links", link: "/lightning/setup/ContentDistribution/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false }, - { label: "Files Connect", link: "/lightning/setup/ContentHub/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false }, - { label: "General Settings", link: "/lightning/setup/FilesGeneralSettings/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false }, - { label: "Regenerate Previews", link: "/lightning/setup/RegeneratePreviews/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false }, - { label: "Salesforce CRM Content", link: "/lightning/setup/SalesforceCRMContent/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false }, + //Platform Tools > Feature Settings > Salesforce Files + {label: "Asset Files", link: "/lightning/setup/ContentAssets/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false}, + {label: "Content Deliveries and Public Links", link: "/lightning/setup/ContentDistribution/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false}, + {label: "Files Connect", link: "/lightning/setup/ContentHub/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false}, + {label: "General Settings", link: "/lightning/setup/FilesGeneralSettings/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false}, + {label: "Regenerate Previews", link: "/lightning/setup/RegeneratePreviews/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false}, + {label: "Salesforce CRM Content", link: "/lightning/setup/SalesforceCRMContent/home", section: "Platform Tools > Feature Settings > Salesforce Files", prod: false}, - //Platform Tools > Feature Settings > Sales - { label: "Activity Settings", link: "/lightning/setup/HomeActivitiesSetupPage/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "Contact Roles on Contracts", link: "/lightning/setup/ContractContactRoles/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "Contact Roles on Opportunities", link: "/lightning/setup/OpportunityRoles/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "Contract Settings", link: "/lightning/setup/ContractSettings/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "Individual Settings", link: "/lightning/setup/IndividualSettings/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "LinkedIn Sales Navigator", link: "/lightning/setup/LinkedInSalesNavigatorPage/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "Notes Settings", link: "/lightning/setup/NotesSetupPage/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "Order Settings", link: "/lightning/setup/OrderSettings/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "Sales Processes", link: "/lightning/setup/OpportunityProcess/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "Social Accounts and Contacts Settings", link: "/lightning/setup/SocialProfileOrgSettings/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, - { label: "Update Reminders", link: "/lightning/setup/OpportunityUpdateReminders/home", section: "Platform Tools > Feature Settings > Sales", prod: false }, + //Platform Tools > Feature Settings > Sales + {label: "Activity Settings", link: "/lightning/setup/HomeActivitiesSetupPage/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "Contact Roles on Contracts", link: "/lightning/setup/ContractContactRoles/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "Contact Roles on Opportunities", link: "/lightning/setup/OpportunityRoles/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "Contract Settings", link: "/lightning/setup/ContractSettings/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "Individual Settings", link: "/lightning/setup/IndividualSettings/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "LinkedIn Sales Navigator", link: "/lightning/setup/LinkedInSalesNavigatorPage/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "Notes Settings", link: "/lightning/setup/NotesSetupPage/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "Order Settings", link: "/lightning/setup/OrderSettings/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "Sales Processes", link: "/lightning/setup/OpportunityProcess/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "Social Accounts and Contacts Settings", link: "/lightning/setup/SocialProfileOrgSettings/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, + {label: "Update Reminders", link: "/lightning/setup/OpportunityUpdateReminders/home", section: "Platform Tools > Feature Settings > Sales", prod: false}, - //Platform Tools > Feature Settings > Sales > Account - { label: "Account Settings", link: "/lightning/setup/AccountSettings/home", section: "Platform Tools > Feature Settings > Sales > Account", prod: false }, - { label: "Account Teams", link: "/lightning/setup/AccountTeamSelling/home", section: "Platform Tools > Feature Settings > Sales > Account", prod: false }, - { label: "Person Account", link: "/lightning/setup/PersonAccountSettings/home", section: "Platform Tools > Feature Settings > Sales > Account", prod: false }, + //Platform Tools > Feature Settings > Sales > Account + {label: "Account Settings", link: "/lightning/setup/AccountSettings/home", section: "Platform Tools > Feature Settings > Sales > Account", prod: false}, + {label: "Account Teams", link: "/lightning/setup/AccountTeamSelling/home", section: "Platform Tools > Feature Settings > Sales > Account", prod: false}, + {label: "Person Account", link: "/lightning/setup/PersonAccountSettings/home", section: "Platform Tools > Feature Settings > Sales > Account", prod: false}, - //Platform Tools > Feature Settings > Service - { label: "Case Assignment Rules", link: "/lightning/setup/CaseRules/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Case Auto-Response Rules", link: "/lightning/setup/CaseResponses/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Case Comment Triggers", link: "/lightning/setup/CaseCommentTriggers/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Case Team Roles", link: "/lightning/setup/CaseTeamRoles/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Predefined Case Teams", link: "/lightning/setup/CaseTeamTemplates/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Contact Roles on Cases", link: "/lightning/setup/CaseContactRoles/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Customer Contact Requests", link: "/lightning/setup/ContactRequestFlows/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Email-to-Case", link: "/lightning/setup/EmailToCase/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Escalation Rules", link: "/lightning/setup/CaseEscRules/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Feed Filters", link: "/lightning/setup/FeedFilterDefinitions/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Field Service Settings", link: "/lightning/setup/FieldServiceSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Macro Settings", link: "/lightning/setup/MacroSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Omni-Channel Settings", link: "/lightning/setup/OmniChannelSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Snap-ins", link: "/lightning/setup/Snap-ins/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Social Business Rules", link: "/lightning/setup/SocialCustomerServiceBusinessRules/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Social Customer Service", link: "/lightning/setup/SocialCustomerManagementAccountSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Support Processes", link: "/lightning/setup/CaseProcess/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Support Settings", link: "/lightning/setup/CaseSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false }, - { label: "Web-to-Case", link: "/lightning/setup/CaseWebtocase/home", section: "Platform Tools > Feature Settings > Service", prod: false }, - { label: "Web-to-Case HTML Generator", link: "/lightning/setup/CaseWebToCaseHtmlGenerator/home", section: "Platform Tools > Feature Settings > Service", prod: false }, - //Platform Tools > Feature Settings > Survey - { label: "Survey Settings", link: "/lightning/setup/SurveySettings/home", section: "Platform Tools > Feature Settings > Survey", prod: false }, - //Platform Tools > Objects and Fields - { label: "Object Manager", link: "/lightning/setup/ObjectManager/home", section: "Platform Tools > Objects and Fields", prod: false }, - { label: "Picklist Value Sets", link: "/lightning/setup/Picklists/home", section: "Platform Tools > Objects and Fields", prod: false }, - { label: "Schema Builder", link: "/lightning/setup/SchemaBuilder/home", section: "Platform Tools > Objects and Fields", prod: false }, - //Platform Tools > Events - { label: "Event Manager", link: "/lightning/setup/EventManager/home", section: "Platform Tools > Events", prod: false }, - //Platform Tools > Process Automation - { label: "Approval Processes", link: "/lightning/setup/ApprovalProcesses/home", section: "Platform Tools > Process Automation", prod: false }, - { label: "Automation Home", link: "/lightning/setup/ProcessHome/home", section: "Platform Tools > Process Automation", prod: false }, - { label: "Flows", link: "/lightning/setup/Flows/home", section: "Platform Tools > Process Automation", prod: false }, - { label: "Migrate to Flow", link: "/lightning/setup/MigrateToFlowTool/home", section: "Platform Tools > Process Automation", prod: false }, - { label: "Next Best Action", link: "/lightning/setup/NextBestAction/home", section: "Platform Tools > Process Automation", prod: false }, - { label: "Paused And Failed Flow Interviews", link: "/lightning/setup/Pausedflows/home", section: "Platform Tools > Process Automation", prod: false }, - { label: "Post Templates", link: "/lightning/setup/FeedTemplates/home", section: "Platform Tools > Process Automation", prod: false }, - { label: "Process Automation Settings", link: "/lightning/setup/WorkflowSettings/home", section: "Platform Tools > Process Automation", prod: false }, - { label: "Process Builder", link: "/lightning/setup/ProcessAutomation/home", section: "Platform Tools > Process Automation", prod: false }, + //Platform Tools > Feature Settings > Service + {label: "Case Assignment Rules", link: "/lightning/setup/CaseRules/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Case Auto-Response Rules", link: "/lightning/setup/CaseResponses/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Case Comment Triggers", link: "/lightning/setup/CaseCommentTriggers/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Case Team Roles", link: "/lightning/setup/CaseTeamRoles/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Predefined Case Teams", link: "/lightning/setup/CaseTeamTemplates/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Contact Roles on Cases", link: "/lightning/setup/CaseContactRoles/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Customer Contact Requests", link: "/lightning/setup/ContactRequestFlows/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Email-to-Case", link: "/lightning/setup/EmailToCase/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Escalation Rules", link: "/lightning/setup/CaseEscRules/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Feed Filters", link: "/lightning/setup/FeedFilterDefinitions/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Field Service Settings", link: "/lightning/setup/FieldServiceSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Macro Settings", link: "/lightning/setup/MacroSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Omni-Channel Settings", link: "/lightning/setup/OmniChannelSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Snap-ins", link: "/lightning/setup/Snap-ins/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Social Business Rules", link: "/lightning/setup/SocialCustomerServiceBusinessRules/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Social Customer Service", link: "/lightning/setup/SocialCustomerManagementAccountSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Support Processes", link: "/lightning/setup/CaseProcess/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Support Settings", link: "/lightning/setup/CaseSettings/home", section: "//Platform Tools > Feature Settings > Service", prod: false}, + {label: "Web-to-Case", link: "/lightning/setup/CaseWebtocase/home", section: "Platform Tools > Feature Settings > Service", prod: false}, + {label: "Web-to-Case HTML Generator", link: "/lightning/setup/CaseWebToCaseHtmlGenerator/home", section: "Platform Tools > Feature Settings > Service", prod: false}, + //Platform Tools > Feature Settings > Survey + {label: "Survey Settings", link: "/lightning/setup/SurveySettings/home", section: "Platform Tools > Feature Settings > Survey", prod: false}, + //Platform Tools > Objects and Fields + {label: "Object Manager", link: "/lightning/setup/ObjectManager/home", section: "Platform Tools > Objects and Fields", prod: false}, + {label: "Picklist Value Sets", link: "/lightning/setup/Picklists/home", section: "Platform Tools > Objects and Fields", prod: false}, + {label: "Schema Builder", link: "/lightning/setup/SchemaBuilder/home", section: "Platform Tools > Objects and Fields", prod: false}, + //Platform Tools > Events + {label: "Event Manager", link: "/lightning/setup/EventManager/home", section: "Platform Tools > Events", prod: false}, + //Platform Tools > Process Automation + {label: "Approval Processes", link: "/lightning/setup/ApprovalProcesses/home", section: "Platform Tools > Process Automation", prod: false}, + {label: "Automation Home", link: "/lightning/setup/ProcessHome/home", section: "Platform Tools > Process Automation", prod: false}, + {label: "Flows", link: "/lightning/setup/Flows/home", section: "Platform Tools > Process Automation", prod: false}, + {label: "Migrate to Flow", link: "/lightning/setup/MigrateToFlowTool/home", section: "Platform Tools > Process Automation", prod: false}, + {label: "Next Best Action", link: "/lightning/setup/NextBestAction/home", section: "Platform Tools > Process Automation", prod: false}, + {label: "Paused And Failed Flow Interviews", link: "/lightning/setup/Pausedflows/home", section: "Platform Tools > Process Automation", prod: false}, + {label: "Post Templates", link: "/lightning/setup/FeedTemplates/home", section: "Platform Tools > Process Automation", prod: false}, + {label: "Process Automation Settings", link: "/lightning/setup/WorkflowSettings/home", section: "Platform Tools > Process Automation", prod: false}, + {label: "Process Builder", link: "/lightning/setup/ProcessAutomation/home", section: "Platform Tools > Process Automation", prod: false}, - { label: "Email Alerts", link: "/lightning/setup/WorkflowEmails/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false }, - { label: "Field Updates", link: "/lightning/setup/WorkflowFieldUpdates/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false }, - { label: "Outbound Messages", link: "/lightning/setup/WorkflowOutboundMessaging/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false }, - { label: "Send Actions", link: "/lightning/setup/SendAction/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false }, - { label: "Tasks", link: "/lightning/setup/WorkflowTasks/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false }, - { label: "Workflow Rules", link: "/lightning/setup/WorkflowRules/home", section: "Platform Tools > Process Automation", prod: false }, - //User Interface - { label: "Action Link Templates", link: "/lightning/setup/ActionLinkGroupTemplates/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Guided Actions", link: "/lightning/setup/GuidedActions/home", section: "Platform Tools > User Interface", prod: false }, - { label: "App Menu", link: "/lightning/setup/AppMenu/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Custom Labels", link: "/lightning/setup/ExternalStrings/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Density Settings", link: "/lightning/setup/DensitySetup/home", section: "Platform Tools > User Interface", prod: false }, + {label: "Email Alerts", link: "/lightning/setup/WorkflowEmails/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false}, + {label: "Field Updates", link: "/lightning/setup/WorkflowFieldUpdates/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false}, + {label: "Outbound Messages", link: "/lightning/setup/WorkflowOutboundMessaging/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false}, + {label: "Send Actions", link: "/lightning/setup/SendAction/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false}, + {label: "Tasks", link: "/lightning/setup/WorkflowTasks/home", section: "Platform Tools > Process Automation > Workflow Actions", prod: false}, + {label: "Workflow Rules", link: "/lightning/setup/WorkflowRules/home", section: "Platform Tools > Process Automation", prod: false}, + //User Interface + {label: "Action Link Templates", link: "/lightning/setup/ActionLinkGroupTemplates/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Guided Actions", link: "/lightning/setup/GuidedActions/home", section: "Platform Tools > User Interface", prod: false}, + {label: "App Menu", link: "/lightning/setup/AppMenu/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Custom Labels", link: "/lightning/setup/ExternalStrings/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Density Settings", link: "/lightning/setup/DensitySetup/home", section: "Platform Tools > User Interface", prod: false}, - { label: "Global Actions", link: "/lightning/setup/GlobalActions/home", section: "Platform Tools > User Interface > Global Actions", prod: false }, - { label: "Publisher Layouts", link: "/lightning/setup/GlobalPublisherLayouts/home", section: "Platform Tools > User Interface > Global Actions", prod: false }, + {label: "Global Actions", link: "/lightning/setup/GlobalActions/home", section: "Platform Tools > User Interface > Global Actions", prod: false}, + {label: "Publisher Layouts", link: "/lightning/setup/GlobalPublisherLayouts/home", section: "Platform Tools > User Interface > Global Actions", prod: false}, - { label: "Lightning App Builder", link: "/lightning/setup/FlexiPageList/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Lightning Extension", link: "/lightning/setup/LightningExtension/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Loaded Console Tab Limit", link: "/lightning/setup/ConsoleMaxTabCacheSetup/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Path Settings", link: "/lightning/setup/PathAssistantSetupHome/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Quick Text Settings", link: "/lightning/setup/LightningQuickTextSettings/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Record Page Settings", link: "/lightning/setup/SimpleRecordHome/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Rename Tabs and Labels", link: "/lightning/setup/RenameTab/home", section: "Platform Tools > User Interface", prod: false }, - //Sites and Domains - { label: "Custom URLs", link: "/lightning/setup/DomainSites/home", section: "Platform Tools > User Interface > Sites and Domains", prod: false }, - { label: "Domains", link: "/lightning/setup/DomainNames/home", section: "Platform Tools > User Interface > Sites and Domains", prod: false }, - { label: "Sites", link: "/lightning/setup/CustomDomain/home", section: "Platform Tools > User Interface > Sites and Domains", prod: false }, + {label: "Lightning App Builder", link: "/lightning/setup/FlexiPageList/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Lightning Extension", link: "/lightning/setup/LightningExtension/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Loaded Console Tab Limit", link: "/lightning/setup/ConsoleMaxTabCacheSetup/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Path Settings", link: "/lightning/setup/PathAssistantSetupHome/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Quick Text Settings", link: "/lightning/setup/LightningQuickTextSettings/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Record Page Settings", link: "/lightning/setup/SimpleRecordHome/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Rename Tabs and Labels", link: "/lightning/setup/RenameTab/home", section: "Platform Tools > User Interface", prod: false}, + //Sites and Domains + {label: "Custom URLs", link: "/lightning/setup/DomainSites/home", section: "Platform Tools > User Interface > Sites and Domains", prod: false}, + {label: "Domains", link: "/lightning/setup/DomainNames/home", section: "Platform Tools > User Interface > Sites and Domains", prod: false}, + {label: "Sites", link: "/lightning/setup/CustomDomain/home", section: "Platform Tools > User Interface > Sites and Domains", prod: false}, - { label: "Tabs", link: "/lightning/setup/CustomTabs/home", section: "Platform Tools > User Interface", prod: false }, - { label: "Themes and Branding", link: "/lightning/setup/ThemingAndBranding/home", section: "Platform Tools > User Interface", prod: false }, - //Translation Workbench - { label: "Data Translation Settings", link: "/lightning/setup/LabelWorkbenchDataTranslationSetup/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false }, - { label: "Export", link: "/lightning/setup/LabelWorkbenchExport/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false }, - { label: "Import", link: "/lightning/setup/LabelWorkbenchImport/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false }, - { label: "Override", link: "/lightning/setup/LabelWorkbenchOverride/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false }, - { label: "Translate", link: "/lightning/setup/LabelWorkbenchTranslate/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false }, - { label: "Translation Settings", link: "/lightning/setup/LabelWorkbenchSetup/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false }, + {label: "Tabs", link: "/lightning/setup/CustomTabs/home", section: "Platform Tools > User Interface", prod: false}, + {label: "Themes and Branding", link: "/lightning/setup/ThemingAndBranding/home", section: "Platform Tools > User Interface", prod: false}, + //Translation Workbench + {label: "Data Translation Settings", link: "/lightning/setup/LabelWorkbenchDataTranslationSetup/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false}, + {label: "Export", link: "/lightning/setup/LabelWorkbenchExport/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false}, + {label: "Import", link: "/lightning/setup/LabelWorkbenchImport/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false}, + {label: "Override", link: "/lightning/setup/LabelWorkbenchOverride/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false}, + {label: "Translate", link: "/lightning/setup/LabelWorkbenchTranslate/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false}, + {label: "Translation Settings", link: "/lightning/setup/LabelWorkbenchSetup/home", section: "Platform Tools > User Interface > Translation Workbench", prod: false}, - { label: "User Interface", link: "/lightning/setup/UserInterfaceUI/home", section: "Platform Tools > User Interface", prod: false }, - //Custom Code - { label: "Apex Classes", link: "/lightning/setup/ApexClasses/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Apex Hammer Test Results", link: "/lightning/setup/ApexHammerResultStatus/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Apex Settings", link: "/lightning/setup/ApexSettings/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Apex Test Execution", link: "/lightning/setup/ApexTestQueue/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Apex Test History", link: "/lightning/setup/ApexTestHistory/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Apex Triggers", link: "/lightning/setup/ApexTriggers/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Canvas App Previewer", link: "/lightning/setup/CanvasPreviewerUi/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Custom Metadata Types", link: "/lightning/setup/CustomMetadata/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Custom Permissions", link: "/lightning/setup/CustomPermissions/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Custom Settings", link: "/lightning/setup/CustomSettings/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Email Services", link: "/lightning/setup/EmailToApexFunction/home", section: "Platform Tools > Custom Code", prod: false }, - //Lightning Components - { label: "Debug Mode", link: "/lightning/setup/UserDebugModeSetup/home", section: "Platform Tools > Custom Code > Lightning Components", prod: false }, - { label: "Lightning Components", link: "/lightning/setup/LightningComponentBundles/home", section: "Platform Tools > Custom Code > Lightning Components", prod: false }, + {label: "User Interface", link: "/lightning/setup/UserInterfaceUI/home", section: "Platform Tools > User Interface", prod: false}, + //Custom Code + {label: "Apex Classes", link: "/lightning/setup/ApexClasses/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Apex Hammer Test Results", link: "/lightning/setup/ApexHammerResultStatus/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Apex Settings", link: "/lightning/setup/ApexSettings/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Apex Test Execution", link: "/lightning/setup/ApexTestQueue/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Apex Test History", link: "/lightning/setup/ApexTestHistory/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Apex Triggers", link: "/lightning/setup/ApexTriggers/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Canvas App Previewer", link: "/lightning/setup/CanvasPreviewerUi/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Custom Metadata Types", link: "/lightning/setup/CustomMetadata/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Custom Permissions", link: "/lightning/setup/CustomPermissions/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Custom Settings", link: "/lightning/setup/CustomSettings/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Email Services", link: "/lightning/setup/EmailToApexFunction/home", section: "Platform Tools > Custom Code", prod: false}, + //Lightning Components + {label: "Debug Mode", link: "/lightning/setup/UserDebugModeSetup/home", section: "Platform Tools > Custom Code > Lightning Components", prod: false}, + {label: "Lightning Components", link: "/lightning/setup/LightningComponentBundles/home", section: "Platform Tools > Custom Code > Lightning Components", prod: false}, - { label: "Platform Cache", link: "/lightning/setup/PlatformCache/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Remote Access", link: "/lightning/setup/RemoteAccess/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Static Resources", link: "/lightning/setup/StaticResources/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Tools", link: "/lightning/setup/ClientDevTools/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Visualforce Components", link: "/lightning/setup/ApexComponents/home", section: "Platform Tools > Custom Code", prod: false }, - { label: "Visualforce Pages", link: "/lightning/setup/ApexPages/home", section: "Platform Tools > Custom Code", prod: false }, - //Development - { label: "Dev Hub", link: "/lightning/setup/DevHub/home", section: "Platform Tools > Dev Hub", prod: true }, - { label: "DevOps Center", link: "/lightning/setup/DevOpsCenterSetup/home", section: "Platform Tools > Dev Hub", prod: true }, - { label: "Org Shape", link: "/lightning/setup/ShapeGrantAccess/home", section: "Platform Tools > Dev Hub", prod: true }, - //Performance - { label: "Performance Assistant", link: "/lightning/setup/PerformanceAssistant/home", section: "Platform Tools > Performance > Performance Testing", prod: false }, - //Platform Tools > Environments - { label: "Inbound Change Sets", link: "/lightning/setup/InboundChangeSet/home", section: "Platform Tools > Environments > Change Sets", prod: false }, - { label: "Outbound Change Sets", link: "/lightning/setup/OutboundChangeSet/home", section: "Platform Tools > Environments > Change Sets", prod: false }, - //Platform Tools > Environments > Deploy - { label: "Deployment Settings", link: "/lightning/setup/DeploymentSettings/home", section: "Platform Tools > Environments > Deploy", prod: false }, - { label: "Deployment Status", link: "/lightning/setup/DeployStatus/home", section: "Platform Tools > Environments > Deploy", prod: false }, - //Platform Tools > Environments > Jobs - { label: "Apex Flex Queue", link: "/lightning/setup/ApexFlexQueue/home", section: "Platform Tools > Environments > Jobs", prod: false }, - { label: "Apex Jobs", link: "/lightning/setup/AsyncApexJobs/home", section: "Platform Tools > Environments > Jobs", prod: false }, - { label: "Background Jobs", link: "/lightning/setup/ParallelJobsStatus/home", section: "Platform Tools > Environments > Jobs", prod: false }, - { label: "Bulk Data Load Jobs", link: "/lightning/setup/AsyncApiJobStatus/home", section: "Platform Tools > Environments > Jobs", prod: false }, - { label: "Scheduled Jobs", link: "/lightning/setup/ScheduledJobs/home", section: "Platform Tools > Environments > Jobs", prod: false }, - //Platform Tools > Environments > Logs - { label: "Debug Logs", link: "/lightning/setup/ApexDebugLogs/home", section: "Platform Tools > Environments > Logs", prod: false }, - { label: "Email Log Files", link: "/lightning/setup/EmailLogFiles/home", section: "Platform Tools > Environments > Logs", prod: false }, - //Platform Tools > Environments > Monitoring - { label: "API Usage Notifications", link: "/lightning/setup/MonitoringRateLimitingNotification/home", section: "Platform Tools > Environments > Monitoring", prod: false }, - { label: "Case Escalations", link: "/lightning/setup/DataManagementManageCaseEscalation/home", section: "Platform Tools > Environments > Monitoring", prod: false }, - { label: "Email Snapshots", link: "/lightning/setup/EmailCapture/home", section: "Platform Tools > Environments > Monitoring", prod: false }, - { label: "Outbound Messages", link: "/lightning/setup/WorkflowOmStatus/home", section: "Platform Tools > Environments > Monitoring", prod: false }, - { label: "Time-Based Workflow", link: "/lightning/setup/DataManagementManageWorkflowQueue/home", section: "Platform Tools > Environments > Monitoring", prod: false }, + {label: "Platform Cache", link: "/lightning/setup/PlatformCache/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Remote Access", link: "/lightning/setup/RemoteAccess/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Static Resources", link: "/lightning/setup/StaticResources/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Tools", link: "/lightning/setup/ClientDevTools/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Visualforce Components", link: "/lightning/setup/ApexComponents/home", section: "Platform Tools > Custom Code", prod: false}, + {label: "Visualforce Pages", link: "/lightning/setup/ApexPages/home", section: "Platform Tools > Custom Code", prod: false}, + //Development + {label: "Dev Hub", link: "/lightning/setup/DevHub/home", section: "Platform Tools > Dev Hub", prod: true}, + {label: "DevOps Center", link: "/lightning/setup/DevOpsCenterSetup/home", section: "Platform Tools > Dev Hub", prod: true}, + {label: "Org Shape", link: "/lightning/setup/ShapeGrantAccess/home", section: "Platform Tools > Dev Hub", prod: true}, + //Performance + {label: "Performance Assistant", link: "/lightning/setup/PerformanceAssistant/home", section: "Platform Tools > Performance > Performance Testing", prod: false}, + //Platform Tools > Environments + {label: "Inbound Change Sets", link: "/lightning/setup/InboundChangeSet/home", section: "Platform Tools > Environments > Change Sets", prod: false}, + {label: "Outbound Change Sets", link: "/lightning/setup/OutboundChangeSet/home", section: "Platform Tools > Environments > Change Sets", prod: false}, + //Platform Tools > Environments > Deploy + {label: "Deployment Settings", link: "/lightning/setup/DeploymentSettings/home", section: "Platform Tools > Environments > Deploy", prod: false}, + {label: "Deployment Status", link: "/lightning/setup/DeployStatus/home", section: "Platform Tools > Environments > Deploy", prod: false}, + //Platform Tools > Environments > Jobs + {label: "Apex Flex Queue", link: "/lightning/setup/ApexFlexQueue/home", section: "Platform Tools > Environments > Jobs", prod: false}, + {label: "Apex Jobs", link: "/lightning/setup/AsyncApexJobs/home", section: "Platform Tools > Environments > Jobs", prod: false}, + {label: "Background Jobs", link: "/lightning/setup/ParallelJobsStatus/home", section: "Platform Tools > Environments > Jobs", prod: false}, + {label: "Bulk Data Load Jobs", link: "/lightning/setup/AsyncApiJobStatus/home", section: "Platform Tools > Environments > Jobs", prod: false}, + {label: "Scheduled Jobs", link: "/lightning/setup/ScheduledJobs/home", section: "Platform Tools > Environments > Jobs", prod: false}, + //Platform Tools > Environments > Logs + {label: "Debug Logs", link: "/lightning/setup/ApexDebugLogs/home", section: "Platform Tools > Environments > Logs", prod: false}, + {label: "Email Log Files", link: "/lightning/setup/EmailLogFiles/home", section: "Platform Tools > Environments > Logs", prod: false}, + //Platform Tools > Environments > Monitoring + {label: "API Usage Notifications", link: "/lightning/setup/MonitoringRateLimitingNotification/home", section: "Platform Tools > Environments > Monitoring", prod: false}, + {label: "Case Escalations", link: "/lightning/setup/DataManagementManageCaseEscalation/home", section: "Platform Tools > Environments > Monitoring", prod: false}, + {label: "Email Snapshots", link: "/lightning/setup/EmailCapture/home", section: "Platform Tools > Environments > Monitoring", prod: false}, + {label: "Outbound Messages", link: "/lightning/setup/WorkflowOmStatus/home", section: "Platform Tools > Environments > Monitoring", prod: false}, + {label: "Time-Based Workflow", link: "/lightning/setup/DataManagementManageWorkflowQueue/home", section: "Platform Tools > Environments > Monitoring", prod: false}, - { label: "Sandboxes", link: "/lightning/setup/DataManagementCreateTestInstance/home", section: "Platform Tools > Environments", prod: true }, - { label: "System Overview", link: "/lightning/setup/SystemOverview/home", section: "Platform Tools > Environments", prod: false }, - //Platform Tools > User Engagement - { label: "Adoption Assistance", link: "/lightning/setup/AdoptionAssistance/home", section: "Platform Tools > User Engagement", prod: false }, - { label: "Guidance Center", link: "/lightning/setup/LearningSetup/home", section: "Platform Tools > User Engagement", prod: false }, - { label: "Help Menu", link: "/lightning/setup/HelpMenu/home", section: "Platform Tools > User Engagement", prod: false }, - { label: "In-App Guidance", link: "/lightning/setup/Prompts/home", section: "Platform Tools > User Engagement", prod: false }, - //Platform Tools > Integrations - { label: "API", link: "/lightning/setup/WebServices/home", section: "Platform Tools > Integrations", prod: false }, - { label: "Basic Data Import", link: "/lightning/setup/BasicDataImport/home", section: "Platform Tools > Integrations", prod: false }, - { label: "Change Data Capture", link: "/lightning/setup/CdcObjectEnablement/home", section: "Platform Tools > Integrations", prod: false }, - { label: "Data Import Wizard", link: "/lightning/setup/DataManagementDataImporter/home", section: "Platform Tools > Integrations", prod: false }, - { label: "Data Loader", link: "/lightning/setup/DataLoader/home", section: "Platform Tools > Integrations", prod: false }, - { label: "Dataloader.io", link: "/lightning/setup/DataLoaderIo/home", section: "Platform Tools > Integrations", prod: false }, - { label: "External Data Sources", link: "/lightning/setup/ExternalDataSource/home", section: "Platform Tools > Integrations", prod: false }, - { label: "External Objects", link: "/lightning/setup/ExternalObjects/home", section: "Platform Tools > Integrations", prod: false }, - { label: "External Services", link: "/lightning/setup/ExternalServices/home", section: "Platform Tools > Integrations", prod: false }, - { label: "Platform Events", link: "/lightning/setup/EventObjects/home", section: "Platform Tools > Integrations", prod: false }, - { label: "Teams Integration", link: "/lightning/setup/MicrosoftTeamsIntegration/home", section: "Platform Tools > Integrations", prod: false }, - //Platform Tools > Notification Builder - { label: "Custom Notifications", link: "/lightning/setup/CustomNotifications/home", section: "Platform Tools > Notification Builder", prod: false }, - { label: "Notification Delivery Settings", link: "/lightning/setup/NotificationTypesManager/home", section: "Platform Tools > Notification Builder", prod: false }, - //Settings > Company Settings - { label: "Business Hours", link: "/lightning/setup/BusinessHours/home", section: "Settings > Company Settings", prod: false }, - { label: "Public Calendars and Resources", link: "/lightning/setup/Calendars/home", section: "Settings > Company Settings > Calendar Settings", prod: false }, - { label: "Company Information", link: "/lightning/setup/CompanyProfileInfo/home", section: "Settings > Company Settings", prod: false }, - { label: "Data Protection and Privacy", link: "/lightning/setup/ConsentManagement/home", section: "Settings > Company Settings", prod: false }, - { label: "Fiscal Year", link: "/lightning/setup/ForecastFiscalYear/home", section: "Settings > Company Settings", prod: false }, - { label: "Holidays", link: "/lightning/setup/Holiday/home", section: "Settings > Company Settings", prod: false }, - { label: "Language Settings", link: "/lightning/setup/LanguageSettings/home", section: "Settings > Company Settings", prod: false }, - { label: "Manage Currencies", link: "/lightning/setup/CompanyCurrency/home", section: "Settings > Company Settings", prod: false }, - { label: "Maps and Location Settings", link: "/lightning/setup/MapsAndLocationServicesSettings/home", section: "Settings > Company Settings", prod: false }, - { label: "My Domain", link: "/lightning/setup/OrgDomain/home", section: "Settings > Company Settings", prod: false }, - //Settings > Data Classification - { label: "Data Classification", link: "/lightning/setup/DataClassificationSettings/home", section: "Settings > Data Classification", prod: false }, - { label: "Data Classification Download", link: "/lightning/setup/DataClassificationDownload/home", section: "Settings > Data Classification", prod: false }, - { label: "Data Classification Upload", link: "/lightning/setup/DataClassificationUpload/home", section: "Settings > Data Classification", prod: false }, - //Settings > Privacy Center - { label: "Consent Event Stream", link: "/lightning/setup/ConsentEventStream/home", section: "Settings > Privacy Center", prod: false }, - //Settings > Identity - { label: "Auth. Providers", link: "/lightning/setup/AuthProviders/home", section: "Settings > Identity", prod: false }, - { label: "Identity Provider", link: "/lightning/setup/IdpPage/home", section: "Settings > Identity", prod: false }, - { label: "Identity Provider Event Log", link: "/lightning/setup/IdpErrorLog/home", section: "Settings > Identity", prod: false }, - { label: "Identity Verification", link: "/lightning/setup/IdentityVerification/home", section: "Settings > Identity", prod: false }, - { label: "Identity Verification History", link: "/lightning/setup/VerificationHistory/home", section: "Settings > Identity", prod: false }, - { label: "Login Flows", link: "/lightning/setup/LoginFlow/home", section: "Settings > Identity", prod: false }, - { label: "Login History", link: "/lightning/setup/OrgLoginHistory/home", section: "Settings > Identity", prod: false }, - { label: "OAuth Custom Scopes", link: "/lightning/setup/OauthCustomScope/home", section: "Settings > Identity", prod: false }, - { label: "OAuth and OpenID Connect Settings", link: "/lightning/setup/OauthOidcSettings/home", section: "Settings > Identity", prod: false }, - { label: "Single Sign-On Settings", link: "/lightning/setup/SingleSignOn/home", section: "Settings > Identity", prod: false }, - //Settings > Security - { label: "Account Owner Report", link: "/lightning/setup/SecurityAccountOwner/home", section: "Settings > Security", prod: false }, - { label: "Activations", link: "/lightning/setup/ActivatedIpAddressAndClientBrowsersPage/home", section: "Settings > Security", prod: false }, - { label: "CORS", link: "/lightning/setup/CorsWhitelistEntries/home", section: "Settings > Security", prod: false }, - { label: "CSP Trusted Sites", link: "/lightning/setup/SecurityCspTrustedSite/home", section: "Settings > Security", prod: false }, - { label: "Certificate and Key Management", link: "/lightning/setup/CertificatesAndKeysManagement/home", section: "Settings > Security", prod: false }, - { label: "Delegated Administration", link: "/lightning/setup/DelegateGroups/home", section: "Settings > Security", prod: false }, - //Settings > Security > Event Monitoring - { label: "Event Monitoring Settings", link: "/lightning/setup/EventMonitoringSetup/home", section: "Settings > Security > Event Monitoring", prod: false }, - { label: "Transaction Security Policies", link: "/lightning/setup/TransactionSecurityNew/home", section: "Settings > Security > Event Monitoring", prod: false }, - //Settings > Security - { label: "Expire All Passwords", link: "/lightning/setup/SecurityExpirePasswords/home", section: "Settings > Security", prod: false }, - { label: "Field Accessibility", link: "/lightning/setup/FieldAccessibility/home", section: "Settings > Security", prod: false }, - { label: "File Upload and Download Security", link: "/lightning/setup/FileTypeSetting/home", section: "Settings > Security", prod: false }, - { label: "Health Check", link: "/lightning/setup/HealthCheck/home", section: "Settings > Security", prod: false }, - { label: "Login Access Policies", link: "/lightning/setup/LoginAccessPolicies/home", section: "Settings > Security", prod: false }, - { label: "Named Credentials", link: "/lightning/setup/NamedCredential/home", section: "Settings > Security", prod: false }, - { label: "Network Access", link: "/lightning/setup/NetworkAccess/home", section: "Settings > Security", prod: false }, - { label: "Password Policies", link: "/lightning/setup/SecurityPolicies/home", section: "Settings > Security", prod: false }, - //Settings > Security > Platform Encryption - { label: "Advanced Settings", link: "/lightning/setup/SecurityRemoteProxy/home", section: "Settings > Security > Platform Encryption", prod: false }, - { label: "Encryption Policy", link: "/lightning/setup/EncryptionPolicy/home", section: "Settings > Security > Platform Encryption", prod: false }, - { label: "Encryption Statistics", link: "/lightning/setup/EncryptionStatistics/home", section: "Settings > Security > Platform Encryption", prod: false }, - { label: "Key Management", link: "/lightning/setup/PlatformEncryptionKeyManagement/home", section: "Settings > Security > Platform Encryption", prod: false }, - //Settings > Security - { label: "Portal Health Check", link: "/lightning/setup/PortalSecurityReport/home", section: "Settings > Security", prod: false }, - { label: "Private Connect", link: "/lightning/setup/PrivateConnect/home", section: "Settings > Security", prod: false }, - { label: "Remote Site Settings", link: "/lightning/setup/SecurityRemoteProxy/home", section: "Settings > Security", prod: false }, - { label: "Session Management", link: "/lightning/setup/SessionManagementPage/home", section: "Settings > Security", prod: false }, - { label: "Session Settings", link: "/lightning/setup/SecuritySession/home", section: "Settings > Security", prod: false }, - { label: "Sharing Settings", link: "/lightning/setup/SecuritySharing/home", section: "Settings > Security", prod: false }, - { label: "Trusted URLs for Redirects", link: "/lightning/setup/SecurityRedirectWhitelistUrl/home", section: "Settings > Security", prod: false }, - { label: "View Setup Audit Trail", link: "/lightning/setup/SecurityEvents/home", section: "Settings > Security", prod: false } -] \ No newline at end of file + {label: "Sandboxes", link: "/lightning/setup/DataManagementCreateTestInstance/home", section: "Platform Tools > Environments", prod: true}, + {label: "System Overview", link: "/lightning/setup/SystemOverview/home", section: "Platform Tools > Environments", prod: false}, + //Platform Tools > User Engagement + {label: "Adoption Assistance", link: "/lightning/setup/AdoptionAssistance/home", section: "Platform Tools > User Engagement", prod: false}, + {label: "Guidance Center", link: "/lightning/setup/LearningSetup/home", section: "Platform Tools > User Engagement", prod: false}, + {label: "Help Menu", link: "/lightning/setup/HelpMenu/home", section: "Platform Tools > User Engagement", prod: false}, + {label: "In-App Guidance", link: "/lightning/setup/Prompts/home", section: "Platform Tools > User Engagement", prod: false}, + //Platform Tools > Integrations + {label: "API", link: "/lightning/setup/WebServices/home", section: "Platform Tools > Integrations", prod: false}, + {label: "Basic Data Import", link: "/lightning/setup/BasicDataImport/home", section: "Platform Tools > Integrations", prod: false}, + {label: "Change Data Capture", link: "/lightning/setup/CdcObjectEnablement/home", section: "Platform Tools > Integrations", prod: false}, + {label: "Data Import Wizard", link: "/lightning/setup/DataManagementDataImporter/home", section: "Platform Tools > Integrations", prod: false}, + {label: "Data Loader", link: "/lightning/setup/DataLoader/home", section: "Platform Tools > Integrations", prod: false}, + {label: "Dataloader.io", link: "/lightning/setup/DataLoaderIo/home", section: "Platform Tools > Integrations", prod: false}, + {label: "External Data Sources", link: "/lightning/setup/ExternalDataSource/home", section: "Platform Tools > Integrations", prod: false}, + {label: "External Objects", link: "/lightning/setup/ExternalObjects/home", section: "Platform Tools > Integrations", prod: false}, + {label: "External Services", link: "/lightning/setup/ExternalServices/home", section: "Platform Tools > Integrations", prod: false}, + {label: "Platform Events", link: "/lightning/setup/EventObjects/home", section: "Platform Tools > Integrations", prod: false}, + {label: "Teams Integration", link: "/lightning/setup/MicrosoftTeamsIntegration/home", section: "Platform Tools > Integrations", prod: false}, + //Platform Tools > Notification Builder + {label: "Custom Notifications", link: "/lightning/setup/CustomNotifications/home", section: "Platform Tools > Notification Builder", prod: false}, + {label: "Notification Delivery Settings", link: "/lightning/setup/NotificationTypesManager/home", section: "Platform Tools > Notification Builder", prod: false}, + //Settings > Company Settings + {label: "Business Hours", link: "/lightning/setup/BusinessHours/home", section: "Settings > Company Settings", prod: false}, + {label: "Public Calendars and Resources", link: "/lightning/setup/Calendars/home", section: "Settings > Company Settings > Calendar Settings", prod: false}, + {label: "Company Information", link: "/lightning/setup/CompanyProfileInfo/home", section: "Settings > Company Settings", prod: false}, + {label: "Data Protection and Privacy", link: "/lightning/setup/ConsentManagement/home", section: "Settings > Company Settings", prod: false}, + {label: "Fiscal Year", link: "/lightning/setup/ForecastFiscalYear/home", section: "Settings > Company Settings", prod: false}, + {label: "Holidays", link: "/lightning/setup/Holiday/home", section: "Settings > Company Settings", prod: false}, + {label: "Language Settings", link: "/lightning/setup/LanguageSettings/home", section: "Settings > Company Settings", prod: false}, + {label: "Manage Currencies", link: "/lightning/setup/CompanyCurrency/home", section: "Settings > Company Settings", prod: false}, + {label: "Maps and Location Settings", link: "/lightning/setup/MapsAndLocationServicesSettings/home", section: "Settings > Company Settings", prod: false}, + {label: "My Domain", link: "/lightning/setup/OrgDomain/home", section: "Settings > Company Settings", prod: false}, + //Settings > Data Classification + {label: "Data Classification", link: "/lightning/setup/DataClassificationSettings/home", section: "Settings > Data Classification", prod: false}, + {label: "Data Classification Download", link: "/lightning/setup/DataClassificationDownload/home", section: "Settings > Data Classification", prod: false}, + {label: "Data Classification Upload", link: "/lightning/setup/DataClassificationUpload/home", section: "Settings > Data Classification", prod: false}, + //Settings > Privacy Center + {label: "Consent Event Stream", link: "/lightning/setup/ConsentEventStream/home", section: "Settings > Privacy Center", prod: false}, + //Settings > Identity + {label: "Auth. Providers", link: "/lightning/setup/AuthProviders/home", section: "Settings > Identity", prod: false}, + {label: "Identity Provider", link: "/lightning/setup/IdpPage/home", section: "Settings > Identity", prod: false}, + {label: "Identity Provider Event Log", link: "/lightning/setup/IdpErrorLog/home", section: "Settings > Identity", prod: false}, + {label: "Identity Verification", link: "/lightning/setup/IdentityVerification/home", section: "Settings > Identity", prod: false}, + {label: "Identity Verification History", link: "/lightning/setup/VerificationHistory/home", section: "Settings > Identity", prod: false}, + {label: "Login Flows", link: "/lightning/setup/LoginFlow/home", section: "Settings > Identity", prod: false}, + {label: "Login History", link: "/lightning/setup/OrgLoginHistory/home", section: "Settings > Identity", prod: false}, + {label: "OAuth Custom Scopes", link: "/lightning/setup/OauthCustomScope/home", section: "Settings > Identity", prod: false}, + {label: "OAuth and OpenID Connect Settings", link: "/lightning/setup/OauthOidcSettings/home", section: "Settings > Identity", prod: false}, + {label: "Single Sign-On Settings", link: "/lightning/setup/SingleSignOn/home", section: "Settings > Identity", prod: false}, + //Settings > Security + {label: "Account Owner Report", link: "/lightning/setup/SecurityAccountOwner/home", section: "Settings > Security", prod: false}, + {label: "Activations", link: "/lightning/setup/ActivatedIpAddressAndClientBrowsersPage/home", section: "Settings > Security", prod: false}, + {label: "CORS", link: "/lightning/setup/CorsWhitelistEntries/home", section: "Settings > Security", prod: false}, + {label: "CSP Trusted Sites", link: "/lightning/setup/SecurityCspTrustedSite/home", section: "Settings > Security", prod: false}, + {label: "Certificate and Key Management", link: "/lightning/setup/CertificatesAndKeysManagement/home", section: "Settings > Security", prod: false}, + {label: "Delegated Administration", link: "/lightning/setup/DelegateGroups/home", section: "Settings > Security", prod: false}, + //Settings > Security > Event Monitoring + {label: "Event Monitoring Settings", link: "/lightning/setup/EventMonitoringSetup/home", section: "Settings > Security > Event Monitoring", prod: false}, + {label: "Transaction Security Policies", link: "/lightning/setup/TransactionSecurityNew/home", section: "Settings > Security > Event Monitoring", prod: false}, + //Settings > Security + {label: "Expire All Passwords", link: "/lightning/setup/SecurityExpirePasswords/home", section: "Settings > Security", prod: false}, + {label: "Field Accessibility", link: "/lightning/setup/FieldAccessibility/home", section: "Settings > Security", prod: false}, + {label: "File Upload and Download Security", link: "/lightning/setup/FileTypeSetting/home", section: "Settings > Security", prod: false}, + {label: "Health Check", link: "/lightning/setup/HealthCheck/home", section: "Settings > Security", prod: false}, + {label: "Login Access Policies", link: "/lightning/setup/LoginAccessPolicies/home", section: "Settings > Security", prod: false}, + {label: "Named Credentials", link: "/lightning/setup/NamedCredential/home", section: "Settings > Security", prod: false}, + {label: "Network Access", link: "/lightning/setup/NetworkAccess/home", section: "Settings > Security", prod: false}, + {label: "Password Policies", link: "/lightning/setup/SecurityPolicies/home", section: "Settings > Security", prod: false}, + //Settings > Security > Platform Encryption + {label: "Advanced Settings", link: "/lightning/setup/SecurityRemoteProxy/home", section: "Settings > Security > Platform Encryption", prod: false}, + {label: "Encryption Policy", link: "/lightning/setup/EncryptionPolicy/home", section: "Settings > Security > Platform Encryption", prod: false}, + {label: "Encryption Statistics", link: "/lightning/setup/EncryptionStatistics/home", section: "Settings > Security > Platform Encryption", prod: false}, + {label: "Key Management", link: "/lightning/setup/PlatformEncryptionKeyManagement/home", section: "Settings > Security > Platform Encryption", prod: false}, + //Settings > Security + {label: "Portal Health Check", link: "/lightning/setup/PortalSecurityReport/home", section: "Settings > Security", prod: false}, + {label: "Private Connect", link: "/lightning/setup/PrivateConnect/home", section: "Settings > Security", prod: false}, + {label: "Remote Site Settings", link: "/lightning/setup/SecurityRemoteProxy/home", section: "Settings > Security", prod: false}, + {label: "Session Management", link: "/lightning/setup/SessionManagementPage/home", section: "Settings > Security", prod: false}, + {label: "Session Settings", link: "/lightning/setup/SecuritySession/home", section: "Settings > Security", prod: false}, + {label: "Sharing Settings", link: "/lightning/setup/SecuritySharing/home", section: "Settings > Security", prod: false}, + {label: "Trusted URLs for Redirects", link: "/lightning/setup/SecurityRedirectWhitelistUrl/home", section: "Settings > Security", prod: false}, + {label: "View Setup Audit Trail", link: "/lightning/setup/SecurityEvents/home", section: "Settings > Security", prod: false}, + + //Custom Link: + {label: "Create New Flow", link: "/builder_platform_interaction/flowBuilder.app", section: "Platform Tools > Objects and Fields > New", prod: false}, + {label: "Create New Custom Object", link: "/lightning/setup/ObjectManager/new", section: "Platform Tools > Process Automation", prod: false}, + {label: "Create New Permission Set", link: "/lightning/setup/PermSets/page?address=/udd/PermissionSet/newPermissionSet.apexp", section: "Administration > Users > Permission Set", prod: false}, + {label: "Create New Custom Permission", link: "/lightning/setup/CustomPermissions/page?address=/0CP/e", section: "Platform Tools > Custom Code > Custom Permission", prod: false}, + {label: "Recycle Bin", link: "/lightning/o/DeleteEvent/home", section: "App Launcher > Custom Link", prod: false} +]; diff --git a/addon/manifest-template.json b/addon/manifest-template.json index 2c486b0a..e201f6f0 100644 --- a/addon/manifest-template.json +++ b/addon/manifest-template.json @@ -1,7 +1,7 @@ { "name": "Salesforce Inspector reloaded", "description": "Productivity tools for Salesforce administrators and developers to inspect data and metadata directly from the Salesforce UI.", - "version": "1.19", + "version": "1.20", "icons": { "128": "icon128.png" }, @@ -12,14 +12,17 @@ } }, "minimum_chrome_version": "88", - "permissions": [ - "cookies" - ], + "permissions": ["cookies"], "host_permissions": [ "https://*.salesforce.com/*", "https://*.force.com/*", "https://*.cloudforce.com/*", - "https://*.visualforce.com/*" + "https://*.visualforce.com/*", + "https://*.salesforce.mil/*", + "https://*.force.mil/*", + "https://*.cloudforce.mil/*", + "https://*.visualforce.mil/*", + "https://*.crmforce.mil/*" ], "content_scripts": [ { @@ -29,18 +32,18 @@ "https://*.vf.force.com/*", "https://*.lightning.force.com/*", "https://*.cloudforce.com/*", - "https://*.visualforce.com/*" + "https://*.visualforce.com/*", + "https://*.salesforce.mil/*", + "https://*.visual.force.mil/*", + "https://*.vf.force.mil/*", + "https://*.lightning.force.mil/*", + "https://*.cloudforce.mil/*", + "https://*.visualforce.mil/*", + "https://*.crmforce.mil/*" ], "all_frames": true, - "css": [ - "button.css", - "inspect-inline.css" - ], - "js": [ - "button.js", - "inspect-inline.js", - "links.js" - ] + "css": ["button.css", "inspect-inline.css"], + "js": ["button.js", "inspect-inline.js", "links.js"] } ], "background": { @@ -58,12 +61,10 @@ "explore-api.html", "limits.html" ], - "matches": [ - "https://*/*" - ], + "matches": ["https://*/*"], "extension_ids": [] } ], "incognito": "replaced-at-build", "manifest_version": 3 -} \ No newline at end of file +} diff --git a/addon/manifest.json b/addon/manifest.json index baab7ae0..1737080c 100644 --- a/addon/manifest.json +++ b/addon/manifest.json @@ -1,19 +1,22 @@ { "name": "Salesforce Inspector reloaded", "description": "Productivity tools for Salesforce administrators and developers to inspect data and metadata directly from the Salesforce UI.", - "version": "1.19", + "version": "1.20", "icons": { "128": "icon128.png" }, "minimum_chrome_version": "88", - "permissions": [ - "cookies" - ], + "permissions": ["cookies"], "host_permissions": [ "https://*.salesforce.com/*", "https://*.force.com/*", "https://*.cloudforce.com/*", - "https://*.visualforce.com/*" + "https://*.visualforce.com/*", + "https://*.salesforce.mil/*", + "https://*.force.mil/*", + "https://*.cloudforce.mil/*", + "https://*.visualforce.mil/*", + "https://*.crmforce.mil/*" ], "content_scripts": [ { @@ -23,17 +26,18 @@ "https://*.vf.force.com/*", "https://*.lightning.force.com/*", "https://*.cloudforce.com/*", - "https://*.visualforce.com/*" + "https://*.visualforce.com/*", + "https://*.salesforce.mil/*", + "https://*.visual.force.mil/*", + "https://*.vf.force.mil/*", + "https://*.lightning.force.mil/*", + "https://*.cloudforce.mil/*", + "https://*.visualforce.mil/*", + "https://*.crmforce.mil/*" ], "all_frames": true, - "css": [ - "button.css", - "inspect-inline.css" - ], - "js": [ - "button.js", - "inspect-inline.js" - ] + "css": ["button.css", "inspect-inline.css"], + "js": ["button.js", "inspect-inline.js"] } ], "background": { @@ -51,12 +55,10 @@ "explore-api.html", "limits.html" ], - "matches": [ - "https://*/*" - ], + "matches": ["https://*/*"], "extension_ids": [] } ], "incognito": "split", "manifest_version": 3 -} \ No newline at end of file +} diff --git a/addon/popup.js b/addon/popup.js index 6f9d7dd5..535ffead 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -1,12 +1,12 @@ /* global React ReactDOM */ -import { sfConn, apiVersion } from "./inspector.js"; -import { getAllFieldSetupLinks } from "./setup-links.js"; -import { setupLinks } from "./links.js"; +import {sfConn, apiVersion} from "./inspector.js"; +import {getAllFieldSetupLinks} from "./setup-links.js"; +import {setupLinks} from "./links.js"; let h = React.createElement; { - parent.postMessage({ insextInitRequest: true }, "*"); + parent.postMessage({insextInitRequest: true}, "*"); addEventListener("message", function initResponseHandler(e) { if (e.source == parent && e.data.insextInitResponse) { removeEventListener("message", initResponseHandler); @@ -17,10 +17,10 @@ let h = React.createElement; } function closePopup() { - parent.postMessage({ insextClosePopup: true }, "*"); + parent.postMessage({insextClosePopup: true}, "*"); } -function init({ sfHost, inDevConsole, inLightning, inInspector }) { +function init({sfHost, inDevConsole, inLightning, inInspector}) { let addonVersion = chrome.runtime.getManifest().version; sfConn.getSession(sfHost).then(() => { @@ -60,7 +60,7 @@ class App extends React.PureComponent { } onContextUrlMessage(e) { if (e.source == parent && e.data.insextUpdateRecordId) { - let { locationHref } = e.data; + let {locationHref} = e.data; this.setState({ isInSetup: locationHref.includes("/lightning/setup/"), contextUrl: locationHref @@ -79,41 +79,55 @@ class App extends React.PureComponent { } if (e.key == "e") { e.preventDefault(); + this.refs.dataExportBtn.target = getLinkTarget(e); this.refs.dataExportBtn.click(); } if (e.key == "i") { e.preventDefault(); + this.refs.dataImportBtn.target = getLinkTarget(e); this.refs.dataImportBtn.click(); } if (e.key == "l") { e.preventDefault(); + this.refs.limitsBtn.target = getLinkTarget(e); this.refs.limitsBtn.click(); } if (e.key == "d") { e.preventDefault(); - this.refs.metaRetrieveBtn.click(); - } - if (e.key == "d") { - e.preventDefault(); + this.refs.metaRetrieveBtn.target = getLinkTarget(e); this.refs.metaRetrieveBtn.click(); } if (e.key == "x") { e.preventDefault(); + this.refs.apiExploreBtn.target = getLinkTarget(e); this.refs.apiExploreBtn.click(); } if (e.key == "h" && this.refs.homeBtn) { + e.preventDefault(); + this.refs.homeBtn.target = getLinkTarget(e); this.refs.homeBtn.click(); } - //TODO: Add shortcut for "u to go to user aspect" + if (e.key == "o") { + e.preventDefault(); + this.refs.showAllDataBox.refs.objectTab.click(); + } + if (e.key == "u") { + e.preventDefault(); + this.refs.showAllDataBox.refs.userTab.click(); + } + if (e.key == "s") { + e.preventDefault(); + this.refs.showAllDataBox.refs.shortcutTab.click(); + } } onChangeApi(e) { localStorage.setItem("apiVersion", e.target.value + ".0"); - this.setState({ apiVersionInput: e.target.value }); + this.setState({apiVersionInput: e.target.value}); } componentDidMount() { addEventListener("message", this.onContextUrlMessage); addEventListener("keydown", this.onShortcutKey); - parent.postMessage({ insextLoaded: true }, "*"); + parent.postMessage({insextLoaded: true}, "*"); } componentWillUnmount() { removeEventListener("message", this.onContextUrlMessage); @@ -137,19 +151,20 @@ class App extends React.PureComponent { inInspector, addonVersion } = this.props; - let { isInSetup, contextUrl, apiVersionInput } = this.state; + let {isInSetup, contextUrl, apiVersionInput} = this.state; let clientId = localStorage.getItem(sfHost + "_clientId"); let orgInstance = this.getOrgInstance(sfHost); let hostArg = new URLSearchParams(); hostArg.set("host", sfHost); let linkInNewTab = localStorage.getItem("openLinksInNewTab"); let linkTarget = inDevConsole || linkInNewTab ? "_blank" : "_top"; + let browser = navigator.userAgent.includes("Chrome") ? "chrome" : "moz"; return ( h("div", {}, - h("div", { className: "slds-grid slds-theme_shade slds-p-vertical_x-small slds-border_bottom" }, - h("div", { className: "header-logo" }, - h("div", { className: "header-icon slds-icon_container" }, - h("svg", { className: "slds-icon", viewBox: "0 0 24 24" }, + h("div", {className: "slds-grid slds-theme_shade slds-p-vertical_x-small slds-border_bottom"}, + h("div", {className: "header-logo"}, + h("div", {className: "header-icon slds-icon_container"}, + h("svg", {className: "slds-icon", viewBox: "0 0 24 24"}, h("path", { d: ` M11 9c-.5 0-1-.5-1-1s.5-1 1-1 1 .5 1 1-.5 1-1 1z @@ -160,42 +175,42 @@ class App extends React.PureComponent { `}) ) ), - "Salesforce Inspector" + "Salesforce Inspector Reloaded" ) ), - h("div", { className: "main" }, - h(AllDataBox, { ref: "showAllDataBox", sfHost, showDetailsSupported: !inLightning && !inInspector, linkTarget, contextUrl }), - h("div", { className: "slds-p-vertical_x-small slds-p-horizontal_x-small slds-border_bottom" }, - h("div", { className: "slds-m-bottom_xx-small" }, - h("a", { ref: "dataExportBtn", href: "data-export.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral" }, h("span", {}, "Data ", h("u", {}, "E"), "xport")) + h("div", {className: "main"}, + h(AllDataBox, {ref: "showAllDataBox", sfHost, showDetailsSupported: !inLightning && !inInspector, linkTarget, contextUrl}), + h("div", {className: "slds-p-vertical_x-small slds-p-horizontal_x-small slds-border_bottom"}, + h("div", {className: "slds-m-bottom_xx-small"}, + h("a", {ref: "dataExportBtn", href: "data-export.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral"}, h("span", {}, "Data ", h("u", {}, "E"), "xport")) ), - h("div", { className: "slds-m-bottom_xx-small" }, - h("a", { ref: "dataImportBtn", href: "data-import.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral" }, h("span", {}, "Data ", h("u", {}, "I"), "mport")) + h("div", {className: "slds-m-bottom_xx-small"}, + h("a", {ref: "dataImportBtn", href: "data-import.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral"}, h("span", {}, "Data ", h("u", {}, "I"), "mport")) ), h("div", {}, - h("a", { ref: "limitsBtn", href: "limits.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral" }, h("span", {}, "Org ", h("u", {}, "L"), "imits")) + h("a", {ref: "limitsBtn", href: "limits.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral"}, h("span", {}, "Org ", h("u", {}, "L"), "imits")) ), ), - h("div", { className: "slds-p-vertical_x-small slds-p-horizontal_x-small" }, + h("div", {className: "slds-p-vertical_x-small slds-p-horizontal_x-small"}, // Advanded features should be put below this line, and the layout adjusted so they are below the fold - h("div", { className: "slds-m-bottom_xx-small" }, - h("a", { ref: "metaRetrieveBtn", href: "metadata-retrieve.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral" }, h("span", {}, h("u", {}, "D"), "ownload Metadata")) + h("div", {className: "slds-m-bottom_xx-small"}, + h("a", {ref: "metaRetrieveBtn", href: "metadata-retrieve.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral"}, h("span", {}, h("u", {}, "D"), "ownload Metadata")) ), - h("div", { className: "slds-m-bottom_xx-small" }, - h("a", { ref: "apiExploreBtn", href: "explore-api.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral" }, h("span", {}, "E", h("u", {}, "x"), "plore API")) + h("div", {className: "slds-m-bottom_xx-small"}, + h("a", {ref: "apiExploreBtn", href: "explore-api.html?" + hostArg, target: linkTarget, className: "page-button slds-button slds-button_neutral"}, h("span", {}, "E", h("u", {}, "x"), "plore API")) ), - h("div", { className: "slds-m-bottom_xx-small" }, + h("div", {className: "slds-m-bottom_xx-small"}, h("a", { ref: "generateToken", - href: `https://${sfHost}/services/oauth2/authorize?response_type=token&client_id=` + clientId + "&redirect_uri=chrome-extension://" + chrome.runtime.id + "/data-export.html?host=" + sfHost + "%26", + href: `https://${sfHost}/services/oauth2/authorize?response_type=token&client_id=` + clientId + "&redirect_uri=" + browser + "-extension://" + chrome.i18n.getMessage("@@extension_id") + "/data-export.html?host=" + sfHost + "%26", target: linkTarget, className: !clientId ? "button hide" : "page-button slds-button slds-button_neutral" }, h("span", {}, h("u", {}, "G"), "enerate Connected App Token")) ), // Workaround for in Lightning the link to Setup always opens a new tab, and the link back cannot open a new tab. - inLightning && isInSetup && h("div", { className: "slds-m-bottom_xx-small" }, + inLightning && isInSetup && h("div", {className: "slds-m-bottom_xx-small"}, h("a", { ref: "homeBtn", @@ -206,7 +221,7 @@ class App extends React.PureComponent { }, h("span", {}, "Salesforce ", h("u", {}, "H"), "ome")) ), - inLightning && !isInSetup && h("div", { className: "slds-m-bottom_xx-small" }, + inLightning && !isInSetup && h("div", {className: "slds-m-bottom_xx-small"}, h("a", { ref: "homeBtn", @@ -219,11 +234,11 @@ class App extends React.PureComponent { ), ) ), - h("div", { className: "slds-grid slds-theme_shade slds-p-around_small slds-border_top" }, - h("div", { className: "slds-col slds-size_5-of-12 footer-small-text slds-m-top_xx-small" }, - h("a", { href: "https://github.com/tprouvot/Chrome-Salesforce-inspector/blob/master/CHANGES.md", title: "Release note", target: linkTarget }, "v" + addonVersion), + h("div", {className: "slds-grid slds-theme_shade slds-p-around_small slds-border_top"}, + h("div", {className: "slds-col slds-size_5-of-12 footer-small-text slds-m-top_xx-small"}, + h("a", {href: "https://tprouvot.github.io/Salesforce-Inspector-reloaded/release-note/", title: "Release note", target: linkTarget}, "v" + addonVersion), h("span", {}, " / "), - h("a", { href: "https://status.salesforce.com/instances/" + orgInstance, title: "Instance status", target: linkTarget }, orgInstance), + h("a", {href: "https://status.salesforce.com/instances/" + orgInstance, title: "Instance status", target: linkTarget}, orgInstance), h("span", {}, " / "), h("input", { className: "api-input", @@ -233,14 +248,14 @@ class App extends React.PureComponent { value: apiVersionInput.split(".0")[0] }) ), - h("div", { className: "slds-col slds-size_3-of-12 slds-text-align_left" }, - h("span", { className: "footer-small-text" }, navigator.userAgentData.platform.indexOf("mac") > -1 ? "[ctrl+option+i]" : "[ctrl+alt+i]" + " to open") + h("div", {className: "slds-col slds-size_3-of-12 slds-text-align_left"}, + h("span", {className: "footer-small-text"}, navigator.userAgentData.platform.indexOf("mac") > -1 ? "[ctrl+option+i]" : "[ctrl+alt+i]" + " to open") ), - h("div", { className: "slds-col slds-size_2-of-12 slds-text-align_right" }, - h("a", { href: "https://github.com/tprouvot/Chrome-Salesforce-inspector", target: linkTarget }, "About") + h("div", {className: "slds-col slds-size_2-of-12 slds-text-align_right"}, + h("a", {href: "https://github.com/tprouvot/Salesforce-Inspector-reloaded#salesforce-inspector-reloaded", target: linkTarget}, "About") ), - h("div", { className: "slds-col slds-size_2-of-12 slds-text-align_right" }, - h("a", { href: "https://github.com/tprouvot/Chrome-Salesforce-inspector/wiki", target: linkTarget }, "Wiki") + h("div", {className: "slds-col slds-size_2-of-12 slds-text-align_right"}, + h("a", {href: "https://tprouvot.github.io/Salesforce-Inspector-reloaded/", target: linkTarget}, "Doc") ) ) ) @@ -252,7 +267,7 @@ class AllDataBox extends React.PureComponent { constructor(props) { super(props); - this.SearchAspectTypes = Object.freeze({ sobject: "sobject", users: "users", shortcuts: "shortcuts" }); //Enum. Supported aspects + this.SearchAspectTypes = Object.freeze({sobject: "sobject", users: "users", shortcuts: "shortcuts"}); //Enum. Supported aspects this.state = { activeSearchAspect: this.SearchAspectTypes.sobject, @@ -263,6 +278,7 @@ class AllDataBox extends React.PureComponent { contextUserId: null, contextOrgId: null, contextPath: null, + contextSobject: null }; this.onAspectClick = this.onAspectClick.bind(this); this.parseContextUrl = this.ensureKnownBrowserContext.bind(this); @@ -274,7 +290,7 @@ class AllDataBox extends React.PureComponent { } componentDidUpdate(prevProps, prevState) { - let { activeSearchAspect } = this.state; + let {activeSearchAspect} = this.state; if (prevProps.contextUrl !== this.props.contextUrl) { this.ensureKnownBrowserContext(); } @@ -294,38 +310,40 @@ class AllDataBox extends React.PureComponent { } ensureKnownBrowserContext() { - let { contextUrl } = this.props; + let {contextUrl} = this.props; if (contextUrl) { let recordId = getRecordId(contextUrl); let path = getSfPathFromUrl(contextUrl); + let sobject = getSobject(contextUrl); this.setState({ contextRecordId: recordId, - contextPath: path + contextPath: path, + contextSobject: sobject }); } } setIsLoading(aspect, value) { switch (aspect) { - case "usersBox": this.setState({ usersBoxLoading: value }); + case "usersBox": this.setState({usersBoxLoading: value}); break; } } isLoading() { - let { usersBoxLoading, sobjectsLoading } = this.state; + let {usersBoxLoading, sobjectsLoading} = this.state; return sobjectsLoading || usersBoxLoading; } async ensureKnownUserContext() { - let { contextUserId, contextOrgId } = this.state; + let {contextUserId, contextOrgId} = this.state; if (!contextUserId || !contextOrgId) { try { const userInfo = await sfConn.rest("/services/oauth2/userinfo"); let contextUserId = userInfo.user_id; let contextOrgId = userInfo.organization_id; - this.setState({ contextUserId, contextOrgId }); + this.setState({contextUserId, contextOrgId}); } catch (err) { console.error("Unable to query user context", err); } @@ -341,7 +359,7 @@ class AllDataBox extends React.PureComponent { loadSobjects() { let entityMap = new Map(); - function addEntity({ name, label, keyPrefix, durableId, isCustomSetting }, api) { + function addEntity({name, label, keyPrefix, durableId, isCustomSetting}, api) { label = label || ""; // Avoid null exceptions if the object does not have a label (some don't). All objects have a name. Not needed for keyPrefix since we only do equality comparisons on those. let entity = entityMap.get(name); if (entity) { @@ -381,21 +399,31 @@ class AllDataBox extends React.PureComponent { }); } - function getEntityDefinitions(bucket) { - let query = "select QualifiedApiName, Label, KeyPrefix, DurableId, IsCustomSetting from EntityDefinition" + bucket; - return sfConn.rest("/services/data/v" + apiVersion + "/tooling/query?q=" + encodeURIComponent(query)).then(res => { - for (let record of res.records) { - addEntity({ - name: record.QualifiedApiName, - label: record.Label, - keyPrefix: record.KeyPrefix, - durableId: record.DurableId, - isCustomSetting: record.IsCustomSetting - }, null); - } - }).catch(err => { - console.error("list entity definitions: " + bucket, err); - }); + function getEntityDefinitions(){ + return sfConn.rest("/services/data/v" + apiVersion + "/tooling/query?q=" + encodeURIComponent("SELECT COUNT() FROM EntityDefinition")) + .then(res => { + let entityNb = res.totalSize; + for (let bucket = 0; bucket < Math.ceil(entityNb / 2000); bucket++) { + let offset = bucket > 0 ? " OFFSET " + (bucket * 2000) : ""; + let query = "SELECT QualifiedApiName, Label, KeyPrefix, DurableId, IsCustomSetting FROM EntityDefinition ORDER BY QualifiedApiName ASC LIMIT 2000" + offset; + sfConn.rest("/services/data/v" + apiVersion + "/tooling/query?q=" + encodeURIComponent(query)) + .then(respEntity => { + for (let record of respEntity.records) { + addEntity({ + name: record.QualifiedApiName, + label: record.Label, + keyPrefix: record.KeyPrefix, + durableId: record.DurableId, + isCustomSetting: record.IsCustomSetting + }, null); + } + }).catch(err => { + console.error("list entity definitions: ", err); + }); + } + }).catch(err => { + console.error("count entity definitions: ", err); + }); } Promise.all([ @@ -406,11 +434,8 @@ class AllDataBox extends React.PureComponent { // Get all objects, even the ones the user cannot access from any API // These records are less interesting than the ones the user has access to, but still interesting since we can get information about them using the tooling API // If there are too many records, we get "EXCEEDED_ID_LIMIT: EntityDefinition does not support queryMore(), use LIMIT to restrict the results to a single batch" - // We cannot use limit and offset to work around it, since EntityDefinition does not support those according to the documentation, and they seem to work in a querky way in practice. - // Tried to use http://salesforce.stackexchange.com/a/22643, but "order by x" uses AaBbCc as sort order, while "where x > ..." uses sort order ABCabc, so it does not work on text fields, and there is no unique numerical field we can sort by. - // Here we split the query into a somewhat arbitrary set of fixed buckets, and hope none of the buckets exceed 2000 records. - getEntityDefinitions(" where QualifiedApiName < 'M' limit 2000"), - getEntityDefinitions(" where QualifiedApiName >= 'M' limit 2000"), + // Even if documentation mention that LIMIT and OFFSET are not supported, we use it to split the EntityDefinition queries into 2000 buckets + getEntityDefinitions(), ]) .then(() => { // TODO progressively display data as each of the three responses becomes available @@ -422,28 +447,28 @@ class AllDataBox extends React.PureComponent { }) .catch(e => { console.error(e); - this.setState({ sobjectsLoading: false }); + this.setState({sobjectsLoading: false}); }); } render() { - let { activeSearchAspect, sobjectsLoading, contextRecordId, contextUserId, contextOrgId, contextPath, sobjectsList } = this.state; - let { sfHost, showDetailsSupported, linkTarget } = this.props; + let {activeSearchAspect, sobjectsLoading, contextRecordId, contextSobject, contextUserId, contextOrgId, contextPath, sobjectsList} = this.state; + let {sfHost, showDetailsSupported, linkTarget} = this.props; return ( - h("div", { className: "slds-p-top_small slds-p-horizontal_x-small slds-p-bottom_x-small slds-border_bottom" + (this.isLoading() ? " loading " : "") }, - h("ul", { className: "small-tabs" }, - h("li", { onClick: this.onAspectClick, "data-aspect": this.SearchAspectTypes.sobject, className: (activeSearchAspect == this.SearchAspectTypes.sobject) ? "active" : "" }, "Objects"), - h("li", { onClick: this.onAspectClick, "data-aspect": this.SearchAspectTypes.users, className: (activeSearchAspect == this.SearchAspectTypes.users) ? "active" : "" }, "Users"), - h("li", { onClick: this.onAspectClick, "data-aspect": this.SearchAspectTypes.shortcuts, className: (activeSearchAspect == this.SearchAspectTypes.shortcuts) ? "active" : "" }, "Shortcuts") + h("div", {className: "slds-p-top_small slds-p-horizontal_x-small slds-p-bottom_x-small slds-border_bottom" + (this.isLoading() ? " loading " : "")}, + h("ul", {className: "small-tabs"}, + h("li", {ref: "objectTab", onClick: this.onAspectClick, "data-aspect": this.SearchAspectTypes.sobject, className: (activeSearchAspect == this.SearchAspectTypes.sobject) ? "active" : ""}, h("span", {}, h("u", {}, "O"), "bjects")), + h("li", {ref: "userTab", onClick: this.onAspectClick, "data-aspect": this.SearchAspectTypes.users, className: (activeSearchAspect == this.SearchAspectTypes.users) ? "active" : ""}, h("span", {}, h("u", {}, "U"), "sers")), + h("li", {ref: "shortcutTab", onClick: this.onAspectClick, "data-aspect": this.SearchAspectTypes.shortcuts, className: (activeSearchAspect == this.SearchAspectTypes.shortcuts) ? "active" : ""}, h("span", {}, h("u", {}, "S"), "hortcuts")) ), (activeSearchAspect == this.SearchAspectTypes.sobject) - ? h(AllDataBoxSObject, { ref: "showAllDataBoxSObject", sfHost, showDetailsSupported, sobjectsList, sobjectsLoading, contextRecordId, linkTarget }) + ? h(AllDataBoxSObject, {ref: "showAllDataBoxSObject", sfHost, showDetailsSupported, sobjectsList, sobjectsLoading, contextRecordId, contextSobject, linkTarget}) : (activeSearchAspect == this.SearchAspectTypes.users) - ? h(AllDataBoxUsers, { ref: "showAllDataBoxUsers", sfHost, linkTarget, contextUserId, contextOrgId, contextPath, setIsLoading: (value) => { this.setIsLoading("usersBox", value); } }, "Users") + ? h(AllDataBoxUsers, {ref: "showAllDataBoxUsers", sfHost, linkTarget, contextUserId, contextOrgId, contextPath, setIsLoading: (value) => { this.setIsLoading("usersBox", value); }}, "Users") : "AllData aspect " + activeSearchAspect + " not implemented" - ? h(AllDataBoxShortcut, { ref: "showAllDataBoxShortcuts", sfHost, linkTarget, contextUserId, contextOrgId, contextPath, setIsLoading: (value) => { this.setIsLoading("shortcutsBox", value); } }, "Users") + ? h(AllDataBoxShortcut, {ref: "showAllDataBoxShortcuts", sfHost, linkTarget, contextUserId, contextOrgId, contextPath, setIsLoading: (value) => { this.setIsLoading("shortcutsBox", value); }}, "Users") : "AllData aspect " + activeSearchAspect + " not implemented" ) ); @@ -462,19 +487,19 @@ class AllDataBoxUsers extends React.PureComponent { } componentDidMount() { - let { contextUserId } = this.props; - this.onDataSelect({ Id: contextUserId }); + let {contextUserId} = this.props; + this.onDataSelect({Id: contextUserId}); this.refs.allDataSearch.refs.showAllDataInp.focus(); } componentDidUpdate(prevProps) { if (prevProps.contextUserId !== this.props.contextUserId) { - this.onDataSelect({ Id: this.props.contextUserId }); + this.onDataSelect({Id: this.props.contextUserId}); } } async getMatches(userQuery) { - let { setIsLoading } = this.props; + let {setIsLoading} = this.props; if (!userQuery) { return []; } @@ -499,7 +524,7 @@ class AllDataBoxUsers extends React.PureComponent { try { setIsLoading(true); - const userSearchResult = await sfConn.rest("/services/data/v" + apiVersion + "/composite", { method: "POST", body: compositeQuery }); + const userSearchResult = await sfConn.rest("/services/data/v" + apiVersion + "/composite", {method: "POST", body: compositeQuery}); let users = userSearchResult.compositeResponse.find((elm) => elm.httpStatusCode == 200).body.records; return users; } catch (err) { @@ -513,22 +538,22 @@ class AllDataBoxUsers extends React.PureComponent { async onDataSelect(userRecord) { if (userRecord && userRecord.Id) { - await this.setState({ selectedUserId: userRecord.Id, selectedUser: null }); + await this.setState({selectedUserId: userRecord.Id, selectedUser: null}); await this.querySelectedUserDetails(); } } async querySelectedUserDetails() { - let { selectedUserId } = this.state; - let { setIsLoading } = this.props; + let {selectedUserId} = this.state; + let {setIsLoading} = this.props; if (!selectedUserId) { return; } //Optimistically attempt broad query (fullQuery) and fall back to minimalQuery to ensure some data is returned in most cases (e.g. profile cannot be queried by community users) - const fullQuerySelect = "select Id, Name, Email, Username, UserRole.Name, Alias, LocaleSidKey, LanguageLocaleKey, IsActive, FederationIdentifier, ProfileId, Profile.Name"; - const minimalQuerySelect = "select Id, Name, Email, Username, UserRole.Name, Alias, LocaleSidKey, LanguageLocaleKey, IsActive, FederationIdentifier"; - const queryFrom = "from User where Id='" + selectedUserId + "' limit 1"; + const fullQuerySelect = "SELECT Id, Name, Email, Username, UserRole.Name, Alias, LocaleSidKey, LanguageLocaleKey, IsActive, FederationIdentifier, ProfileId, Profile.Name, ContactId, IsPortalEnabled"; + const minimalQuerySelect = "SELECT Id, Name, Email, Username, UserRole.Name, Alias, LocaleSidKey, LanguageLocaleKey, IsActive, FederationIdentifier, ContactId, IsPortalEnabled"; + const queryFrom = "FROM User WHERE Id='" + selectedUserId + "' LIMIT 1"; const compositeQuery = { "compositeRequest": [ { @@ -546,9 +571,17 @@ class AllDataBoxUsers extends React.PureComponent { try { setIsLoading(true); //const userResult = await sfConn.rest("/services/data/v" + apiVersion + "/sobjects/User/" + selectedUserId); //Does not return profile details. Query call is therefore prefered - const userResult = await sfConn.rest("/services/data/v" + apiVersion + "/composite", { method: "POST", body: compositeQuery }); + const userResult = await sfConn.rest("/services/data/v" + apiVersion + "/composite", {method: "POST", body: compositeQuery}); let userDetail = userResult.compositeResponse.find((elm) => elm.httpStatusCode == 200).body.records[0]; - await this.setState({ selectedUser: userDetail }); + //query NetworkMember only if it is a portal user (display "Login to Experience" button) + if (userDetail.IsPortalEnabled){ + await sfConn.rest("/services/data/v" + apiVersion + "/query/?q=SELECT+NetworkId+FROM+NetworkMember+WHERE+MemberId='" + userDetail.Id + "'").then(res => { + if (res.records && res.records.length > 0){ + userDetail.NetworkId = res.records[0].NetworkId; + } + }); + } + await this.setState({selectedUser: userDetail}); } catch (err) { console.error("Unable to query user details with: " + JSON.stringify(compositeQuery) + ".", err); } finally { @@ -561,13 +594,13 @@ class AllDataBoxUsers extends React.PureComponent { key: value.Id, value, element: [ - h("div", { className: "autocomplete-item-main", key: "main" }, + h("div", {className: "autocomplete-item-main", key: "main"}, h(MarkSubstring, { text: value.Name + " (" + value.Alias + ")", start: value.Name.toLowerCase().indexOf(userQuery.toLowerCase()), length: userQuery.length })), - h("div", { className: "autocomplete-item-sub small", key: "sub" }, + h("div", {className: "autocomplete-item-sub small", key: "sub"}, h("div", {}, (value.Profile) ? value.Profile.Name : ""), h(MarkSubstring, { text: (!value.IsActive) ? "⚠ " + value.Username : value.Username, @@ -579,16 +612,16 @@ class AllDataBoxUsers extends React.PureComponent { } render() { - let { selectedUser } = this.state; - let { sfHost, linkTarget, contextOrgId, contextUserId, contextPath } = this.props; + let {selectedUser} = this.state; + let {sfHost, linkTarget, contextOrgId, contextUserId, contextPath} = this.props; return ( - h("div", { ref: "usersBox", className: "users-box" }, - h(AllDataSearch, { ref: "allDataSearch", getMatches: this.getMatches, onDataSelect: this.onDataSelect, inputSearchDelay: 400, placeholderText: "Username, email, alias or name of user", resultRender: this.resultRender }), - h("div", { className: "all-data-box-inner" + (!selectedUser ? " empty" : "") }, + h("div", {ref: "usersBox", className: "users-box"}, + h(AllDataSearch, {ref: "allDataSearch", getMatches: this.getMatches, onDataSelect: this.onDataSelect, inputSearchDelay: 400, placeholderText: "Username, email, alias or name of user", resultRender: this.resultRender}), + h("div", {className: "all-data-box-inner" + (!selectedUser ? " empty" : "")}, selectedUser - ? h(UserDetails, { user: selectedUser, sfHost, contextOrgId, currentUserId: contextUserId, linkTarget, contextPath }) - : h("div", { className: "center" }, "No user details available") + ? h(UserDetails, {user: selectedUser, sfHost, contextOrgId, currentUserId: contextUserId, linkTarget, contextPath}) + : h("div", {className: "center"}, "No user details available") )) ); } @@ -606,60 +639,73 @@ class AllDataBoxSObject extends React.PureComponent { } componentDidMount() { - let { contextRecordId } = this.props; - this.updateSelection(contextRecordId); + let {contextRecordId, contextSobject} = this.props; + this.updateSelection(contextRecordId, contextSobject); } componentDidUpdate(prevProps) { - let { contextRecordId, sobjectsLoading } = this.props; + let {contextRecordId, sobjectsLoading, contextSobject} = this.props; if (prevProps.contextRecordId !== contextRecordId) { - this.updateSelection(contextRecordId); + this.updateSelection(contextRecordId, contextSobject); } if (prevProps.sobjectsLoading !== sobjectsLoading && !sobjectsLoading) { - this.updateSelection(contextRecordId); + this.updateSelection(contextRecordId, contextSobject); } } - async updateSelection(query) { - let match = this.getBestMatch(query); - await this.setState({ selectedValue: match }); + async updateSelection(query, contextSobject) { + let match; + if (query === "list"){ + match = this.getBestMatch(contextSobject); + } else { + match = this.getBestMatch(query); + } + + await this.setState({selectedValue: match}); this.loadRecordIdDetails(); } loadRecordIdDetails() { - let { selectedValue } = this.state; + let {selectedValue} = this.state; //If a recordId is selected and the object supports regularApi if (selectedValue && selectedValue.recordId && selectedValue.sobject && selectedValue.sobject.availableApis && selectedValue.sobject.availableApis.includes("regularApi")) { - //optimistically assume the object has certain attribues. If some are not present, no recordIdDetails are displayed - //TODO: Better handle objects with no recordtypes. Currently the optimistic approach results in no record details being displayed for ids for objects without record types. - let query = "select Id, LastModifiedBy.Alias, CreatedBy.Alias, RecordType.DeveloperName, RecordType.Id, CreatedDate, LastModifiedDate from " + selectedValue.sobject.name + " where id='" + selectedValue.recordId + "'"; - sfConn.rest("/services/data/v" + apiVersion + "/query?q=" + encodeURIComponent(query), { logErrors: false }).then(res => { - for (let record of res.records) { - let lastModifiedDate = new Date(record.LastModifiedDate); - let createdDate = new Date(record.CreatedDate); - this.setState({ - recordIdDetails: { - "recordTypeId": (record.RecordType) ? record.RecordType.Id : "-", - "recordTypeName": (record.RecordType) ? record.RecordType.DeveloperName : "-", - "createdBy": record.CreatedBy.Alias, - "lastModifiedBy": record.LastModifiedBy.Alias, - "created": createdDate.toLocaleDateString() + " " + createdDate.toLocaleTimeString(), - "lastModified": lastModifiedDate.toLocaleDateString() + " " + lastModifiedDate.toLocaleTimeString(), - } - }); - } - }).catch(() => { - //Swallow this exception since it is likely due to missing standard attributes on the record - i.e. an invalid query. - this.setState({ recordIdDetails: null }); - }); - + let fields = ["Id", "LastModifiedBy.Alias", "CreatedBy.Alias", "RecordType.DeveloperName", "RecordType.Id", "CreatedDate", "LastModifiedDate", "Name"]; + this.restCallForRecordDetails(fields, selectedValue); } else { - this.setState({ recordIdDetails: null }); + this.setState({recordIdDetails: null}); } } + restCallForRecordDetails(fields, selectedValue){ + let query = "SELECT " + fields.join() + " FROM " + selectedValue.sobject.name + " where id='" + selectedValue.recordId + "'"; + sfConn.rest("/services/data/v" + apiVersion + "/query?q=" + encodeURIComponent(query), {logErrors: false}).then(res => { + for (let record of res.records) { + let lastModifiedDate = new Date(record.LastModifiedDate); + let createdDate = new Date(record.CreatedDate); + this.setState({ + recordIdDetails: { + "recordTypeId": (record.RecordType) ? record.RecordType.Id : "", + "recordName": (record.Name) ? record.Name : "", + "recordTypeName": (record.RecordType) ? record.RecordType.DeveloperName : "", + "createdBy": record.CreatedBy.Alias, + "lastModifiedBy": record.LastModifiedBy.Alias, + "created": createdDate.toLocaleDateString() + " " + createdDate.toLocaleTimeString(), + "lastModified": lastModifiedDate.toLocaleDateString() + " " + lastModifiedDate.toLocaleTimeString(), + } + }); + } + }).catch(e => { + //some fields (Name, RecordTypeId) are not available for particular objects, in this case remove it from the fields list + if (e.message.includes("No such column ")){ + this.restCallForRecordDetails(fields.filter(field => field !== "Name"), selectedValue); + } else if (e.message.includes("Didn't understand relationship 'RecordType'")){ + this.restCallForRecordDetails(fields.filter(field => !field.startsWith("RecordType.")), selectedValue); + } + }); + } + getBestMatch(query) { - let { sobjectsList } = this.props; + let {sobjectsList} = this.props; // Find the best match based on the record id or object name from the page URL. if (!query) { return null; @@ -682,11 +728,11 @@ class AllDataBoxSObject extends React.PureComponent { if (sobject.keyPrefix == queryKeyPrefix && query.match(/^([a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$/)) { recordId = query; } - return { recordId, sobject }; + return {recordId, sobject}; } getMatches(query) { - let { sobjectsList, contextRecordId } = this.props; + let {sobjectsList, contextRecordId} = this.props; if (!sobjectsList) { return []; @@ -700,21 +746,21 @@ class AllDataBoxSObject extends React.PureComponent { // TO-DO: merge with the sortRank function in data-export relevance: (sobject.keyPrefix == queryKeyPrefix ? 2 - : sobject.name.toLowerCase() == query.toLowerCase() ? 3 - : sobject.label.toLowerCase() == query.toLowerCase() ? 4 - : sobject.name.toLowerCase().startsWith(query.toLowerCase()) ? 5 - : sobject.label.toLowerCase().startsWith(query.toLowerCase()) ? 6 - : sobject.name.toLowerCase().includes("__" + query.toLowerCase()) ? 7 - : sobject.name.toLowerCase().includes("_" + query.toLowerCase()) ? 8 - : sobject.label.toLowerCase().includes(" " + query.toLowerCase()) ? 9 - : 10) + (sobject.availableApis.length == 0 ? 20 : 0) + : sobject.name.toLowerCase() == query.toLowerCase() ? 3 + : sobject.label.toLowerCase() == query.toLowerCase() ? 4 + : sobject.name.toLowerCase().startsWith(query.toLowerCase()) ? 5 + : sobject.label.toLowerCase().startsWith(query.toLowerCase()) ? 6 + : sobject.name.toLowerCase().includes("__" + query.toLowerCase()) ? 7 + : sobject.name.toLowerCase().includes("_" + query.toLowerCase()) ? 8 + : sobject.label.toLowerCase().includes(" " + query.toLowerCase()) ? 9 + : 10) + (sobject.availableApis.length == 0 ? 20 : 0) })); query = query || contextRecordId || ""; queryKeyPrefix = query.substring(0, 3); if (query.match(/^([a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$/)) { let objectsForId = sobjectsList.filter(sobject => sobject.keyPrefix == queryKeyPrefix); for (let sobject of objectsForId) { - res.unshift({ recordId: query, sobject, relevance: 1 }); + res.unshift({recordId: query, sobject, relevance: 1}); } } res.sort((a, b) => a.relevance - b.relevance || a.sobject.name.localeCompare(b.sobject.name)); @@ -722,7 +768,7 @@ class AllDataBoxSObject extends React.PureComponent { } onDataSelect(value) { - this.setState({ selectedValue: value }, () => { + this.setState({selectedValue: value}, () => { this.loadRecordIdDetails(); }); } @@ -744,7 +790,7 @@ class AllDataBoxSObject extends React.PureComponent { key: value.recordId + "#" + value.sobject.name, value, element: [ - h("div", { className: "autocomplete-item-main", key: "main" }, + h("div", {className: "autocomplete-item-main", key: "main"}, value.recordId || h(MarkSubstring, { text: value.sobject.name, start: value.sobject.name.toLowerCase().indexOf(userQuery.toLowerCase()), @@ -752,7 +798,7 @@ class AllDataBoxSObject extends React.PureComponent { }), value.sobject.availableApis.length == 0 ? " (Not readable)" : "" ), - h("div", { className: "autocomplete-item-sub", key: "sub" }, + h("div", {className: "autocomplete-item-sub", key: "sub"}, h(MarkSubstring, { text: value.sobject.keyPrefix || "---", start: value.sobject.keyPrefix == userQuery.substring(0, 3) ? 0 : -1, @@ -770,14 +816,14 @@ class AllDataBoxSObject extends React.PureComponent { } render() { - let { sfHost, showDetailsSupported, sobjectsList, linkTarget, contextRecordId } = this.props; - let { selectedValue, recordIdDetails } = this.state; + let {sfHost, showDetailsSupported, sobjectsList, linkTarget, contextRecordId} = this.props; + let {selectedValue, recordIdDetails} = this.state; return ( h("div", {}, - h(AllDataSearch, { ref: "allDataSearch", onDataSelect: this.onDataSelect, sobjectsList, getMatches: this.getMatches, inputSearchDelay: 0, placeholderText: "Record id, id prefix or object name", resultRender: this.resultRender }), + h(AllDataSearch, {ref: "allDataSearch", onDataSelect: this.onDataSelect, sobjectsList, getMatches: this.getMatches, inputSearchDelay: 0, placeholderText: "Record id, id prefix or object name", resultRender: this.resultRender}), selectedValue - ? h(AllDataSelection, { ref: "allDataSelection", sfHost, showDetailsSupported, selectedValue, linkTarget, recordIdDetails, contextRecordId }) - : h("div", { className: "all-data-box-inner empty" }, "No record to display") + ? h(AllDataSelection, {ref: "allDataSelection", sfHost, showDetailsSupported, selectedValue, linkTarget, recordIdDetails, contextRecordId}) + : h("div", {className: "all-data-box-inner empty"}, "No record to display") ) ); } @@ -799,7 +845,7 @@ class AllDataBoxShortcut extends React.PureComponent { } async getMatches(shortcutSearch) { - let { setIsLoading } = this.props; + let {setIsLoading} = this.props; if (!shortcutSearch) { return []; } @@ -824,7 +870,7 @@ class AllDataBoxShortcut extends React.PureComponent { if (metadataShortcutSearch == "true"){ const flowSelect = "SELECT LatestVersionId, ApiName, Label, ProcessType FROM FlowDefinitionView WHERE Label LIKE '%" + shortcutSearch + "%' LIMIT 30"; const profileSelect = "SELECT Id, Name, UserLicense.Name FROM Profile WHERE Name LIKE '%" + shortcutSearch + "%' LIMIT 30"; - const permSetSelect = "SELECT Id, Name, Label, Type, LicenseId, License.Name FROM PermissionSet WHERE Label LIKE '%" + shortcutSearch + "%' LIMIT 30"; + const permSetSelect = "SELECT Id, Name, Label, Type, LicenseId, License.Name, PermissionSetGroupId FROM PermissionSet WHERE Label LIKE '%" + shortcutSearch + "%' LIMIT 30"; const compositeQuery = { "compositeRequest": [ { @@ -843,31 +889,40 @@ class AllDataBoxShortcut extends React.PureComponent { ] }; - const searchResult = await sfConn.rest("/services/data/v" + apiVersion + "/composite", { method: "POST", body: compositeQuery }); + const searchResult = await sfConn.rest("/services/data/v" + apiVersion + "/composite", {method: "POST", body: compositeQuery}); let results = searchResult.compositeResponse.filter((elm) => elm.httpStatusCode == 200 && elm.body.records.length > 0); + let enablePermSetSummary = localStorage.getItem("enablePermSetSummary") === "true"; + results.forEach(element => { element.body.records.forEach(rec => { - switch (rec.attributes.type) { - case "FlowDefinitionView": - rec.link = "/builder_platform_interaction/flowBuilder.app?flowId=" + rec.LatestVersionId; - rec.label = rec.Label; - rec.name = rec.ApiName; - rec.detail = rec.attributes.type + " • " + rec.ProcessType; - break; - case "Profile": - rec.link = "/lightning/setup/EnhancedProfiles/page?address=%2F" + rec.Id; - rec.label = rec.Name; - rec.name = rec.Id; - rec.detail = rec.attributes.type + " • " + rec.UserLicense.Name; - break; - case "PermissionSet": - rec.link = "/lightning/setup/PermSets/page?address=%2F" + rec.Id; - rec.label = rec.Label; - rec.name = rec.Name; - rec.detail = rec.attributes.type + " • " + rec.Type; - rec.detail += rec.License?.Name != null ? " • " + rec.License?.Name : ""; - break; + if (rec.attributes.type === "FlowDefinitionView"){ + rec.link = "/builder_platform_interaction/flowBuilder.app?flowId=" + rec.LatestVersionId; + rec.label = rec.Label; + rec.name = rec.ApiName; + rec.detail = rec.attributes.type + " • " + rec.ProcessType; + } else if (rec.attributes.type === "Profile"){ + rec.link = "/lightning/setup/EnhancedProfiles/page?address=%2F" + rec.Id; + rec.label = rec.Name; + rec.name = rec.Id; + rec.detail = rec.attributes.type + " • " + rec.UserLicense.Name; + } else if (rec.attributes.type === "PermissionSet"){ + rec.label = rec.Label; + rec.name = rec.Name; + rec.detail = rec.attributes.type + " • " + rec.Type; + rec.detail += rec.License?.Name != null ? " • " + rec.License?.Name : ""; + + let psetOrGroupId; + let type; + if (rec.Type === "Group"){ + psetOrGroupId = rec.PermissionSetGroupId; + type = "PermSetGroups"; + } else { + psetOrGroupId = rec.Id; + type = "PermSets"; + } + let endLink = enablePermSetSummary ? psetOrGroupId + "/summary" : "page?address=%2F" + psetOrGroupId; + rec.link = "/lightning/setup/" + type + "/" + endLink; } result.push(rec); }); @@ -883,7 +938,7 @@ class AllDataBoxShortcut extends React.PureComponent { } async onDataSelect(shortcut) { - let { sfHost } = this.props; + let {sfHost} = this.props; window.open("https://" + sfHost + shortcut.link); } @@ -892,13 +947,13 @@ class AllDataBoxShortcut extends React.PureComponent { key: value.Id, value, element: [ - h("div", { className: "autocomplete-item-main", key: "main" }, + h("div", {className: "autocomplete-item-main", key: "main"}, h(MarkSubstring, { text: value.label, start: value.label.toLowerCase().indexOf(shortcutQuery.toLowerCase()), length: shortcutQuery.length })), - h("div", { className: "autocomplete-item-sub small", key: "sub" }, + h("div", {className: "autocomplete-item-sub small", key: "sub"}, h("div", {}, value.detail), h(MarkSubstring, { text: value.name, @@ -910,16 +965,16 @@ class AllDataBoxShortcut extends React.PureComponent { } render() { - let { selectedUser } = this.state; - let { sfHost, linkTarget, contextOrgId, contextUserId, contextPath } = this.props; + let {selectedUser} = this.state; + let {sfHost, linkTarget, contextOrgId, contextUserId, contextPath} = this.props; return ( - h("div", { ref: "shortcutsBox", className: "users-box" }, - h(AllDataSearch, { ref: "allDataSearch", getMatches: this.getMatches, onDataSelect: this.onDataSelect, inputSearchDelay: 200, placeholderText: "Quick find links, shortcuts", resultRender: this.resultRender }), - h("div", { className: "all-data-box-inner" + (!selectedUser ? " empty" : "") }, + h("div", {ref: "shortcutsBox", className: "users-box"}, + h(AllDataSearch, {ref: "allDataSearch", getMatches: this.getMatches, onDataSelect: this.onDataSelect, inputSearchDelay: 200, placeholderText: "Quick find links, shortcuts", resultRender: this.resultRender}), + h("div", {className: "all-data-box-inner" + (!selectedUser ? " empty" : "")}, selectedUser - ? h(UserDetails, { user: selectedUser, sfHost, contextOrgId, currentUserId: contextUserId, linkTarget, contextPath }) - : h("div", { className: "center" }, "No shortcut found") + ? h(UserDetails, {user: selectedUser, sfHost, contextOrgId, currentUserId: contextUserId, linkTarget, contextPath}) + : h("div", {className: "center"}, "No shortcut found") )) ); } @@ -927,7 +982,7 @@ class AllDataBoxShortcut extends React.PureComponent { class UserDetails extends React.PureComponent { doSupportLoginAs(user) { - let { currentUserId } = this.props; + let {currentUserId} = this.props; //Optimistically show login unless it's logged in user's userid or user is inactive. //No API to determine if user is allowed to login as given user. See https://salesforce.stackexchange.com/questions/224342/query-can-i-login-as-for-users if (!user || user.Id == currentUserId || !user.IsActive) { @@ -936,35 +991,45 @@ class UserDetails extends React.PureComponent { return true; } + canLoginAsPortal(user){ + return user.IsActive && user.NetworkId; + } + getLoginAsLink(userId) { - let { sfHost, contextOrgId, contextPath } = this.props; + let {sfHost, contextOrgId, contextPath} = this.props; const retUrl = contextPath || "/"; const targetUrl = contextPath || "/"; return "https://" + sfHost + "/servlet/servlet.su" + "?oid=" + encodeURIComponent(contextOrgId) + "&suorgadminid=" + encodeURIComponent(userId) + "&retURL=" + encodeURIComponent(retUrl) + "&targetURL=" + encodeURIComponent(targetUrl); } + getLoginAsPortalLink(user){ + let {sfHost, contextOrgId, contextPath} = this.props; + const retUrl = contextPath || "/"; + return "https://" + sfHost + "/servlet/servlet.su" + "?oid=" + encodeURIComponent(contextOrgId) + "&retURL=" + encodeURIComponent(retUrl) + "&sunetworkid=" + encodeURIComponent(user.NetworkId) + "&sunetworkuserid=" + encodeURIComponent(user.Id); + } + getUserDetailLink(userId) { - let { sfHost } = this.props; + let {sfHost} = this.props; return "https://" + sfHost + "/lightning/setup/ManageUsers/page?address=%2F" + userId + "%3Fnoredirect%3D1"; } getUserPsetLink(userId) { - let { sfHost } = this.props; + let {sfHost} = this.props; return "https://" + sfHost + "/lightning/setup/PermSets/page?address=%2Fudd%2FPermissionSet%2FassignPermissionSet.apexp%3FuserId%3D" + userId; } getUserPsetGroupLink(userId) { - let { sfHost } = this.props; + let {sfHost} = this.props; return "https://" + sfHost + "/lightning/setup/PermSetGroups/page?address=%2Fudd%2FPermissionSetGroup%2FassignPermissionSet.apexp%3FuserId%3D" + userId + "%26isPermsetGroup%3D1"; } getProfileLink(profileId) { - let { sfHost } = this.props; + let {sfHost} = this.props; return "https://" + sfHost + "/lightning/setup/EnhancedProfiles/page?address=%2F" + profileId; } getShowAllDataLink(userId) { - let { sfHost } = this.props; + let {sfHost} = this.props; let args = new URLSearchParams(); args.set("host", sfHost); args.set("objectType", "User"); @@ -973,60 +1038,64 @@ class UserDetails extends React.PureComponent { } render() { - let { user, linkTarget, sfHost } = this.props; + let {user, linkTarget, sfHost} = this.props; return ( - h("div", { className: "all-data-box-inner" }, - h("div", { className: "all-data-box-data slds-m-bottom_xx-small" }, - h("table", { className: (user.IsActive) ? "" : "inactive" }, + h("div", {className: "all-data-box-inner"}, + h("div", {className: "all-data-box-data slds-m-bottom_xx-small"}, + h("table", {className: (user.IsActive) ? "" : "inactive"}, h("tbody", {}, h("tr", {}, h("th", {}, "Name:"), h("td", {}, - (user.IsActive) ? "" : h("span", { title: "User is inactive" }, "⚠ "), + (user.IsActive) ? "" : h("span", {title: "User is inactive"}, "⚠ "), user.Name + " (" + user.Alias + ")" ) ), h("tr", {}, h("th", {}, "Username:"), - h("td", { className: "oneliner" }, user.Username) + h("td", {className: "oneliner"}, user.Username) ), h("tr", {}, h("th", {}, "Id:"), - h("td", { className: "oneliner" }, - h("a", { href: this.getShowAllDataLink(user.Id), target: linkTarget, title: "Show all data" }, user.Id)) + h("td", {className: "oneliner"}, + h("a", {href: this.getShowAllDataLink(user.Id), target: linkTarget, title: "Show all data"}, user.Id)) ), h("tr", {}, h("th", {}, "E-mail:"), - h("td", { className: "oneliner" }, user.Email) + h("td", {className: "oneliner"}, user.Email) ), h("tr", {}, h("th", {}, "Profile:"), - h("td", { className: "oneliner" }, + h("td", {className: "oneliner"}, (user.Profile) - ? h("a", { href: this.getProfileLink(user.ProfileId), target: linkTarget }, user.Profile.Name) - : h("em", { className: "inactive" }, "unknown") + ? h("a", {href: this.getProfileLink(user.ProfileId), target: linkTarget}, user.Profile.Name) + : h("em", {className: "inactive"}, "unknown") ) ), h("tr", {}, h("th", {}, "Role:"), - h("td", { className: "oneliner" }, (user.UserRole) ? user.UserRole.Name : "") + h("td", {className: "oneliner"}, (user.UserRole) ? user.UserRole.Name : "") ), h("tr", {}, h("th", {}, "Language:"), h("td", {}, - h("div", { className: "flag flag-" + sfLocaleKeyToCountryCode(user.LanguageLocaleKey), title: "Language: " + user.LanguageLocaleKey }), + h("div", {className: "flag flag-" + sfLocaleKeyToCountryCode(user.LanguageLocaleKey), title: "Language: " + user.LanguageLocaleKey}), " | ", - h("div", { className: "flag flag-" + sfLocaleKeyToCountryCode(user.LocaleSidKey), title: "Locale: " + user.LocaleSidKey }) + h("div", {className: "flag flag-" + sfLocaleKeyToCountryCode(user.LocaleSidKey), title: "Locale: " + user.LocaleSidKey}) ) ) ) )), - h("div", { ref: "userButtons", className: "center small-font" }, - this.doSupportLoginAs(user) ? h("a", { href: this.getLoginAsLink(user.Id), target: linkTarget, className: "slds-button slds-button_neutral" }, "Try login as") : null, - h("a", { href: this.getUserDetailLink(user.Id), target: linkTarget, className: "slds-button slds-button_neutral" }, "Details"), - h("a", { href: this.getUserPsetLink(user.Id), target: linkTarget, className: "slds-button slds-button_neutral", title: "Show / assign user's permission sets" }, "PSet"), - h("a", { href: this.getUserPsetGroupLink(user.Id), target: linkTarget, className: "slds-button slds-button_neutral", title: "Show / assign user's permission set groups" }, "PSetG") - )) + h("div", {ref: "userButtons", className: "center small-font"}, + h("a", {href: this.getUserDetailLink(user.Id), target: linkTarget, className: "slds-button slds-button_neutral"}, "Details"), + h("a", {href: this.getUserPsetLink(user.Id), target: linkTarget, className: "slds-button slds-button_neutral", title: "Show / assign user's permission sets"}, "PSet"), + h("a", {href: this.getUserPsetGroupLink(user.Id), target: linkTarget, className: "slds-button slds-button_neutral", title: "Show / assign user's permission set groups"}, "PSetG") + ), + h("div", {ref: "userButtons", className: "center small-font top-space"}, + this.doSupportLoginAs(user) ? h("a", {href: this.getLoginAsLink(user.Id), target: linkTarget, className: "slds-button slds-button_neutral"}, "Try login as") : null, + this.canLoginAsPortal(user) ? h("a", {href: this.getLoginAsPortalLink(user), target: linkTarget, className: "slds-button slds-button_neutral"}, "Login to Experience") : null, + ) + ) ); } } @@ -1042,33 +1111,33 @@ class ShowDetailsButton extends React.PureComponent { this.onDetailsClick = this.onDetailsClick.bind(this); } canShowDetails() { - let { showDetailsSupported, selectedValue, contextRecordId } = this.props; + let {showDetailsSupported, selectedValue, contextRecordId} = this.props; return showDetailsSupported && contextRecordId && selectedValue.sobject.keyPrefix == contextRecordId.substring(0, 3) && selectedValue.sobject.availableApis.length > 0; } onDetailsClick() { - let { sfHost, selectedValue } = this.props; - let { detailsShown } = this.state; + let {sfHost, selectedValue} = this.props; + let {detailsShown} = this.state; if (detailsShown || !this.canShowDetails()) { return; } let tooling = !selectedValue.sobject.availableApis.includes("regularApi"); let url = "/services/data/v" + apiVersion + "/" + (tooling ? "tooling/" : "") + "sobjects/" + selectedValue.sobject.name + "/describe/"; - this.setState({ detailsShown: true, detailsLoading: true }); + this.setState({detailsShown: true, detailsLoading: true}); Promise.all([ sfConn.rest(url), getAllFieldSetupLinks(sfHost, selectedValue.sobject.name) ]).then(([res, insextAllFieldSetupLinks]) => { - this.setState({ detailsShown: true, detailsLoading: false }); - parent.postMessage({ insextShowStdPageDetails: true, insextData: res, insextAllFieldSetupLinks }, "*"); + this.setState({detailsShown: true, detailsLoading: false}); + parent.postMessage({insextShowStdPageDetails: true, insextData: res, insextAllFieldSetupLinks}, "*"); closePopup(); }).catch(error => { - this.setState({ detailsShown: false, detailsLoading: false }); + this.setState({detailsShown: false, detailsLoading: false}); console.error(error); alert(error); }); } render() { - let { detailsLoading, detailsShown } = this.state; + let {detailsLoading, detailsShown} = this.state; return ( h("div", {}, h("a", @@ -1077,7 +1146,7 @@ class ShowDetailsButton extends React.PureComponent { className: "button" + (detailsLoading ? " loading" : "" + " page-button slds-button slds-button_neutral slds-m-bottom_xx-small"), disabled: detailsShown, onClick: this.onDetailsClick, - style: { display: !this.canShowDetails() ? "none" : "" } + style: {display: !this.canShowDetails() ? "none" : ""} }, h("span", {}, "Show field ", h("u", {}, "m"), "etadata") ) @@ -1095,7 +1164,7 @@ class AllDataSelection extends React.PureComponent { this.refs.showAllDataBtn.click(); } getAllDataUrl(toolingApi) { - let { sfHost, selectedValue } = this.props; + let {sfHost, selectedValue} = this.props; if (selectedValue) { let args = new URLSearchParams(); args.set("host", sfHost); @@ -1112,7 +1181,7 @@ class AllDataSelection extends React.PureComponent { } } getDeployStatusUrl() { - let { sfHost, selectedValue } = this.props; + let {sfHost, selectedValue} = this.props; let args = new URLSearchParams(); args.set("host", sfHost); args.set("checkDeployStatus", selectedValue.recordId); @@ -1165,7 +1234,7 @@ class AllDataSelection extends React.PureComponent { } } render() { - let { sfHost, showDetailsSupported, contextRecordId, selectedValue, linkTarget, recordIdDetails } = this.props; + let {sfHost, showDetailsSupported, contextRecordId, selectedValue, linkTarget, recordIdDetails} = this.props; // Show buttons for the available APIs. let buttons = Array.from(selectedValue.sobject.availableApis); buttons.sort(); @@ -1174,24 +1243,24 @@ class AllDataSelection extends React.PureComponent { buttons.push("noApi"); } return ( - h("div", { className: "all-data-box-inner" }, - h("div", { className: "all-data-box-data slds-m-bottom_xx-small" }, + h("div", {className: "all-data-box-inner"}, + h("div", {className: "all-data-box-data slds-m-bottom_xx-small"}, h("table", {}, h("tbody", {}, h("tr", {}, h("th", {}, "Name:"), h("td", {}, - h("a", { href: this.getObjectSetupLink(selectedValue.sobject.name, selectedValue.sobject.durableId, selectedValue.sobject.isCustomSetting), target: linkTarget }, selectedValue.sobject.name) + h("a", {href: this.getObjectSetupLink(selectedValue.sobject.name, selectedValue.sobject.durableId, selectedValue.sobject.isCustomSetting), target: linkTarget}, selectedValue.sobject.name) ) ), h("tr", {}, h("th", {}, "Links:"), h("td", {}, - h("a", { href: this.getObjectFieldsSetupLink(selectedValue.sobject.name, selectedValue.sobject.durableId, selectedValue.sobject.isCustomSetting), target: linkTarget }, "Fields"), + h("a", {href: this.getObjectFieldsSetupLink(selectedValue.sobject.name, selectedValue.sobject.durableId, selectedValue.sobject.isCustomSetting), target: linkTarget}, "Fields"), h("span", {}, " / "), - h("a", { href: this.getRecordTypesLink(sfHost, selectedValue.sobject.name, selectedValue.sobject.durableId), target: linkTarget }, "Record Types"), + h("a", {href: this.getRecordTypesLink(sfHost, selectedValue.sobject.name, selectedValue.sobject.durableId), target: linkTarget}, "Record Types"), h("span", {}, " / "), - h("a", { href: this.getObjectListLink(selectedValue.sobject.name, selectedValue.sobject.keyPrefix, selectedValue.sobject.isCustomSetting), target: linkTarget }, "Object List") + h("a", {href: this.getObjectListLink(selectedValue.sobject.name, selectedValue.sobject.keyPrefix, selectedValue.sobject.isCustomSetting), target: linkTarget}, "Object List") ), ), h("tr", {}, @@ -1207,11 +1276,11 @@ class AllDataSelection extends React.PureComponent { ))), - h(AllDataRecordDetails, { sfHost, selectedValue, recordIdDetails, className: "top-space" }), + h(AllDataRecordDetails, {sfHost, selectedValue, recordIdDetails, className: "top-space", linkTarget}), ), - h(ShowDetailsButton, { ref: "showDetailsBtn", sfHost, showDetailsSupported, selectedValue, contextRecordId }), + h(ShowDetailsButton, {ref: "showDetailsBtn", sfHost, showDetailsSupported, selectedValue, contextRecordId}), selectedValue.recordId && selectedValue.recordId.startsWith("0Af") - ? h("a", { href: this.getDeployStatusUrl(), target: linkTarget, className: "button page-button slds-button slds-button_neutral slds-m-bottom_xx-small" }, "Check Deploy Status") : null, + ? h("a", {href: this.getDeployStatusUrl(), target: linkTarget, className: "button page-button slds-button slds-button_neutral slds-m-bottom_xx-small"}, "Check Deploy Status") : null, buttons.map((button, index) => h("div", {}, h("a", { key: button, @@ -1223,8 +1292,8 @@ class AllDataSelection extends React.PureComponent { }, index == 0 ? h("span", {}, "Show ", h("u", {}, "a"), "ll data") : "Show all data", button == "regularApi" ? "" - : button == "toolingApi" ? " (Tooling API)" - : " (Not readable)" + : button == "toolingApi" ? " (Tooling API)" + : " (Not readable)" ))) ) ); @@ -1233,19 +1302,28 @@ class AllDataSelection extends React.PureComponent { class AllDataRecordDetails extends React.PureComponent { + getRecordLink(sfHost, recordId) { + return "https://" + sfHost + "/" + recordId; + } getRecordTypeLink(sfHost, sobjectName, recordtypeId) { return "https://" + sfHost + "/lightning/setup/ObjectManager/" + sobjectName + "/RecordTypes/" + recordtypeId + "/view"; } render() { - let { sfHost, recordIdDetails, className, selectedValue } = this.props; + let {sfHost, recordIdDetails, className, selectedValue, linkTarget} = this.props; if (recordIdDetails) { return ( - h("table", { className }, + h("table", {className}, h("tbody", {}, + h("tr", {}, + h("th", {}, "Name:"), + h("td", {}, + h("a", {href: this.getRecordLink(sfHost, selectedValue.recordId), target: linkTarget}, recordIdDetails.recordName) + ) + ), h("tr", {}, h("th", {}, "RecType:"), h("td", {}, - h("a", { href: this.getRecordTypeLink(sfHost, selectedValue.sobject.name, recordIdDetails.recordTypeId), target: "" }, recordIdDetails.recordTypeName) + h("a", {href: this.getRecordTypeLink(sfHost, selectedValue.sobject.name, recordIdDetails.recordTypeId), target: linkTarget}, recordIdDetails.recordTypeName) ) ), h("tr", {}, @@ -1279,14 +1357,14 @@ class AllDataSearch extends React.PureComponent { this.onAllDataArrowClick = this.onAllDataArrowClick.bind(this); } componentDidMount() { - let { queryString } = this.state; + let {queryString} = this.state; this.getMatchesDelayed(queryString); } onAllDataInput(e) { let val = e.target.value; this.refs.autoComplete.handleInput(); this.getMatchesDelayed(val); - this.setState({ queryString: val }); + this.setState({queryString: val}); } onAllDataFocus() { this.refs.autoComplete.handleFocus(); @@ -1300,32 +1378,32 @@ class AllDataSearch extends React.PureComponent { } updateAllDataInput(value) { this.props.onDataSelect(value); - this.setState({ queryString: "" }); + this.setState({queryString: ""}); this.getMatchesDelayed(""); } onAllDataArrowClick() { this.refs.showAllDataInp.focus(); } getMatchesDelayed(userQuery) { - let { queryDelayTimer } = this.state; - let { inputSearchDelay } = this.props; + let {queryDelayTimer} = this.state; + let {inputSearchDelay} = this.props; if (queryDelayTimer) { clearTimeout(queryDelayTimer); } queryDelayTimer = setTimeout(async () => { - let { getMatches } = this.props; + let {getMatches} = this.props; const matchingResults = await getMatches(userQuery); - await this.setState({ matchingResults }); + await this.setState({matchingResults}); }, inputSearchDelay); - this.setState({ queryDelayTimer }); + this.setState({queryDelayTimer}); } render() { - let { queryString, matchingResults } = this.state; - let { placeholderText, resultRender } = this.props; + let {queryString, matchingResults} = this.state; + let {placeholderText, resultRender} = this.props; return ( - h("div", { className: "input-with-dropdown" }, + h("div", {className: "input-with-dropdown"}, h("input", { className: "all-data-input", ref: "showAllDataInp", @@ -1341,15 +1419,15 @@ class AllDataSearch extends React.PureComponent { updateInput: this.updateAllDataInput, matchingResults: resultRender(matchingResults, queryString) }), - h("svg", { viewBox: "0 0 24 24", onClick: this.onAllDataArrowClick }, - h("path", { d: "M3.8 6.5h16.4c.4 0 .8.6.4 1l-8 9.8c-.3.3-.9.3-1.2 0l-8-9.8c-.4-.4-.1-1 .4-1z" }) + h("svg", {viewBox: "0 0 24 24", onClick: this.onAllDataArrowClick}, + h("path", {d: "M3.8 6.5h16.4c.4 0 .8.6.4 1l-8 9.8c-.3.3-.9.3-1.2 0l-8-9.8c-.4-.4-.1-1 .4-1z"}) ) ) ); } } -function MarkSubstring({ text, start, length }) { +function MarkSubstring({text, start, length}) { if (start == -1) { return h("span", {}, text); } @@ -1378,33 +1456,33 @@ class Autocomplete extends React.PureComponent { this.onScroll = this.onScroll.bind(this); } handleInput() { - this.setState({ showResults: true, selectedIndex: 0, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1 }); + this.setState({showResults: true, selectedIndex: 0, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1}); } handleFocus() { - this.setState({ showResults: true, selectedIndex: 0, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1 }); + this.setState({showResults: true, selectedIndex: 0, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1}); } handleBlur() { - this.setState({ showResults: false }); + this.setState({showResults: false}); } handleKeyDown(e) { - let { matchingResults } = this.props; - let { selectedIndex, showResults, scrollToSelectedIndex } = this.state; + let {matchingResults} = this.props; + let {selectedIndex, showResults, scrollToSelectedIndex} = this.state; if (e.key == "Enter") { if (!showResults) { - this.setState({ showResults: true, selectedIndex: 0, scrollToSelectedIndex: scrollToSelectedIndex + 1 }); + this.setState({showResults: true, selectedIndex: 0, scrollToSelectedIndex: scrollToSelectedIndex + 1}); return; } if (selectedIndex < matchingResults.length) { e.preventDefault(); - let { value } = matchingResults[selectedIndex]; + let {value} = matchingResults[selectedIndex]; this.props.updateInput(value); - this.setState({ showResults: false, selectedIndex: 0 }); + this.setState({showResults: false, selectedIndex: 0}); } return; } if (e.key == "Escape") { e.preventDefault(); - this.setState({ showResults: false, selectedIndex: 0 }); + this.setState({showResults: false, selectedIndex: 0}); return; } let selectionMove = 0; @@ -1417,7 +1495,7 @@ class Autocomplete extends React.PureComponent { if (selectionMove != 0) { e.preventDefault(); if (!showResults) { - this.setState({ showResults: true, selectedIndex: 0, scrollToSelectedIndex: scrollToSelectedIndex + 1 }); + this.setState({showResults: true, selectedIndex: 0, scrollToSelectedIndex: scrollToSelectedIndex + 1}); return; } let index = selectedIndex + selectionMove; @@ -1428,26 +1506,26 @@ class Autocomplete extends React.PureComponent { if (index > length - 1) { index = 0; } - this.setState({ selectedIndex: index, scrollToSelectedIndex: scrollToSelectedIndex + 1 }); + this.setState({selectedIndex: index, scrollToSelectedIndex: scrollToSelectedIndex + 1}); } } onResultsMouseDown() { - this.setState({ resultsMouseIsDown: true }); + this.setState({resultsMouseIsDown: true}); } onResultsMouseUp() { - this.setState({ resultsMouseIsDown: false }); + this.setState({resultsMouseIsDown: false}); } onResultClick(value) { this.props.updateInput(value); - this.setState({ showResults: false, selectedIndex: 0 }); + this.setState({showResults: false, selectedIndex: 0}); } onResultMouseEnter(index) { - this.setState({ selectedIndex: index, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1 }); + this.setState({selectedIndex: index, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1}); } onScroll() { let scrollTopIndex = Math.floor(this.refs.scrollBox.scrollTop / this.state.itemHeight); if (scrollTopIndex != this.state.scrollTopIndex) { - this.setState({ scrollTopIndex }); + this.setState({scrollTopIndex}); } } componentDidUpdate(prevProps, prevState) { @@ -1456,7 +1534,7 @@ class Autocomplete extends React.PureComponent { if (anItem) { let itemHeight = anItem.offsetHeight; if (itemHeight > 0) { - this.setState({ itemHeight }); + this.setState({itemHeight}); } } return; @@ -1472,7 +1550,7 @@ class Autocomplete extends React.PureComponent { } } render() { - let { matchingResults } = this.props; + let {matchingResults} = this.props; let { showResults, selectedIndex, @@ -1490,12 +1568,12 @@ class Autocomplete extends React.PureComponent { let bottomSpace = (lastIndex - lastRenderedIndex) * itemHeight; let topSelected = (selectedIndex - firstIndex) * itemHeight; return ( - h("div", { className: "autocomplete-container", style: { display: (showResults && matchingResults.length > 0) || resultsMouseIsDown ? "" : "none" }, onMouseDown: this.onResultsMouseDown, onMouseUp: this.onResultsMouseUp }, - h("div", { className: "autocomplete", onScroll: this.onScroll, ref: "scrollBox" }, - h("div", { ref: "selectedItem", style: { position: "absolute", top: topSelected + "px", height: itemHeight + "px" } }), - h("div", { style: { height: topSpace + "px" } }), + h("div", {className: "autocomplete-container", style: {display: (showResults && matchingResults.length > 0) || resultsMouseIsDown ? "" : "none"}, onMouseDown: this.onResultsMouseDown, onMouseUp: this.onResultsMouseUp}, + h("div", {className: "autocomplete", onScroll: this.onScroll, ref: "scrollBox"}, + h("div", {ref: "selectedItem", style: {position: "absolute", top: topSelected + "px", height: itemHeight + "px"}}), + h("div", {style: {height: topSpace + "px"}}), matchingResults.slice(firstRenderedIndex, lastRenderedIndex + 1) - .map(({ key, value, element }, index) => + .map(({key, value, element}, index) => h("a", { key, className: "autocomplete-item " + (selectedIndex == index + firstRenderedIndex ? "selected" : ""), @@ -1503,7 +1581,7 @@ class Autocomplete extends React.PureComponent { onMouseEnter: () => this.onResultMouseEnter(index + firstRenderedIndex) }, element) ), - h("div", { style: { height: bottomSpace + "px" } }) + h("div", {style: {height: bottomSpace + "px"}}) ) ) ); @@ -1515,7 +1593,7 @@ function getRecordId(href) { // Find record ID from URL let searchParams = new URLSearchParams(url.search.substring(1)); // Salesforce Classic and Console - if (url.hostname.endsWith(".salesforce.com")) { + if (url.hostname.endsWith(".salesforce.com") || url.hostname.endsWith(".salesforce.mil")) { let match = url.pathname.match(/\/([a-zA-Z0-9]{3}|[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})(?:\/|$)/); if (match) { let res = match[1]; @@ -1526,7 +1604,7 @@ function getRecordId(href) { } // Lightning Experience and Salesforce1 - if (url.hostname.endsWith(".lightning.force.com")) { + if (url.hostname.endsWith(".lightning.force.com") || url.hostname.endsWith(".lightning.force.mil") || url.hostname.endsWith(".lightning.crmforce.mil")) { let match; if (url.pathname == "/one/one.app") { @@ -1555,6 +1633,16 @@ function getRecordId(href) { return null; } +function getSobject(href) { + let url = new URL(href); + if (url.pathname && url.pathname.endsWith("/list")){ + let sobject = url.pathname.substring(0, url.pathname.lastIndexOf("/list")); + sobject = sobject.substring(sobject.lastIndexOf("/") + 1); + return sobject; + } + return null; +} + function getSfPathFromUrl(href) { let url = new URL(href); if (url.protocol.endsWith("-extension:")) { @@ -1570,4 +1658,12 @@ function sfLocaleKeyToCountryCode(localeKey) { return splitted[(splitted.length > 1 && !localeKey.includes("_LATN_")) ? 1 : 0].toLowerCase(); } +function getLinkTarget(e) { + if (localStorage.getItem("openLinksInNewTab") == "true" || (e.ctrlKey || e.metaKey)){ + return "_blank"; + } else { + return "_top"; + } +} + window.getRecordId = getRecordId; // for unit tests diff --git a/addon/setup-links.js b/addon/setup-links.js index f80c95bf..ef1bac29 100644 --- a/addon/setup-links.js +++ b/addon/setup-links.js @@ -12,21 +12,24 @@ export async function getObjectSetupLinks(sfHost, sobjectName) { }; } -function getFieldDefinitionSetupLinks(sfHost, fieldName, fieldDefinition) { +function getFieldDefinitionSetupLinks(sfHost, fieldName, fieldDefinition, isCustomSetting, isCustomMetadata) { let durableId = fieldDefinition.DurableId.split("."); let entityDurableId = durableId[0]; let fieldDurableId = durableId[durableId.length - 1]; + let customType = isCustomMetadata ? "CustomMetadata" : isCustomSetting ? "CustomSettings" : ""; + let lightSetupLink = isCustomMetadata ? `https://${sfHost}/lightning/setup/${customType}/page?address=%2F${fieldDurableId}%3Fsetupid%3D${customType}` : `https://${sfHost}/lightning/setup/ObjectManager/${entityDurableId}/FieldsAndRelationships/${fieldDurableId}/view`; return { - lightningSetupLink: `https://${sfHost}/lightning/setup/ObjectManager/${entityDurableId}/FieldsAndRelationships/${fieldDurableId}/view`, + lightningSetupLink: lightSetupLink, classicSetupLink: fieldName.includes("__") ? `https://${sfHost}/${fieldDurableId}` : `https://${sfHost}/p/setup/field/StandardFieldAttributes/d?id=${fieldDurableId}&type=${entityDurableId}` }; } -export async function getFieldSetupLinks(sfHost, sobjectName, fieldName) { +export async function getFieldSetupLinks(sfHost, sobjectName, fieldName, isCustomSetting) { let {records: fieldDefinitions} = await sfConn.rest(`/services/data/v${apiVersion}/tooling/query/?q=${encodeURIComponent(`select DurableId from FieldDefinition where EntityDefinition.QualifiedApiName = '${sobjectName}' and QualifiedApiName = '${fieldName}'`)}`); - return getFieldDefinitionSetupLinks(sfHost, fieldName, fieldDefinitions[0]); + let isCmdt = sobjectName.endsWith("__mdt"); + return getFieldDefinitionSetupLinks(sfHost, fieldName, fieldDefinitions[0], isCustomSetting, isCmdt); } export async function getAllFieldSetupLinks(sfHost, sobjectName) { @@ -34,7 +37,7 @@ export async function getAllFieldSetupLinks(sfHost, sobjectName) { let fields = new Map(); for (let fieldDefinition of fieldDefinitions) { let fieldName = fieldDefinition.QualifiedApiName; - fields.set(fieldName, getFieldDefinitionSetupLinks(sfHost, fieldName, fieldDefinition)); + fields.set(fieldName, getFieldDefinitionSetupLinks(sfHost, fieldName, fieldDefinition, false, false)); } return fields; } diff --git a/addon/test-framework.js b/addon/test-framework.js index 575d888b..974cb2c6 100644 --- a/addon/test-framework.js +++ b/addon/test-framework.js @@ -70,13 +70,12 @@ class Test { } } - loadPage(url) { + loadPage(url, args = new URLSearchParams()) { return new Promise(resolve => { window.insextTestLoaded = testData => { window.insextTestLoaded = null; resolve(testData); }; - let args = new URLSearchParams(); args.set("host", this.sfHost); window.page.src = url + "?" + args; }); diff --git a/docs/assets/images/how-to/csv-separator.png b/docs/assets/images/how-to/csv-separator.png new file mode 100644 index 00000000..8aea7e3f Binary files /dev/null and b/docs/assets/images/how-to/csv-separator.png differ diff --git a/docs/how-to.md b/docs/how-to.md index 78a5c548..0218aeb0 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -26,7 +26,7 @@ To secure the extension usage, you can use a auth flow to get an access token li 1. Open data export page on legacy extension Inspect legacy -2. Get saved queries from "insextSavedQueryHistory" property +2. Get saved queries from `insextSavedQueryHistory` property Inspect legacy 3. Open it in VS Code, you should have a JSON like this one: @@ -55,8 +55,94 @@ To secure the extension usage, you can use a auth flow to get an access token li ] ``` -Re-import this json in the new extension (with the same key "insextSavedQueryHistory") +Re-import this json in the new extension (with the same key `insextSavedQueryHistory`) -## Access Recycle Bin from Objects tab -![2023-09-19_16-59-10 (1)](https://github.com/tprouvot/Salesforce-Inspector-reloaded/assets/35368290/bfab8827-c78f-40ac-b26f-4bb27cf20425) +## Define a CSV separator +Add a new property `csvSeparator` containing the needed separator for CSV files + + Update csv separator + +## Disable query input autofocus + +Add a new property `disableQueryInputAutoFocus` with `true` + +![image](https://github.com/tprouvot/Salesforce-Inspector-reloaded/assets/35368290/89563a58-d8fa-4b14-a150-99c389e8df75) + +## Add custom query templates + +Add a new property `queryTemplates` with your custom queries separated by "//" character. +Example: + +`SELECT Id FROM// SELECT Id FROM WHERE//SELECT Id FROM WHERE IN//SELECT Id FROM WHERE LIKE//SELECT Id FROM ORDER BY//SELECT ID FROM MYTEST__c//SELECT ID WHERE` + +## Open links in a new tab + +If you want to _always_ open extension's links in a new tab, you can set the `openLinksInNewTab` property to `true` + +![image](https://github.com/tprouvot/Salesforce-Inspector-reloaded/assets/35368290/e6ae08a9-1ee9-4809-a820-1377aebcd547) + +If you want to open popup keyboard shortcuts, you can use the 'ctrl' (windows) or 'command' (mac) key with the corresponding key. +Example: + +- Data Export : e +- Data Import : i +- Org Limits : l +- Download Metadata : d +- Explore API : x + +## Disable metadata search from Shortcut tab + +By default when you enter keyword in the Shortcut tab, the search is performed on the Setup link shortcuts _AND_ metadata (Flows, PermissionSets and Profiles). +If you want to disable the search on the metadata, set `metadataShortcutSearch` to `false` + +![image](https://github.com/tprouvot/Salesforce-Inspector-reloaded/assets/35368290/a31566d8-0ad4-47e5-a1ab-3eada43b3430) + +## Enable / Disable Flow scrollability + +Go on a Salesforce flow and check / uncheck the checbox to update navigation scrollability on the Flow Builder + +![2023-09-29_16-01-14 (1)](https://github.com/tprouvot/Salesforce-Inspector-reloaded/assets/35368290/91845a31-8f53-4ea1-b895-4cb036d1bed0) + +## Add custom links to "Shortcut" tab + +Because one of the main use case for custom links is to refer to a record in your org, those links are stored under a property prefixed by the org host url. +You can find the value by checking the property `_isSandbox` + +![image](https://github.com/tprouvot/Salesforce-Inspector-reloaded/assets/35368290/319585eb-03a3-4c16-948f-fa721214ba14) + +Then copy the url and add `_orgLinks` for the property name. +Now you can enter the custom links following this convention: + +```json +[ + { + "label": "Test myLink", + "link": "/lightning/setup/SetupOneHome/home", + "section": "Custom", + "prod": false + }, + { + "label": "EnhancedProfiles", + "section": "Custom", + "link": "/lightning/setup/EnhancedProfiles/home", + "prod": false + } +] +``` + +ET VOILA ! + +image + +## Enable summary view of PermissionSet / PermissionSetGroups from shortcut tab + +Since Winter 24, there is a beta functionality to view a summary of the PermissionSet / PermissionSetGroups + +image + +You can enable this view for the Shortcut search by creating a new localVariable as shown below. + +![image](https://github.com/tprouvot/Salesforce-Inspector-reloaded/assets/35368290/f3093e4b-438c-4795-b64a-8d37651906a5) + +Then when you click on a PermissionSet / PermissionSetGroups search result, you'll be redirected to the summary. diff --git a/mkdocs.yml b/mkdocs.yml index 9e5331ed..6fb8e914 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,11 @@ site_name: Salesforce Inspector reloaded +site_url: https://tprouvot.github.io/Salesforce-Inspector-reloaded/ +repo_url: https://github.com/tprouvot/Salesforce-Inspector-reloaded +site_author: Thomas Prouvot theme: name: material - logo: ./assets/images/icon128.png - favicon: ./assets/images/icon128.png + logo: assets/images/icon128.png + favicon: assets/images/icon128.png features: - navigation.tabs - navigation.sections diff --git a/package-lock.json b/package-lock.json index ed3bff49..73a06b01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1532,9 +1532,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" diff --git a/sfdx-project.json b/sfdx-project.json index a6ae1cfc..5dd1c75d 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -7,5 +7,5 @@ ], "namespace": "", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "58.0" -} \ No newline at end of file + "sourceApiVersion": "59.0" +} diff --git a/test/main/default/package.xml b/test/main/default/package.xml index cef45927..346b7499 100644 --- a/test/main/default/package.xml +++ b/test/main/default/package.xml @@ -8,5 +8,9 @@ SalesforceInspectorTest ApexClass - 47.0 + + Account + Settings + + 58.0 \ No newline at end of file diff --git a/test/main/default/settings/Account.settings-meta.xml b/test/main/default/settings/Account.settings-meta.xml new file mode 100644 index 00000000..3909c82f --- /dev/null +++ b/test/main/default/settings/Account.settings-meta.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file