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 @@
-