From b7cd520d76d4bd1aa7f47b03584a15fae62c4042 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 12 Jul 2023 14:34:34 +0200 Subject: [PATCH 01/54] Auto detect SObject when visualizing list views page --- addon/popup.js | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/addon/popup.js b/addon/popup.js index 6f9d7dd5..13325aa4 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -263,6 +263,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); @@ -298,9 +299,11 @@ class AllDataBox extends React.PureComponent { if (contextUrl) { let recordId = getRecordId(contextUrl); let path = getSfPathFromUrl(contextUrl); + let sobject = getSobject(contextUrl); this.setState({ contextRecordId: recordId, - contextPath: path + contextPath: path, + contextSobject: sobject }); } } @@ -427,7 +430,7 @@ class AllDataBox extends React.PureComponent { } render() { - let { activeSearchAspect, sobjectsLoading, contextRecordId, contextUserId, contextOrgId, contextPath, sobjectsList } = this.state; + let { activeSearchAspect, sobjectsLoading, contextRecordId, contextSobject, contextUserId, contextOrgId, contextPath, sobjectsList } = this.state; let { sfHost, showDetailsSupported, linkTarget } = this.props; return ( @@ -439,7 +442,7 @@ class AllDataBox extends React.PureComponent { ), (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") : "AllData aspect " + activeSearchAspect + " not implemented" @@ -606,22 +609,30 @@ 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); + async updateSelection(query, contextSobject) { + let match; + if (query === "list"){ + match = this.getBestMatch(contextSobject); + } else { + match = this.getBestMatch(query); + } + console.log(query); + console.log(match); + await this.setState({ selectedValue: match }); this.loadRecordIdDetails(); } @@ -1555,6 +1566,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:")) { From 063b3d8ef95598933fcb9a5d30ae3e08959d110a Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 12 Jul 2023 14:37:39 +0200 Subject: [PATCH 02/54] Update changes.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 5c74f986..a7627593 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ Version 1.19 General ------- +* Detect SObject on listview page [feature 121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/121) (idea by [Mehdi Cherfaoui](https://github.com/mehdisfdc)) * Inspect Page Restyling [PR105](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/105) (contribution by [Pietro Martino](https://github.com/pietromartino)) * Navigate to record detail (Flows, Profiles and PermissionSet) from shortcut search [feature 118](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/118) * Fix country codes from LocalSidKey convention [PR117](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/117) (contribution by [Luca Bassani](https://github.com/baslu93)) From 9c7520ae665302ad13ac1f221b45b3d52c3b0f43 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 12 Jul 2023 15:43:47 +0200 Subject: [PATCH 03/54] Remove console.log --- addon/popup.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/addon/popup.js b/addon/popup.js index 13325aa4..e8cdca67 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -630,8 +630,6 @@ class AllDataBoxSObject extends React.PureComponent { } else { match = this.getBestMatch(query); } - console.log(query); - console.log(match); await this.setState({ selectedValue: match }); this.loadRecordIdDetails(); From c189fdbed38d18c29f5cfaca73037bf9e898f22b Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Tue, 18 Jul 2023 11:17:28 +0200 Subject: [PATCH 04/54] Initiate new version --- CHANGES.md | 675 +++++++++++++++++------------------ addon/manifest-template.json | 23 +- addon/manifest.json | 22 +- 3 files changed, 339 insertions(+), 381 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2d8f89e0..680d8227 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,383 +1,362 @@ -Version 1.19 -=========== - -General -------- - -* 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)) -* Navigate to record detail (Flows, Profiles and PermissionSet) from shortcut search [feature 118](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/118) -* Fix country codes from LocalSidKey convention [PR117](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/117) (contribution by [Luca Bassani](https://github.com/baslu93)) -* Use custom shortcuts [feature 115](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/115) -* Add Export Query button [feature 109](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/109) (idea by [Ryan Sherry](https://github.com/rpsherry-starburst)) -* Add permission set group assignment button from popup [feature 106](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/106) - -Version 1.18 -=========== - -General -------- -* Update to Salesforce API v 58.0 (Summer '23) -* Restyle popup with SLDS (Salesforce Lightning Design System) [feature 9](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/9) (idea by [Loïc BERBEY](https://github.com/lberbey), contribution by [Pietro Martino](https://github.com/pietromartino)) -* Fix "Show all data" shortcut from popup [issue 96](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/96) (fix by [Pietro Martino](https://github.com/pietromartino)) - -Version 1.17 -=========== - -General -------- -* Add toLabel function among autocomplete query suggestions [feature 90](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/90) (idea by [Mickael Gudin](https://github.com/mickaelgudin)) -* Update spinner on inspect page when loading or saving records and disable button [feature 69](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/69) (idea by [Camille Guillory](https://github.com/CamilleGuillory)) -* Show "Copy Id" from Inspect page [feature 12](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/12) -* Add a configuration option for links to open in a new tab [feature 78](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/78) (idea by [Henri Vilminko](https://github.com/hvilminko)) -* Import data as JSON [feature 75](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/75) (idea by [gaelguimini](https://github.com/gaelguimini)) -* Fix auto update action on data import [issue 73](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/73) (issue by [Juul1](https://github.com/Juul1)) -* Restore focus on suggested fields when pressing tab key in query editor [issue 66](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/66) (idea by [Enrique Muñoz](https://github.com/emunoz-at-wiris)) -* Update shortcut indication for mac users -* Fix links for custom object [PR80](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/80) (contribution by [Mouloud Habchi](https://github.com/MD931)) -* Fix links for custom setting [PR82](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/82) (contribution by [Mouloud Habchi](https://github.com/MD931)) - -Version 1.16 -=========== - -General -------- -* Select "Update" action by default when the data paste in data-import page contains Id column [feature 60](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/60) (idea by Bilel Morsli) -* Allow users to update API Version [feature 58](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/58) -* Add org instance in the popup and a link to Salesforce trust status website [feature 53](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/53) (idea by [Camille Guillory](https://github.com/CamilleGuillory) ) -* Fix saved query when it contains ":" [issue 55](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/55) (bug found by [Victor Garcia](https://github.com/victorgz/) ) - -Version 1.15 -=========== - -General -------- -* Add "PSet" button to access user permission set assignment from User tab [feature 49](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/49) -* Add shortcut tab to access setup quick links [feature 42](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/42) - -Version 1.14 -=========== - -General -------- -* Add checkbox in flow builder to give the possibility to the user to scroll on the flow (by [Samuel Krissi](https://github.com/samuelkrissi) ) +# Version 1.20 + +## General + +# Version 1.19 + +## General + +- 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)) +- Navigate to record detail (Flows, Profiles and PermissionSet) from shortcut search [feature 118](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/118) +- Fix country codes from LocalSidKey convention [PR117](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/117) (contribution by [Luca Bassani](https://github.com/baslu93)) +- Use custom shortcuts [feature 115](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/115) +- Add Export Query button [feature 109](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/109) (idea by [Ryan Sherry](https://github.com/rpsherry-starburst)) +- Add permission set group assignment button from popup [feature 106](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/106) + +# Version 1.18 + +## General + +- Update to Salesforce API v 58.0 (Summer '23) +- Restyle popup with SLDS (Salesforce Lightning Design System) [feature 9](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/9) (idea by [Loïc BERBEY](https://github.com/lberbey), contribution by [Pietro Martino](https://github.com/pietromartino)) +- Fix "Show all data" shortcut from popup [issue 96](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/96) (fix by [Pietro Martino](https://github.com/pietromartino)) + +# Version 1.17 + +## General + +- Add toLabel function among autocomplete query suggestions [feature 90](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/90) (idea by [Mickael Gudin](https://github.com/mickaelgudin)) +- Update spinner on inspect page when loading or saving records and disable button [feature 69](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/69) (idea by [Camille Guillory](https://github.com/CamilleGuillory)) +- Show "Copy Id" from Inspect page [feature 12](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/12) +- Add a configuration option for links to open in a new tab [feature 78](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/78) (idea by [Henri Vilminko](https://github.com/hvilminko)) +- Import data as JSON [feature 75](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/75) (idea by [gaelguimini](https://github.com/gaelguimini)) +- Fix auto update action on data import [issue 73](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/73) (issue by [Juul1](https://github.com/Juul1)) +- Restore focus on suggested fields when pressing tab key in query editor [issue 66](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/66) (idea by [Enrique Muñoz](https://github.com/emunoz-at-wiris)) +- Update shortcut indication for mac users +- Fix links for custom object [PR80](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/80) (contribution by [Mouloud Habchi](https://github.com/MD931)) +- Fix links for custom setting [PR82](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/82) (contribution by [Mouloud Habchi](https://github.com/MD931)) + +# Version 1.16 + +## General + +- Select "Update" action by default when the data paste in data-import page contains Id column [feature 60](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/60) (idea by Bilel Morsli) +- Allow users to update API Version [feature 58](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/58) +- Add org instance in the popup and a link to Salesforce trust status website [feature 53](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/53) (idea by [Camille Guillory](https://github.com/CamilleGuillory) ) +- Fix saved query when it contains ":" [issue 55](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/55) (bug found by [Victor Garcia](https://github.com/victorgz/) ) + +# Version 1.15 + +## General + +- Add "PSet" button to access user permission set assignment from User tab [feature 49](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/49) +- Add shortcut tab to access setup quick links [feature 42](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/42) + +# Version 1.14 + +## General + +- 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) -* Fix links (object fields and object list) for custom metadata objects [issue 39](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/39) -* Add shortcut link to object list from popup (idea by [Samuel Krissi](https://github.com/samuelkrissi) ) -* Add shortcuts links to (list of record types, current SObject RecordType and objet details, show all data from user tab) from popup [feature 34](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/34) -* Update manifest version from [v2](https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/) to v3 -* Auto detect SObject on import page when posting data which contain SObject header [feature 30](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/30) -* Update to Salesforce API v 57.0 (Spring '23) -* [Switch background color on import page to alert users that it's a production environnement](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/20) -* Implement Auth2 flow to generate access token for connected App - -Version 1.13 -=========== - -General -------- -* [Automatically remove spaces from column name in import](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/23) -* Update to Salesforce API v 56.0 (Winter '23) -* Add "Skip all unknown fields" to import page -* Add User Id to pop-up +- Fix links (object fields and object list) for custom metadata objects [issue 39](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/39) +- Add shortcut link to object list from popup (idea by [Samuel Krissi](https://github.com/samuelkrissi) ) +- Add shortcuts links to (list of record types, current SObject RecordType and objet details, show all data from user tab) from popup [feature 34](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/34) +- Update manifest version from [v2](https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/) to v3 +- Auto detect SObject on import page when posting data which contain SObject header [feature 30](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/30) +- Update to Salesforce API v 57.0 (Spring '23) +- [Switch background color on import page to alert users that it's a production environnement](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/20) +- Implement Auth2 flow to generate access token for connected App + +# Version 1.13 + +## General + +- [Automatically remove spaces from column name in import](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/23) +- Update to Salesforce API v 56.0 (Winter '23) +- Add "Skip all unknown fields" to import page +- Add User Id to pop-up Inspector menu -* Support Enhanced Domain [issue #222](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector/issues/222) from [PR223](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector/pull/223) -* [Add inactive users to search result](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/21) +- Support Enhanced Domain [issue #222](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector/issues/222) from [PR223](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector/pull/223) +- [Add inactive users to search result](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/21) Inspector menu -* Update to Salesforce API v 55.0 (Summer '22) -* Update to Salesforce API v 54.0 (Spring '22) -* [Sticked table header to the top on export](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/10) -* Update to Salesforce API v 53.0 (Winter '22) -* Add label to saved query and sort list. -* Remove extra comma when autocomplete query in data export, or select a field from suggested fields juste before 'FROM' keyword. +- Update to Salesforce API v 55.0 (Summer '22) +- Update to Salesforce API v 54.0 (Spring '22) +- [Sticked table header to the top on export](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/10) +- Update to Salesforce API v 53.0 (Winter '22) +- Add label to saved query and sort list. +- Remove extra comma when autocomplete query in data export, or select a field from suggested fields juste before 'FROM' keyword. Inspector menu -* Add "Copy Id" option when clicking on a Sobject field or Id in data export page. +- Add "Copy Id" option when clicking on a Sobject field or Id in data export page. Inspector menu -* Integrate UI updates from [Garywoo's fork](https://github.com/Garywoo/Chrome-Salesforce-inspector) +- Integrate UI updates from [Garywoo's fork](https://github.com/Garywoo/Chrome-Salesforce-inspector) + +# Version 1.12 + +## General + +- Update to Salesforce API v 51.0 (Spring '21) + +# Version 1.11 + +## General + +- Make inspector available on Visualforce pages on new visualforce.com domain. See #143 + +## Org Limits + +- Displays "consumed" count + +# Version 1.10 + +## General + +- Update to Salesforce API v 48.0 + +# Version 1.9 + +## Inspector menu + +- Fix a bug fix hiding the "show field metadata" button (#150) + +# Version 1.8 + +## Inspector menu + +- Added user search aspect to simplify access to detailed user data and "login as". + +# Version 1.7 + +## General + +- Update to Salesforce API v 47.0 + +## Inspector menu + +- A new link to switch in and out of Salesforce Setup, where you can choose to open in a new tab or not. + +## Show all data + +- Fixed a bug causing errors when viewing some special objects. +- Link to Salesforce Setup in both Classic and Lightning Experience. +- Use default values for blank fields when creating a new record. This avoids the error message that OwnerId is required but missing. + +## Data import + +- Save import options in your excel sheet, so you can update the same data again and again with a single copy-paste. + +# Version 1.6 + +## General + +- Update to Salesforce API v 45.0 +- Support for cloudforce.com orgs + +## Show all data + +- Buttons to Create, delete and clone records + +## Data export + +- Keyboard shortcut to do export (ctrl+enter) +- Fixes saved query selection + +## Data import + +- Wider import fields + +# Version 1.5 + +## General + +- Update to Salesforce API v 43.0 + +## Inspector menu + +- Show record details - currently for objects with record types only +- Link to LEX object manager/setup for object in focus + +# Version 1.4 + +## Inspector menu + +- Support for Spring '18 LEX URL format (https://docs.releasenotes.salesforce.com/en-us/spring18/release-notes/rn_general_enhanced_urls_cruc.htm) + +# Version 1.3 + +## General + +- Rewritten the implementation of Data Export and Data Import, in order to comply with the updated version of Mozilla's add-ons policy. +- Rewritten the implementation of Data Export and Data Import, in order to comply with the updated version of Mozilla's add-ons policy. + +# Version 1.2 + +## General + +- Update API versoin to Spring 17. + +## Inspector menu + +- Use the autocomplete to find object API names, labels and ID prefixes. +- View some information about the selected record or object directly in the menu. +- Inspect objects in the Tooling API and objects you don't have read access to. +- When viewing a Deployment Status, a new button allows you to get all the details of the deployment. +- The Explore API button is now visible everywhere. + +## Show all data + +- The Type column has more information. (required, unique, auto number etc.) +- Add your own columns, (for example a column showing the formula of formula fields, or a collumn that tells which fields can be used as a filter.) for both fields and relationships. +- The "Advanced filter" option is more discoverable now. +- New button to start data export for the shown object. +- New button to edit the page layout for the shown record. +- Better handling of objects that share a common ID prefix or is available with both the regular API and the Tooling API. + +## Data export + +- Save your favourite SOQL queries. +- The query history remembers if queries were done with the Tooling API or not. +- Fixed right clicking on IDs in the exported data. + +## Data import + +- Fix for importing data from Excel on Mac into Chrome. + +## Org Limits + +- View how much of your org's limits you are currently using. + +## Download Metadata + +- Download all your org's Apex classes, Visualforce pages, objects, fields, validation rules, workflow rules, reports and much more. Use it for backup, or if you want to search for any place a particular item is used, or for many other purposes. + +## API Explorer + +- Choose between showing the result for easy viewing or for easy copying. +- Make SOAP requests. +- Make REST requests for any HTTP method. +- Edit any API request before sending. + +# Version 1.1 + +## General + +- Update API versoin to Winter 17. +- Find the current page's record ID for Visualforce pages that store the record ID in a non-standard parameter name. + +## Data import + +- Don't make describe calls in an infinite loop when Salesforce returns an error (Salesforce Winter 17 Tooling API has a number objects starting with autogen\_\_ that don't work properly). + +# Version 1.0 + +## General + +- The Inspector is now shown in regular tabs instead of popups. You can now choose if you want to open a link in the same tab (the default), or a new tab/window, using normal browser menus and shortcuts. Previously every link opened a new popup window. +- Restyled the Inspector menu to use Lightning Design. Restyling the rest will come later. +- Switched to a more robust API for getting the Salesforce session ID. It now works with all session security settings, and it works in Lightning Experience. +- Added a logo/icon. +- The salesforce hostname is now visible as a parameter in the URL bar. +- If you have an outdated browser version that is not supported by the latest version of Salesforce Inspector, Salesforce Inspector will not autoupdate. +- Updated API version to Summer 16. + +## Show all data + +- When copy-pasting a value, there is no longer extra white-space at the beginning and end of the copied text. + +## Data import + +- Ask for confirmation before closing an in-progress data import. +- Tweaks to how batch concurrency/threads work. + +## Data export + +- If an error occurs during a data export, we now keep the data that is already exported. + +## Known Issues + +- When using Firefox, it no longer works in Private Browsing mode, since it cannot get the Salesforce session ID. See https://bugzilla.mozilla.org/show_bug.cgi?id=1254221 . + +# Version 0.10 + +## General + +- Update API version to Spring 16. + +## Show all data + +- Show information about the page layout of the inspected record. +- Make quick value selection work in Chrome again. + +## Data export + +- Make record IDs clickable in the result table, in adition to object names. +- Offer to either view all data for a record or view the record in normal Salesforce UI. +- Fix bug opening the all data window when exporting with the Tooling API. +- Fix keyboard shortcut issue in some variations of Chrome. + +## Data import + +- Make record IDs clickable in the status table. -Version 1.12 -=========== +## API explorer -General -------- -* Update to Salesforce API v 51.0 (Spring '21) +- Display results as a table instead of CSV. -Version 1.11 -=========== +# Version 0.9 -General -------- -* Make inspector available on Visualforce pages on new visualforce.com domain. See #143 +## General -Org Limits ----------- -* Displays "consumed" count +- Show the inspector menu in the inspector's own windows. +- Better handling of network errors and errors returned by the Salesforce API. -Version 1.10 -=========== +## Show field metadata -General -------- -* Update to Salesforce API v 48.0 +- Fix viewing field metadata for a Visualforce page. -Version 1.9 -=========== +## Show all data -Inspector menu --------------- -* Fix a bug fix hiding the "show field metadata" button (#150) +- Show the object/record input field everywhere instead of only in the developer console. +- Fix "setup" links for person accounts and for orgs with many custom fields. +- Allow editing only specific fields of a record, and refresh the data after saving. +- Improve selection. -Version 1.8 -=========== +## Data export -Inspector menu --------------- -* Added user search aspect to simplify access to detailed user data and "login as". +- Support autocomplete for subqueries in the where clause. +- Sort the autocomplete results by relevance. +- Implement filtering of results (since browser search does not play nice with our lazy rendering). -Version 1.7 -=========== +## Data import -General -------- -* Update to Salesforce API v 47.0 +- Rewrite UI to be more guided. +- Graphical display of import status. +- Support for the tooling API. -Inspector menu --------------- -* A new link to switch in and out of Salesforce Setup, where you can choose to open in a new tab or not. +# Version 0.8 -Show all data -------------- -* Fixed a bug causing errors when viewing some special objects. -* Link to Salesforce Setup in both Classic and Lightning Experience. -* Use default values for blank fields when creating a new record. This avoids the error message that OwnerId is required but missing. +## General -Data import ------------ -* Save import options in your excel sheet, so you can update the same data again and again with a single copy-paste. +- Works in the service cloud console in Chrome (worked previously only in Firefox). +- Uses new extension API for Firefox (requires Firefox 44). +- Partial support for Salesforce1/Lightning. +- Update API version to Winter 16. +## Data export -Version 1.6 -=========== +- New simplified layout, that can handle larger amounts of data. -General -------- -* Update to Salesforce API v 45.0 -* Support for cloudforce.com orgs - -Show all data -------------- -* Buttons to Create, delete and clone records +## Show all data -Data export ------------ -* Keyboard shortcut to do export (ctrl+enter) -* Fixes saved query selection - -Data import ------------ -* Wider import fields - -Version 1.5 -=========== - -General -------- -* Update to Salesforce API v 43.0 - -Inspector menu --------------- -* Show record details - currently for objects with record types only -* Link to LEX object manager/setup for object in focus - -Version 1.4 -=========== - -Inspector menu --------------- -* Support for Spring '18 LEX URL format (https://docs.releasenotes.salesforce.com/en-us/spring18/release-notes/rn_general_enhanced_urls_cruc.htm) - -Version 1.3 -=========== - -General -------- -* Rewritten the implementation of Data Export and Data Import, in order to comply with the updated version of Mozilla's add-ons policy. -* Rewritten the implementation of Data Export and Data Import, in order to comply with the updated version of Mozilla's add-ons policy. - -Version 1.2 -=========== - -General -------- -* Update API versoin to Spring 17. - -Inspector menu --------------- -* Use the autocomplete to find object API names, labels and ID prefixes. -* View some information about the selected record or object directly in the menu. -* Inspect objects in the Tooling API and objects you don't have read access to. -* When viewing a Deployment Status, a new button allows you to get all the details of the deployment. -* The Explore API button is now visible everywhere. - -Show all data -------------- -* The Type column has more information. (required, unique, auto number etc.) -* Add your own columns, (for example a column showing the formula of formula fields, or a collumn that tells which fields can be used as a filter.) for both fields and relationships. -* The "Advanced filter" option is more discoverable now. -* New button to start data export for the shown object. -* New button to edit the page layout for the shown record. -* Better handling of objects that share a common ID prefix or is available with both the regular API and the Tooling API. - -Data export ------------ -* Save your favourite SOQL queries. -* The query history remembers if queries were done with the Tooling API or not. -* Fixed right clicking on IDs in the exported data. - -Data import ------------ -* Fix for importing data from Excel on Mac into Chrome. - -Org Limits ----------- -* View how much of your org's limits you are currently using. - -Download Metadata ------------------ -* Download all your org's Apex classes, Visualforce pages, objects, fields, validation rules, workflow rules, reports and much more. Use it for backup, or if you want to search for any place a particular item is used, or for many other purposes. - -API Explorer ------------- -* Choose between showing the result for easy viewing or for easy copying. -* Make SOAP requests. -* Make REST requests for any HTTP method. -* Edit any API request before sending. - -Version 1.1 -============ - -General -------- -* Update API versoin to Winter 17. -* Find the current page's record ID for Visualforce pages that store the record ID in a non-standard parameter name. - -Data import ------------ -* Don't make describe calls in an infinite loop when Salesforce returns an error (Salesforce Winter 17 Tooling API has a number objects starting with autogen__ that don't work properly). - -Version 1.0 -============ - -General -------- -* The Inspector is now shown in regular tabs instead of popups. You can now choose if you want to open a link in the same tab (the default), or a new tab/window, using normal browser menus and shortcuts. Previously every link opened a new popup window. -* Restyled the Inspector menu to use Lightning Design. Restyling the rest will come later. -* Switched to a more robust API for getting the Salesforce session ID. It now works with all session security settings, and it works in Lightning Experience. -* Added a logo/icon. -* The salesforce hostname is now visible as a parameter in the URL bar. -* If you have an outdated browser version that is not supported by the latest version of Salesforce Inspector, Salesforce Inspector will not autoupdate. -* Updated API version to Summer 16. - -Show all data -------------- -* When copy-pasting a value, there is no longer extra white-space at the beginning and end of the copied text. - -Data import ------------ -* Ask for confirmation before closing an in-progress data import. -* Tweaks to how batch concurrency/threads work. - -Data export ------------ -* If an error occurs during a data export, we now keep the data that is already exported. - -Known Issues ------------- -* When using Firefox, it no longer works in Private Browsing mode, since it cannot get the Salesforce session ID. See https://bugzilla.mozilla.org/show_bug.cgi?id=1254221 . - -Version 0.10 -============ - -General -------- -* Update API version to Spring 16. - -Show all data -------------- -* Show information about the page layout of the inspected record. -* Make quick value selection work in Chrome again. - -Data export ------------ -* Make record IDs clickable in the result table, in adition to object names. -* Offer to either view all data for a record or view the record in normal Salesforce UI. -* Fix bug opening the all data window when exporting with the Tooling API. -* Fix keyboard shortcut issue in some variations of Chrome. - -Data import ------------ -* Make record IDs clickable in the status table. - -API explorer ------------- -* Display results as a table instead of CSV. - -Version 0.9 -=========== - -General -------- -* Show the inspector menu in the inspector's own windows. -* Better handling of network errors and errors returned by the Salesforce API. - -Show field metadata -------------------- -* Fix viewing field metadata for a Visualforce page. - -Show all data -------------- -* Show the object/record input field everywhere instead of only in the developer console. -* Fix "setup" links for person accounts and for orgs with many custom fields. -* Allow editing only specific fields of a record, and refresh the data after saving. -* Improve selection. - -Data export ------------ -* Support autocomplete for subqueries in the where clause. -* Sort the autocomplete results by relevance. -* Implement filtering of results (since browser search does not play nice with our lazy rendering). - -Data import ------------ -* Rewrite UI to be more guided. -* Graphical display of import status. -* Support for the tooling API. - -Version 0.8 -=========== - -General -------- -* Works in the service cloud console in Chrome (worked previously only in Firefox). -* Uses new extension API for Firefox (requires Firefox 44). -* Partial support for Salesforce1/Lightning. -* Update API version to Winter 16. - -Data export ------------ -* New simplified layout, that can handle larger amounts of data. - -Show all data -------------- -* Allow opening the All Data window for any object or record from the developer console. -* Ability to show help text and description. -* Work around a bug in the tooling API introduced in Winter 16. +- Allow opening the All Data window for any object or record from the developer console. +- Ability to show help text and description. +- Work around a bug in the tooling API introduced in Winter 16. diff --git a/addon/manifest-template.json b/addon/manifest-template.json index 2c486b0a..10808124 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,9 +12,7 @@ } }, "minimum_chrome_version": "88", - "permissions": [ - "cookies" - ], + "permissions": ["cookies"], "host_permissions": [ "https://*.salesforce.com/*", "https://*.force.com/*", @@ -32,15 +30,8 @@ "https://*.visualforce.com/*" ], "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 +49,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..c6a949b6 100644 --- a/addon/manifest.json +++ b/addon/manifest.json @@ -1,14 +1,12 @@ { "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/*", @@ -26,14 +24,8 @@ "https://*.visualforce.com/*" ], "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 +43,10 @@ "explore-api.html", "limits.html" ], - "matches": [ - "https://*/*" - ], + "matches": ["https://*/*"], "extension_ids": [] } ], "incognito": "split", "manifest_version": 3 -} \ No newline at end of file +} From 0360c3ea1f680f9dc1edb8bfa73ed968473acec3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 07:33:02 +0000 Subject: [PATCH 05/54] Bump word-wrap from 1.2.3 to 1.2.4 Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed3bff49..3bcdfe6a 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.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" From 9bc6b5f854a5c12b20f46a83fe11817c14e2b43c Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 19 Jul 2023 11:33:10 +0200 Subject: [PATCH 06/54] Update release note link on popup --- CHANGES.md | 2 +- addon/popup.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 680d8227..04c85e5a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ # Version 1.20 ## General - +- Update pop-up release note link to github pages # Version 1.19 ## General diff --git a/addon/popup.js b/addon/popup.js index 6f9d7dd5..64135dd8 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -221,7 +221,7 @@ 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("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("span", {}, " / "), From 829757931e04d75c0ab548d9f5cd49e37d89c30e Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 19 Jul 2023 11:37:38 +0200 Subject: [PATCH 07/54] Update release notes --- docs/release-note.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-note.md b/docs/release-note.md index 1f465fb5..c80b98b6 100644 --- a/docs/release-note.md +++ b/docs/release-note.md @@ -1,5 +1,9 @@ # Release Notes +## Version 1.20 +- Update pop-up release note link to github pages +- Detect SObject on listview page [feature 121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/121) (idea by [Mehdi Cherfaoui](https://github.com/mehdisfdc)) + ## 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)) From 2f39ad7e1aff1f3b6c97c98ebc3ad8156ce4693d Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 19 Jul 2023 15:37:32 +0200 Subject: [PATCH 08/54] Add 'Create New Flow' shortcut --- CHANGES.md | 1 + addon/links.js | 5 ++++- docs/release-note.md | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4f540ca1..4683a76e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- Add "Create New Flow" shortcut - Update pop-up release note link to github pages - Detect SObject on listview page [feature 121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/121) (idea by [Mehdi Cherfaoui](https://github.com/mehdisfdc)) diff --git a/addon/links.js b/addon/links.js index 5dab8ae7..afca1fa2 100644 --- a/addon/links.js +++ b/addon/links.js @@ -346,5 +346,8 @@ export let setupLinks = [ { 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 } + { 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 > Process Automation", prod: false } ] \ No newline at end of file diff --git a/docs/release-note.md b/docs/release-note.md index c80b98b6..83fda443 100644 --- a/docs/release-note.md +++ b/docs/release-note.md @@ -1,6 +1,7 @@ # Release Notes ## Version 1.20 +- Add "Create New Flow" shortcut - Update pop-up release note link to github pages - Detect SObject on listview page [feature 121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/121) (idea by [Mehdi Cherfaoui](https://github.com/mehdisfdc)) From b60e1ec773bd5b61d87b1a371cd819398abef049 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 12:23:37 +0000 Subject: [PATCH 09/54] Bump word-wrap from 1.2.3 to 1.2.5 Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.5. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.5) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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" From d6f94381305165b7d994ae7eee5e8ff495f1ba90 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:35:38 +0200 Subject: [PATCH 10/54] [general] Fix hardcoded browser in generate token url (#139) --- CHANGES.md | 1 + addon/popup.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4683a76e..5951cf26 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- 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" shortcut - Update pop-up release note link to github pages - Detect SObject on listview page [feature 121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/121) (idea by [Mehdi Cherfaoui](https://github.com/mehdisfdc)) diff --git a/addon/popup.js b/addon/popup.js index 496d5b3b..e12dc1c6 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -144,6 +144,7 @@ class App extends React.PureComponent { 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" }, @@ -188,7 +189,7 @@ class App extends React.PureComponent { 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.runtime.id + "/data-export.html?host=" + sfHost + "%26", target: linkTarget, className: !clientId ? "button hide" : "page-button slds-button slds-button_neutral" }, From 6b59f0966914bcc307516918916e9b74f158d65b Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Mon, 21 Aug 2023 10:00:06 +0200 Subject: [PATCH 11/54] Change method to get extension id to be compatible with firefox --- CHANGES.md | 1 + addon/popup.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 5951cf26..19fa0d88 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- 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" shortcut - Update pop-up release note link to github pages diff --git a/addon/popup.js b/addon/popup.js index e12dc1c6..29de4b1f 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -189,7 +189,7 @@ class App extends React.PureComponent { h("a", { ref: "generateToken", - href: `https://${sfHost}/services/oauth2/authorize?response_type=token&client_id=` + clientId + "&redirect_uri=" + browser + "-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" }, From 2841f38f13f33174f00a3efa9e114a66ccbed259 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Mon, 4 Sep 2023 10:07:05 +0200 Subject: [PATCH 12/54] [data-export] Add 'LIMIT 200' when selecting 'FIELDS' in autocomplete (#147) Fix #146 --- CHANGES.md | 1 + addon/data-export.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 19fa0d88..e969d600 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- Add 'LIMIT 200' when selecting 'FIELDS(' in autocomplete [issue 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" shortcut diff --git a/addon/data-export.js b/addon/data-export.js index 465d2a13..3e3887eb 100644 --- a/addon/data-export.js +++ b/addon/data-export.js @@ -289,6 +289,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(); }; From 97739288dbdf3bc0605b998fbbfa4f24920d8e61 Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 4 Sep 2023 18:17:45 +1000 Subject: [PATCH 13/54] Remove test setup manual step (#143) Add relate a contact to multiple accounts as metadata to deploy with other test dependencies --------- Co-authored-by: Aidan Majewski --- CHANGES.md | 1 + README.md | 5 ++--- test/main/default/package.xml | 6 +++++- test/main/default/settings/Account.settings-meta.xml | 4 ++++ 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 test/main/default/settings/Account.settings-meta.xml diff --git a/CHANGES.md b/CHANGES.md index e969d600..7e125316 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ - Add "Create New Flow" shortcut - Update pop-up release note link to github pages - Detect SObject on listview 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](https://github.com/aimaj) # Version 1.19 diff --git a/README.md b/README.md index a8dccdb7..f220fc51 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,8 @@ Linting : to assure indentation, formatting and best practices coherence, please 1. Set up an org (e.g. a Developer Edition) and apply the following customizations: 1. Everything described in metadata in `test/`. Push to org with `sfdx force:source:deploy -p test/ -u [your-test-org-alias]` - 2. Ensure _Allow users to relate a contact to multiple accounts_ is enabled (Setup→Account Settings) - 3. Ensure the org has no _namespace prefix_ (Setup→Package Manager) - 4. Assign PermissionSet SfInspector + 2. Ensure the org has no _namespace prefix_ (Setup→Package Manager) + 3. Assign PermissionSet SfInspector 2. Navigate to one of the extension pages and replace the file name with `test-framework.html`, for example `chrome-extension://example/test-framework.html?host=example.my.salesforce.com`. 3. Wait until "Salesforce Inspector unit test finished successfully" is shown. 4. If the test fails, open your browser's developer tools console to see error messages. 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 From 07fee841451479600f4b9cd4481f4524ca964614 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:43:50 +0200 Subject: [PATCH 14/54] [popup] Reduce the chances to hit limit on EntityDefinition query for large orgs (#149) Fix #138 --- CHANGES.md | 1 + addon/popup.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7e125316..55c1110c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- Reduce the chances to hit limit on EntityDefinition query 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 [issue 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)) diff --git a/addon/popup.js b/addon/popup.js index 29de4b1f..3f9ddcdb 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -413,8 +413,9 @@ class AllDataBox extends React.PureComponent { // 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"), + getEntityDefinitions(" WHERE QualifiedApiName < 'M' LIMIT 2000"), + getEntityDefinitions(" WHERE QualifiedApiName >= 'M' AND QualifiedApiName < 'U' LIMIT 2000"), + getEntityDefinitions(" WHERE QualifiedApiName >= 'U' LIMIT 2000"), ]) .then(() => { // TODO progressively display data as each of the three responses becomes available From 80bb4018878f707eef65975c9502b8f6ae663b5b Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:06:42 +0200 Subject: [PATCH 15/54] Fix/object explorer fix (#151) Fix #138 --- CHANGES.md | 2 +- addon/popup.js | 47 +++++++++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 55c1110c..71edd9d2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ ## General -- Reduce the chances to hit limit on EntityDefinition query for large orgs [issue 138](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/138) (issue by [AjitRajendran](https://github.com/AjitRajendran)) +- 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 [issue 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)) diff --git a/addon/popup.js b/addon/popup.js index 3f9ddcdb..542bd16e 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -385,21 +385,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([ @@ -411,11 +421,8 @@ class AllDataBox extends React.PureComponent { // 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' AND QualifiedApiName < 'U' LIMIT 2000"), - getEntityDefinitions(" WHERE QualifiedApiName >= 'U' 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 From a005227342bddcad2bac99a41b0e8a558867f9ac Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Fri, 8 Sep 2023 17:06:07 +0200 Subject: [PATCH 16/54] feature/add keybord shortcut to extension tabs (#152) #135 --- CHANGES.md | 1 + addon/popup.js | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 71edd9d2..556790fa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- Allow navigation to the extension tabs (Object, Users, Shortcuts) using keyboard [issue 135](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/135) (issue 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 [issue 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)) diff --git a/addon/popup.js b/addon/popup.js index 542bd16e..cbf0bd5b 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -104,7 +104,18 @@ class App extends React.PureComponent { if (e.key == "h" && this.refs.homeBtn) { 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"); @@ -445,9 +456,9 @@ class AllDataBox extends React.PureComponent { 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("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) From 62e84dfa831672026ef1871c88ed0477526b7718 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Fri, 8 Sep 2023 17:13:30 +0200 Subject: [PATCH 17/54] Update changes --- CHANGES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 556790fa..691a2af8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,15 +2,15 @@ ## General -- Allow navigation to the extension tabs (Object, Users, Shortcuts) using keyboard [issue 135](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/135) (issue by [Sarath Addanki](https://github.com/asknet)) +- 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 [issue 146](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/146) ) +- 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" shortcut - Update pop-up release note link to github pages - Detect SObject on listview 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](https://github.com/aimaj) +- Automate test setup manual step of contact to multiple accounts [Aidan Majewski](https://github.com/aimaj) # Version 1.19 From 554d90f6117f7779ea3a8eec0dc412a6532b79a6 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:48:22 +0200 Subject: [PATCH 18/54] Feature/add date litterals ago (#156) Fix #155 --- CHANGES.md | 1 + addon/data-export.js | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 691a2af8..57d5dc1d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- 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) ) diff --git a/addon/data-export.js b/addon/data-export.js index 3e3887eb..1a041405 100644 --- a/addon/data-export.js +++ b/addon/data-export.js @@ -628,7 +628,7 @@ class Model { rank: 1 }; } - // from http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_dateformats.htm Spring 15 + // 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: "" }; @@ -643,29 +643,36 @@ class Model { 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: "" }; From 62595cf67dafd308dd85a2300bcd0b5c8c6a3a66 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Tue, 12 Sep 2023 11:02:42 +0200 Subject: [PATCH 19/54] [show-all-data] Fix field setup link from show all data (#157) #154 --- CHANGES.md | 1 + addon/inspect.js | 6 ++++-- addon/setup-links.js | 13 ++++++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 57d5dc1d..1e22251d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- "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)) diff --git a/addon/inspect.js b/addon/inspect.js index 77918d52..6ace027b 100644 --- a/addon/inspect.js +++ b/addon/inspect.js @@ -702,9 +702,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) ); } @@ -908,9 +909,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) ); } 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; } From b5db8ea0ca255db3a0e6dd18cc3dd03c8826ed01 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 13 Sep 2023 14:03:06 +0200 Subject: [PATCH 20/54] BugFix for JSON paste in data import (sobject autodetect & fields value) --- CHANGES.md | 1 + addon/data-import.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1e22251d..64860182 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- 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)) diff --git a/addon/data-import.js b/addon/data-import.js index 82805e9f..e0387c1e 100644 --- a/addon/data-import.js +++ b/addon/data-import.js @@ -152,8 +152,8 @@ class Model { 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(",") From 5b27f9162c6068e3d0d7e476756c6c7349320d07 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 13 Sep 2023 16:02:34 +0200 Subject: [PATCH 21/54] Fix test --- addon/data-export-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/data-export-test.js b/addon/data-export-test.js index 9b178240..2e67327d 100644 --- a/addon/data-export-test.js +++ b/addon/data-export-test.js @@ -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); From 41fc346fd40345e6fb38b0c44b4d86f93164c2a8 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Mon, 18 Sep 2023 09:22:47 +0200 Subject: [PATCH 22/54] [data-import] [data-export] : Use localStorage variable to store csvSeparator (#150) #144 --- CHANGES.md | 2 ++ addon/data-export.js | 6 +++++- addon/data-import.js | 21 ++++++++++++++++---- addon/popup.js | 1 - docs/assets/images/how-to/csv-separator.png | Bin 0 -> 38207 bytes docs/how-to.md | 6 ++++++ 6 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 docs/assets/images/how-to/csv-separator.png diff --git a/CHANGES.md b/CHANGES.md index 64860182..2c1c7270 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## General +- 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)) +- Reduce the chances to hit limit on EntityDefinition query for large orgs [issue 138](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/138) (issue by [AjitRajendran](https://github.com/AjitRajendran)) - 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) diff --git a/addon/data-export.js b/addon/data-export.js index 1a041405..0ed6a473 100644 --- a/addon/data-export.js +++ b/addon/data-export.js @@ -210,7 +210,11 @@ class Model { copyToClipboard(this.exportedData.csvSerialize("\t")); } copyAsCsv() { - copyToClipboard(this.exportedData.csvSerialize(",")); + let separator = ","; + if (localStorage.getItem("csvSeparator")) { + separator = localStorage.getItem("csvSeparator"); + } + copyToClipboard(this.exportedData.csvSerialize(separator)); } copyAsJson() { copyToClipboard(JSON.stringify(this.exportedData.records, null, " ")); diff --git a/addon/data-import.js b/addon/data-import.js index e0387c1e..f2071124 100644 --- a/addon/data-import.js +++ b/addon/data-import.js @@ -96,7 +96,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,6 +152,11 @@ 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) { @@ -156,10 +165,10 @@ class Model { 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 +831,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(); diff --git a/addon/popup.js b/addon/popup.js index cbf0bd5b..8d796fba 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -431,7 +431,6 @@ 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. // Even if documentation mention that LIMIT and OFFSET are not supported, we use it to split the EntityDefinition queries into 2000 buckets getEntityDefinitions(), ]) 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 0000000000000000000000000000000000000000..8aea7e3fbbc30f3d8b5fe62117e2c7a52c1fb683 GIT binary patch literal 38207 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⪙FAtP(yD5F)>+p*@nJG{dR!!OWdd(#lEfyp?ml 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 literal 0 HcmV?d00001 diff --git a/docs/how-to.md b/docs/how-to.md index 368d604d..ec14d774 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -56,3 +56,9 @@ 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") + +## Define a CSV separator + +Add a new property "csvSeparator" containing the needed separator for CSV files + + Update csv separator From c4dbae709259b6e747abe761893b01608ec82aae Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Tue, 19 Sep 2023 15:12:52 +0200 Subject: [PATCH 23/54] Add 'Recycle Bin' shortcut --- CHANGES.md | 2 +- addon/links.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2c1c7270..2075e7fa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ - 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" shortcut +- Add "Create New Flow" and "Recycle Bin" shortcuts - Update pop-up release note link to github pages - Detect SObject on listview 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) diff --git a/addon/links.js b/addon/links.js index afca1fa2..57c37790 100644 --- a/addon/links.js +++ b/addon/links.js @@ -349,5 +349,6 @@ export let setupLinks = [ { 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 > Process Automation", prod: false } + { label: "Create New Flow", link: "/builder_platform_interaction/flowBuilder.app", section: "Platform Tools > Process Automation", prod: false }, + { label: "Recycle Bin", link: "/lightning/o/DeleteEvent/home", section: "App Launcher > Custom Link", prod: false } ] \ No newline at end of file From 7e1fb2234cf0fa7a649d33725ab46f36283b096d Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Tue, 26 Sep 2023 14:30:57 +0200 Subject: [PATCH 24/54] [explore-api] Page restyling (#160) --- CHANGES.md | 1 + addon/explore-api.css | 16 +++++- addon/explore-api.html | 32 +++++++----- addon/explore-api.js | 116 ++++++++++++++++++++++++----------------- 4 files changed, 103 insertions(+), 62 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2075e7fa..b7d997b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- 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)) - Reduce the chances to hit limit on EntityDefinition query for large orgs [issue 138](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/138) (issue by [AjitRajendran](https://github.com/AjitRajendran)) - Fix SObject auto detect for JSON input in data import 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,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", 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." + ) ); } From 7c390328433713a5a7741446ed68b3456bc747c5 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:12:52 +0200 Subject: [PATCH 25/54] [data-export] Add query templates (#161) Co-authored-by: Samuel Krissi --- CHANGES.md | 3 +- addon/data-export-test.js | 68 +++---- addon/data-export.css | 2 +- addon/data-export.js | 368 ++++++++++++++++++++------------------ 4 files changed, 234 insertions(+), 207 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b7d997b1..d2145679 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## General +- 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)) - Reduce the chances to hit limit on EntityDefinition query for large orgs [issue 138](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/138) (issue by [AjitRajendran](https://github.com/AjitRajendran)) @@ -72,7 +73,7 @@ ## General -- 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/addon/data-export-test.js b/addon/data-export-test.js index 2e67327d..85dd85bd 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","N_DAYS_AGO:n"], 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(); diff --git a/addon/data-export.css b/addon/data-export.css index db63d848..f395cfe9 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; diff --git a/addon/data-export.js b/addon/data-export.js index 0ed6a473..2fbfd0bd 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; @@ -285,7 +300,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"); @@ -294,7 +309,7 @@ class Model { } vm.queryInput.setRangeText(value + suffix, selStart, selEnd, "end"); //add query suffix if needed - if (value.startsWith("FIELDS") && !query.toLowerCase().includes("limit")){ + if (value.startsWith("FIELDS") && !query.toLowerCase().includes("limit")) { vm.queryInput.value += " LIMIT 200"; } vm.queryAutocompleteHandler(); @@ -306,7 +321,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; @@ -344,7 +359,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": @@ -358,7 +373,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; @@ -376,7 +391,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) }; @@ -415,7 +430,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": @@ -429,7 +444,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; @@ -492,7 +507,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 { @@ -516,7 +531,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; @@ -550,7 +565,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) { @@ -575,7 +590,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 = { @@ -597,7 +612,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) }; @@ -609,17 +624,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 { @@ -633,53 +648,53 @@ class Model { }; } // 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: "" }; + 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())) @@ -713,9 +728,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( @@ -723,9 +738,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: ""}; } }) ) @@ -746,7 +761,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 => { @@ -755,7 +770,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; @@ -764,7 +779,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."; @@ -800,7 +815,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; @@ -904,6 +919,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); @@ -924,53 +940,59 @@ class App extends React.Component { 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(); } @@ -980,7 +1002,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(); } @@ -989,29 +1011,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); @@ -1021,42 +1043,42 @@ 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(); } 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); @@ -1076,7 +1098,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(); } }); @@ -1095,7 +1117,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. @@ -1119,114 +1141,118 @@ 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("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 */ ) ) @@ -1242,16 +1268,16 @@ 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 +} From b65260826801989c112c7b7d47dcf5b8a28a3157 Mon Sep 17 00:00:00 2001 From: Joshua Yarmak <82345405+toly11@users.noreply.github.com> Date: Wed, 27 Sep 2023 11:43:36 +0300 Subject: [PATCH 26/54] [popup] Add option to open extension pages in new tab using keyboard shortcuts (#158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** In the current implementation, navigating to the Export and Import pages through keyboard shortcuts (`Ctrl+Option+I` on Mac or `Ctrl+Alt+I` on Windows, followed by `E` or `I`) only allows users to open these pages in the current tab. Opening these pages in the current tab disrupts the user flow In order to open the page in a new tab, users have to resort to using a combination of keyboard commands and mouse clicks which is not ideal. (control or command + mouse click). **Enhancement** This pull request introduces an enhancement to the existing keyboard shortcut functionalities, allowing users to open the extension pages in a new tab seamlessly. By holding the Command key (on Mac) or Control key (on Windows) in conjunction with the existing shortcut keys, users can now open the extension pages in a new tab, thereby preserving their position on the current page and facilitating a smoother navigation experience. **Testing** This feature has been tested across different environments to ensure compatibility and smooth integration with existing functionalities. I encourage further testing to validate its efficiency and usability enhancements. **Request for Review** I kindly request a review of this pull request and am open to making any necessary adjustments based on your feedback. Thank you for considering this valuable enhancement to the user experience. --------- Co-authored-by: user Co-authored-by: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> --- CHANGES.md | 2 ++ addon/popup.js | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d2145679..76dba4d7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## General + +- 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)) diff --git a/addon/popup.js b/addon/popup.js index 8d796fba..28a79e2a 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -79,29 +79,32 @@ 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(); } if (e.key == "o") { @@ -1608,4 +1611,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 From 4144bd4deea26f38c1039aa10af74979884642bd Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 27 Sep 2023 11:24:57 +0200 Subject: [PATCH 27/54] Update documentation's link --- CHANGES.md | 2 +- addon/popup.js | 416 ++++++++++++++++++++++++------------------------- 2 files changed, 209 insertions(+), 209 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 76dba4d7..9237aa48 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ ## General - +- 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 diff --git a/addon/popup.js b/addon/popup.js index 28a79e2a..a7ae0e96 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 @@ -122,12 +122,12 @@ class App extends React.PureComponent { } 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); @@ -151,7 +151,7 @@ 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(); @@ -161,10 +161,10 @@ class App extends React.PureComponent { 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 @@ -178,28 +178,28 @@ class App extends React.PureComponent { "Salesforce Inspector" ) ), - 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", @@ -210,7 +210,7 @@ class App extends React.PureComponent { 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", @@ -221,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", @@ -234,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://tprouvot.github.io/Salesforce-Inspector-reloaded/release-note/", 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", @@ -248,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") ) ) ) @@ -267,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, @@ -290,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(); } @@ -310,7 +310,7 @@ class AllDataBox extends React.PureComponent { } ensureKnownBrowserContext() { - let { contextUrl } = this.props; + let {contextUrl} = this.props; if (contextUrl) { let recordId = getRecordId(contextUrl); let path = getSfPathFromUrl(contextUrl); @@ -325,25 +325,25 @@ class AllDataBox extends React.PureComponent { 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); } @@ -359,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) { @@ -447,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, contextSobject, 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", { 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")) + 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, contextSobject, 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" ) ); @@ -487,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 []; } @@ -524,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) { @@ -538,14 +538,14 @@ 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; @@ -571,9 +571,9 @@ 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 }); + await this.setState({selectedUser: userDetail}); } catch (err) { console.error("Unable to query user details with: " + JSON.stringify(compositeQuery) + ".", err); } finally { @@ -586,13 +586,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, @@ -604,16 +604,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") )) ); } @@ -631,12 +631,12 @@ class AllDataBoxSObject extends React.PureComponent { } componentDidMount() { - let { contextRecordId, contextSobject } = this.props; + let {contextRecordId, contextSobject} = this.props; this.updateSelection(contextRecordId, contextSobject); } componentDidUpdate(prevProps) { - let { contextRecordId, sobjectsLoading, contextSobject } = this.props; + let {contextRecordId, sobjectsLoading, contextSobject} = this.props; if (prevProps.contextRecordId !== contextRecordId) { this.updateSelection(contextRecordId, contextSobject); } @@ -653,18 +653,18 @@ class AllDataBoxSObject extends React.PureComponent { match = this.getBestMatch(query); } - await this.setState({ selectedValue: match }); + 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 => { + 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); @@ -681,16 +681,16 @@ class AllDataBoxSObject extends React.PureComponent { } }).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 }); + this.setState({recordIdDetails: null}); }); } else { - this.setState({ recordIdDetails: null }); + this.setState({recordIdDetails: null}); } } 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; @@ -713,11 +713,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 []; @@ -731,21 +731,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)); @@ -753,7 +753,7 @@ class AllDataBoxSObject extends React.PureComponent { } onDataSelect(value) { - this.setState({ selectedValue: value }, () => { + this.setState({selectedValue: value}, () => { this.loadRecordIdDetails(); }); } @@ -775,7 +775,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()), @@ -783,7 +783,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, @@ -801,14 +801,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") ) ); } @@ -830,7 +830,7 @@ class AllDataBoxShortcut extends React.PureComponent { } async getMatches(shortcutSearch) { - let { setIsLoading } = this.props; + let {setIsLoading} = this.props; if (!shortcutSearch) { return []; } @@ -874,7 +874,7 @@ 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); results.forEach(element => { @@ -914,7 +914,7 @@ class AllDataBoxShortcut extends React.PureComponent { } async onDataSelect(shortcut) { - let { sfHost } = this.props; + let {sfHost} = this.props; window.open("https://" + sfHost + shortcut.link); } @@ -923,13 +923,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, @@ -941,16 +941,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") )) ); } @@ -958,7 +958,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) { @@ -968,34 +968,34 @@ class UserDetails extends React.PureComponent { } 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); } 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"); @@ -1004,59 +1004,59 @@ 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"}, + 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") )) ); } @@ -1073,33 +1073,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", @@ -1108,7 +1108,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") ) @@ -1126,7 +1126,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); @@ -1143,7 +1143,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); @@ -1196,7 +1196,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(); @@ -1205,24 +1205,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", {}, @@ -1238,11 +1238,11 @@ class AllDataSelection extends React.PureComponent { ))), - h(AllDataRecordDetails, { sfHost, selectedValue, recordIdDetails, className: "top-space" }), + h(AllDataRecordDetails, {sfHost, selectedValue, recordIdDetails, className: "top-space"}), ), - 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, @@ -1254,8 +1254,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)" ))) ) ); @@ -1268,15 +1268,15 @@ class AllDataRecordDetails extends React.PureComponent { return "https://" + sfHost + "/lightning/setup/ObjectManager/" + sobjectName + "/RecordTypes/" + recordtypeId + "/view"; } render() { - let { sfHost, recordIdDetails, className, selectedValue } = this.props; + let {sfHost, recordIdDetails, className, selectedValue} = this.props; if (recordIdDetails) { return ( - h("table", { className }, + h("table", {className}, h("tbody", {}, 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: ""}, recordIdDetails.recordTypeName) ) ), h("tr", {}, @@ -1310,14 +1310,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(); @@ -1331,32 +1331,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", @@ -1372,15 +1372,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); } @@ -1409,33 +1409,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; @@ -1448,7 +1448,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; @@ -1459,26 +1459,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) { @@ -1487,7 +1487,7 @@ class Autocomplete extends React.PureComponent { if (anItem) { let itemHeight = anItem.offsetHeight; if (itemHeight > 0) { - this.setState({ itemHeight }); + this.setState({itemHeight}); } } return; @@ -1503,7 +1503,7 @@ class Autocomplete extends React.PureComponent { } } render() { - let { matchingResults } = this.props; + let {matchingResults} = this.props; let { showResults, selectedIndex, @@ -1521,12 +1521,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" : ""), @@ -1534,7 +1534,7 @@ class Autocomplete extends React.PureComponent { onMouseEnter: () => this.onResultMouseEnter(index + firstRenderedIndex) }, element) ), - h("div", { style: { height: bottomSpace + "px" } }) + h("div", {style: {height: bottomSpace + "px"}}) ) ) ); From b04a2508caa7f1e026e68522ba944da9b5dd3993 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 27 Sep 2023 11:27:26 +0200 Subject: [PATCH 28/54] Remove duplicate section --- CHANGES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9237aa48..7bbfd01e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,7 +7,6 @@ - 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)) -- Reduce the chances to hit limit on EntityDefinition query for large orgs [issue 138](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/138) (issue by [AjitRajendran](https://github.com/AjitRajendran)) - 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) From 869ef4bd534b4bfff199056558bf6f51fcaf8101 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 27 Sep 2023 11:54:13 +0200 Subject: [PATCH 29/54] Fix documentation issues --- CHANGES.md | 309 ++----------------------------------------- docs/index.md | 67 ++++++---- docs/release-note.md | 91 +------------ mkdocs.yml | 5 +- 4 files changed, 55 insertions(+), 417 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7bbfd01e..a28ecbf2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,4 @@ -# Version 1.20 - -## General +## Version 1.20 - 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)) @@ -20,9 +18,7 @@ - Detect SObject on listview 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) -# Version 1.19 - -## General +## 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)) - Navigate to record detail (Flows, Profiles and PermissionSet) from shortcut search [feature 118](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/118) @@ -31,17 +27,13 @@ - Add Export Query button [feature 109](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/109) (idea by [Ryan Sherry](https://github.com/rpsherry-starburst)) - Add permission set group assignment button from popup [feature 106](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/106) -# Version 1.18 - -## General +## Version 1.18 - Update to Salesforce API v 58.0 (Summer '23) - Restyle popup with SLDS (Salesforce Lightning Design System) [feature 9](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/9) (idea by [Loïc BERBEY](https://github.com/lberbey), contribution by [Pietro Martino](https://github.com/pietromartino)) - Fix "Show all data" shortcut from popup [issue 96](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/96) (fix by [Pietro Martino](https://github.com/pietromartino)) -# Version 1.17 - -## General +## Version 1.17 - Add toLabel function among autocomplete query suggestions [feature 90](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/90) (idea by [Mickael Gudin](https://github.com/mickaelgudin)) - Update spinner on inspect page when loading or saving records and disable button [feature 69](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/69) (idea by [Camille Guillory](https://github.com/CamilleGuillory)) @@ -54,25 +46,19 @@ - Fix links for custom object [PR80](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/80) (contribution by [Mouloud Habchi](https://github.com/MD931)) - Fix links for custom setting [PR82](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/82) (contribution by [Mouloud Habchi](https://github.com/MD931)) -# Version 1.16 - -## General +## Version 1.16 - Select "Update" action by default when the data paste in data-import page contains Id column [feature 60](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/60) (idea by Bilel Morsli) - Allow users to update API Version [feature 58](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/58) - Add org instance in the popup and a link to Salesforce trust status website [feature 53](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/53) (idea by [Camille Guillory](https://github.com/CamilleGuillory) ) - Fix saved query when it contains ":" [issue 55](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/55) (bug found by [Victor Garcia](https://github.com/victorgz/) ) -# Version 1.15 - -## General +## Version 1.15 - Add "PSet" button to access user permission set assignment from User tab [feature 49](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/49) - Add shortcut tab to access setup quick links [feature 42](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/42) -# Version 1.14 - -## General +## 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)) @@ -87,294 +73,19 @@ - [Switch background color on import page to alert users that it's a production environnement](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/20) - Implement Auth2 flow to generate access token for connected App -# Version 1.13 - -## General +## Version 1.13 - [Automatically remove spaces from column name in import](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/23) - Update to Salesforce API v 56.0 (Winter '23) - Add "Skip all unknown fields" to import page - Add User Id to pop-up - -Inspector menu - - Support Enhanced Domain [issue #222](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector/issues/222) from [PR223](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector/pull/223) - [Add inactive users to search result](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/21) - -Inspector menu - - Update to Salesforce API v 55.0 (Summer '22) - Update to Salesforce API v 54.0 (Spring '22) - [Sticked table header to the top on export](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/10) - Update to Salesforce API v 53.0 (Winter '22) - Add label to saved query and sort list. -- Remove extra comma when autocomplete query in data export, or select a field from suggested fields juste before 'FROM' keyword. - -Inspector menu - -- Add "Copy Id" option when clicking on a Sobject field or Id in data export page. - -Inspector menu - +- Remove extra comma when autocomplete query in data export, or select a field from suggested fields just before 'FROM' keyword. +- Add "Copy Id" option when clicking on a SObject field or Id in data export page. - Integrate UI updates from [Garywoo's fork](https://github.com/Garywoo/Chrome-Salesforce-inspector) - -# Version 1.12 - -## General - -- Update to Salesforce API v 51.0 (Spring '21) - -# Version 1.11 - -## General - -- Make inspector available on Visualforce pages on new visualforce.com domain. See #143 - -## Org Limits - -- Displays "consumed" count - -# Version 1.10 - -## General - -- Update to Salesforce API v 48.0 - -# Version 1.9 - -## Inspector menu - -- Fix a bug fix hiding the "show field metadata" button (#150) - -# Version 1.8 - -## Inspector menu - -- Added user search aspect to simplify access to detailed user data and "login as". - -# Version 1.7 - -## General - -- Update to Salesforce API v 47.0 - -## Inspector menu - -- A new link to switch in and out of Salesforce Setup, where you can choose to open in a new tab or not. - -## Show all data - -- Fixed a bug causing errors when viewing some special objects. -- Link to Salesforce Setup in both Classic and Lightning Experience. -- Use default values for blank fields when creating a new record. This avoids the error message that OwnerId is required but missing. - -## Data import - -- Save import options in your excel sheet, so you can update the same data again and again with a single copy-paste. - -# Version 1.6 - -## General - -- Update to Salesforce API v 45.0 -- Support for cloudforce.com orgs - -## Show all data - -- Buttons to Create, delete and clone records - -## Data export - -- Keyboard shortcut to do export (ctrl+enter) -- Fixes saved query selection - -## Data import - -- Wider import fields - -# Version 1.5 - -## General - -- Update to Salesforce API v 43.0 - -## Inspector menu - -- Show record details - currently for objects with record types only -- Link to LEX object manager/setup for object in focus - -# Version 1.4 - -## Inspector menu - -- Support for Spring '18 LEX URL format (https://docs.releasenotes.salesforce.com/en-us/spring18/release-notes/rn_general_enhanced_urls_cruc.htm) - -# Version 1.3 - -## General - -- Rewritten the implementation of Data Export and Data Import, in order to comply with the updated version of Mozilla's add-ons policy. -- Rewritten the implementation of Data Export and Data Import, in order to comply with the updated version of Mozilla's add-ons policy. - -# Version 1.2 - -## General - -- Update API versoin to Spring 17. - -## Inspector menu - -- Use the autocomplete to find object API names, labels and ID prefixes. -- View some information about the selected record or object directly in the menu. -- Inspect objects in the Tooling API and objects you don't have read access to. -- When viewing a Deployment Status, a new button allows you to get all the details of the deployment. -- The Explore API button is now visible everywhere. - -## Show all data - -- The Type column has more information. (required, unique, auto number etc.) -- Add your own columns, (for example a column showing the formula of formula fields, or a collumn that tells which fields can be used as a filter.) for both fields and relationships. -- The "Advanced filter" option is more discoverable now. -- New button to start data export for the shown object. -- New button to edit the page layout for the shown record. -- Better handling of objects that share a common ID prefix or is available with both the regular API and the Tooling API. - -## Data export - -- Save your favourite SOQL queries. -- The query history remembers if queries were done with the Tooling API or not. -- Fixed right clicking on IDs in the exported data. - -## Data import - -- Fix for importing data from Excel on Mac into Chrome. - -## Org Limits - -- View how much of your org's limits you are currently using. - -## Download Metadata - -- Download all your org's Apex classes, Visualforce pages, objects, fields, validation rules, workflow rules, reports and much more. Use it for backup, or if you want to search for any place a particular item is used, or for many other purposes. - -## API Explorer - -- Choose between showing the result for easy viewing or for easy copying. -- Make SOAP requests. -- Make REST requests for any HTTP method. -- Edit any API request before sending. - -# Version 1.1 - -## General - -- Update API versoin to Winter 17. -- Find the current page's record ID for Visualforce pages that store the record ID in a non-standard parameter name. - -## Data import - -- Don't make describe calls in an infinite loop when Salesforce returns an error (Salesforce Winter 17 Tooling API has a number objects starting with autogen\_\_ that don't work properly). - -# Version 1.0 - -## General - -- The Inspector is now shown in regular tabs instead of popups. You can now choose if you want to open a link in the same tab (the default), or a new tab/window, using normal browser menus and shortcuts. Previously every link opened a new popup window. -- Restyled the Inspector menu to use Lightning Design. Restyling the rest will come later. -- Switched to a more robust API for getting the Salesforce session ID. It now works with all session security settings, and it works in Lightning Experience. -- Added a logo/icon. -- The salesforce hostname is now visible as a parameter in the URL bar. -- If you have an outdated browser version that is not supported by the latest version of Salesforce Inspector, Salesforce Inspector will not autoupdate. -- Updated API version to Summer 16. - -## Show all data - -- When copy-pasting a value, there is no longer extra white-space at the beginning and end of the copied text. - -## Data import - -- Ask for confirmation before closing an in-progress data import. -- Tweaks to how batch concurrency/threads work. - -## Data export - -- If an error occurs during a data export, we now keep the data that is already exported. - -## Known Issues - -- When using Firefox, it no longer works in Private Browsing mode, since it cannot get the Salesforce session ID. See https://bugzilla.mozilla.org/show_bug.cgi?id=1254221 . - -# Version 0.10 - -## General - -- Update API version to Spring 16. - -## Show all data - -- Show information about the page layout of the inspected record. -- Make quick value selection work in Chrome again. - -## Data export - -- Make record IDs clickable in the result table, in adition to object names. -- Offer to either view all data for a record or view the record in normal Salesforce UI. -- Fix bug opening the all data window when exporting with the Tooling API. -- Fix keyboard shortcut issue in some variations of Chrome. - -## Data import - -- Make record IDs clickable in the status table. - -## API explorer - -- Display results as a table instead of CSV. - -# Version 0.9 - -## General - -- Show the inspector menu in the inspector's own windows. -- Better handling of network errors and errors returned by the Salesforce API. - -## Show field metadata - -- Fix viewing field metadata for a Visualforce page. - -## Show all data - -- Show the object/record input field everywhere instead of only in the developer console. -- Fix "setup" links for person accounts and for orgs with many custom fields. -- Allow editing only specific fields of a record, and refresh the data after saving. -- Improve selection. - -## Data export - -- Support autocomplete for subqueries in the where clause. -- Sort the autocomplete results by relevance. -- Implement filtering of results (since browser search does not play nice with our lazy rendering). - -## Data import - -- Rewrite UI to be more guided. -- Graphical display of import status. -- Support for the tooling API. - -# Version 0.8 - -## General - -- Works in the service cloud console in Chrome (worked previously only in Firefox). -- Uses new extension API for Firefox (requires Firefox 44). -- Partial support for Salesforce1/Lightning. -- Update API version to Winter 16. - -## Data export - -- New simplified layout, that can handle larger amounts of data. - -## Show all data - -- Allow opening the All Data window for any object or record from the developer console. -- Ability to show help text and description. -- Work around a bug in the tooling API introduced in Winter 16. diff --git a/docs/index.md b/docs/index.md index 4df43380..0edfe77d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,11 @@ -# Home +# Salesforce Inspector Reloaded Documentation + +![GitHub release](https://img.shields.io/github/v/release/tprouvot/Salesforce-Inspector-reloaded?sort=semver) +[![Chrome Web Store Installs](https://img.shields.io/chrome-web-store/users/hpijlohoihegkfehhibggnkbjhoemldh)](https://chrome.google.com/webstore/detail/salesforce-inspector-relo/hpijlohoihegkfehhibggnkbjhoemldh) +[![Chrome Web Store Rating](https://img.shields.io/chrome-web-store/rating/hpijlohoihegkfehhibggnkbjhoemldh)](https://chrome.google.com/webstore/detail/salesforce-inspector-relo/hpijlohoihegkfehhibggnkbjhoemldh) +[![GitHub stars](https://img.shields.io/github/stars/tprouvot/Salesforce-Inspector-reloaded?cacheSeconds=3600)](https://github.com/tprouvot/Salesforce-Inspector-reloaded/stargazers/) +[![GitHub contributors](https://img.shields.io/github/contributors/tprouvot/Salesforce-Inspector-reloaded.svg)](https://github.com/tprouvot/Salesforce-Inspector-reloaded/graphs/contributors/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) Extension based on [Salesforce Inspector](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector) by Søren Krabbe. @@ -6,18 +13,18 @@ Chrome and Firefox extension to add a metadata layout on top of the standard Sal ## Release Note -[List of changes](CHANGES.md) +[List of changes](/release-note.md) -## New Features +## Best Features -* Allow users to update API Version [feature 58](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/58) -* Add new "Shortcuts" tab to accelerate setup navigation [feature 42](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/42) -* Add shortcuts links to (list of record types, current SObject RecordType and objet details, show all data from user tab) from popup [feature 34](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/34) -* Control access to Salesforce Inspector reloaded with profiles / permissions (Implement Auth2 flow to generate access token for connected App) [how to](https://github.com/tprouvot/Salesforce-Inspector-reloaded/wiki/How-to#use-sf-inspector-with-a-connected-app) -* Update manifest version from [v2](https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/) to v3 (extensions using manifest v2 will be removed from the store) -* New UI for Export / Import +- Add new "Shortcuts" tab to navigate to setup, Profiles, Permission Sets and Flows ! +- Add shortcuts links to (list of record types, current SObject RecordType and objet details, show all data from user tab) from popup [feature 34](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/34) +- Control access to Salesforce Inspector reloaded with profiles / permissions (Implement Auth2 flow to generate access token for connected App) [how to](https://github.com/tprouvot/Salesforce-Inspector-reloaded/wiki/How-to#use-sf-inspector-with-a-connected-app) +- Update manifest version from [v2](https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/) to v3 (extensions using manifest v2 will be removed from the store) +- New UI for Export / Import ## Security and Privacy + The Salesforce Inspector browser extension/plugin communicates directly between the user's web browser and the Salesforce servers. No data is sent to other parties and no data is persisted outside of Salesforce servers after the user leaves the Salesforce Inspector pages. The Inspector communicates via the official Salesforce webservice APIs on behalf of the currently logged in user. This means the Inspector will be capable of accessing nothing but the data and features the user has been granted access to in Salesforce. @@ -30,6 +37,7 @@ To validate the accuracy of this description, inspect the source code, monitor t Follow steps described in [wiki](https://github.com/tprouvot/Salesforce-Inspector-reloaded/wiki/How-to#use-sf-inspector-with-a-connected-app) ## Installation + From the Chrome Web Store : [Salesforce Inspector reloaded](https://chrome.google.com/webstore/detail/salesforce-inspector-relo/hpijlohoihegkfehhibggnkbjhoemldh) If you want to try it locally (to test the release candidate): @@ -43,8 +51,8 @@ If you want to try it locally (to test the release candidate): ## 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). -* When you enable the My Domain feature in Salesforce, Salesforce Inspector may not work until you have restarted your browser (or until you have deleted the "sid" cookie for the old Salesforce domain by other means). +- 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). +- When you enable the My Domain feature in Salesforce, Salesforce Inspector may not work until you have restarted your browser (or until you have deleted the "sid" cookie for the old Salesforce domain by other means). ## Development @@ -52,6 +60,7 @@ If you want to try it locally (to test the release candidate): 2. `npm install` ### Chrome + 1. `npm run chrome-dev-build` 2. Open `chrome://extensions/`. 3. Enable `Developer mode`. @@ -68,10 +77,10 @@ If you want to try it locally (to test the release candidate): ## Unit tests 1. Set up an org (e.g. a Developer Edition) and apply the following customizations: - 1. Everything described in metadata in `test/`. Push to org with `sfdx force:source:deploy -p test/ -u [your-test-org-alias]` - 2. Ensure _Allow users to relate a contact to multiple accounts_ is enabled (Setup→Account Settings) - 3. Ensure the org has no _namespace prefix_ (Setup→Package Manager) - 4. Assign PermissionSet SfInspector + 1. Everything described in metadata in `test/`. Push to org with `sfdx force:source:deploy -p test/ -u [your-test-org-alias]` + 2. Ensure _Allow users to relate a contact to multiple accounts_ is enabled (Setup→Account Settings) + 3. Ensure the org has no _namespace prefix_ (Setup→Package Manager) + 4. Assign PermissionSet SfInspector 2. Navigate to one of the extension pages and replace the file name with `test-framework.html`, for example `chrome-extension://example/test-framework.html?host=example.my.salesforce.com`. 3. Wait until "Salesforce Inspector unit test finished successfully" is shown. 4. If the test fails, open your browser's developer tools console to see error messages. @@ -80,8 +89,8 @@ If you want to try it locally (to test the release candidate): 1. `npm run eslint` -Release -------- +## Release + Version number must be manually incremented in [addon/manifest-template.json](addon/manifest-template.json) file ### Chrome @@ -94,21 +103,25 @@ If the version number is greater than the version currently in Chrome Web Store, 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) -* Stay completely inactive until the user explicitly interacts with it. The tool has the potential to break Salesforce functionality when used, since we rely on monkey patching and internal APIs. We must ensure that you cannot break Salesforce just by having the tool installed or enabled. For example, we won't fix the setup search placeholder bug. -* For manual ad-hoc tasks only. The tool is designed to help administrators and developers interact with Salesforce in the browser. It is after all a browser add-on. Enabling automation is a non-goal. -* User experience is important. Features should be intuitive and discoverable, but efficiency is more important than discoverability. More advanced features should be hidden, and primary features should be central. Performance is key. -* Automatically provide as much contextual information as possible, without overwhelming the user. Information that is presented automatically when needed is a lot more useful than information you need to explicitly request. For example, provide autocomplete for every input. -* Provide easy access to the raw Salesforce API. Enhance the interaction in a way that does not break the core use case, if our enhancements fails. For example, ensure we can display the result of a data export even if we cannot parse the SOQL query. -* It is fine to implement features that are already available in the core Salesforce UI, if we can make it easier, smarter or faster. -* Ensure that it works for as many users as possible. (for system administrators, for standard users, with person accounts, with multi currency, with large data volumes, with professional edition, on a slow network etc.) -* Be conservative about the number and complexity of Salesforce API requests we make, but don't sacrifice the other principles to do so. -* Focus on system administrators, developers and integrators. + +- Stay completely inactive until the user explicitly interacts with it. The tool has the potential to break Salesforce functionality when used, since we rely on monkey patching and internal APIs. We must ensure that you cannot break Salesforce just by having the tool installed or enabled. For example, we won't fix the setup search placeholder bug. +- For manual ad-hoc tasks only. The tool is designed to help administrators and developers interact with Salesforce in the browser. It is after all a browser add-on. Enabling automation is a non-goal. +- User experience is important. Features should be intuitive and discoverable, but efficiency is more important than discoverability. More advanced features should be hidden, and primary features should be central. Performance is key. +- Automatically provide as much contextual information as possible, without overwhelming the user. Information that is presented automatically when needed is a lot more useful than information you need to explicitly request. For example, provide autocomplete for every input. +- Provide easy access to the raw Salesforce API. Enhance the interaction in a way that does not break the core use case, if our enhancements fails. For example, ensure we can display the result of a data export even if we cannot parse the SOQL query. +- It is fine to implement features that are already available in the core Salesforce UI, if we can make it easier, smarter or faster. +- Ensure that it works for as many users as possible. (for system administrators, for standard users, with person accounts, with multi currency, with large data volumes, with professional edition, on a slow network etc.) +- Be conservative about the number and complexity of Salesforce API requests we make, but don't sacrifice the other principles to do so. +- Focus on system administrators, developers and integrators. ## About By Thomas Prouvot and forked from [Søren Krabbe and Jesper Kristensen](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector) ## License + [MIT](./LICENSE) diff --git a/docs/release-note.md b/docs/release-note.md index 83fda443..6d840f8b 100644 --- a/docs/release-note.md +++ b/docs/release-note.md @@ -1,90 +1 @@ -# Release Notes - -## Version 1.20 -- Add "Create New Flow" shortcut -- Update pop-up release note link to github pages -- Detect SObject on listview page [feature 121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/121) (idea by [Mehdi Cherfaoui](https://github.com/mehdisfdc)) - -## 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)) -- Navigate to record detail (Flows, Profiles and PermissionSet) from shortcut search [feature 118](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/118) -- Fix country codes from LocalSidKey convention [PR117](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/117) (contribution by [Luca Bassani](https://github.com/baslu93)) -- Use custom shortcuts [feature 115](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/115) -- Add Export Query button [feature 109](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/109) (idea by [Ryan Sherry](https://github.com/rpsherry-starburst)) -- Add permission set group assignment button from popup [feature 106](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/106) - -## Version 1.18 - -- Update to Salesforce API v 58.0 (Summer '23) -- Restyle popup with SLDS (Salesforce Lightning Design System) [feature 9](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/9) (idea by [Loïc BERBEY](https://github.com/lberbey), contribution by [Pietro Martino](https://github.com/pietromartino)) -- Fix "Show all data" shortcut from popup [issue 96](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/96) (fix by [Pietro Martino](https://github.com/pietromartino)) - -## Version 1.17 - -- Add toLabel function among autocomplete query suggestions [feature 90](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/90) (idea by [Mickael Gudin](https://github.com/mickaelgudin)) -- Update spinner on inspect page when loading or saving records and disable button [feature 69](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/69) (idea by [Camille Guillory](https://github.com/CamilleGuillory)) -- Show "Copy Id" from Inspect page [feature 12](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/12) -- Add a configuration option for links to open in a new tab [feature 78](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/78) (idea by [Henri Vilminko](https://github.com/hvilminko)) -- Import data as JSON [feature 75](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/75) (idea by [gaelguimini](https://github.com/gaelguimini)) -- Fix auto update action on data import [issue 73](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/73) (issue by [Juul1](https://github.com/Juul1)) -- Restore focus on suggested fields when pressing tab key in query editor [issue 66](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/66) (idea by [Enrique Muñoz](https://github.com/emunoz-at-wiris)) -- Update shortcut indication for mac users -- Fix links for custom object [PR80](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/80) (contribution by [Mouloud Habchi](https://github.com/MD931)) -- Fix links for custom setting [PR82](https://github.com/tprouvot/Salesforce-Inspector-reloaded/pull/82) (contribution by [Mouloud Habchi](https://github.com/MD931)) - -## Version 1.16 - -- Select "Update" action by default when the data paste in data-import page contains Id column [feature 60](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/60) (by Bilel Morsli) -- Allow users to update API ## Version [feature 58](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/58) -- Add org instance in the popup and a link to Salesforce trust status website [feature 53](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/53) (by [Camille Guillory](https://github.com/CamilleGuillory) ) -- Fix saved query when it contains ":" [issue 55](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/55) (by [Victor Garcia](https://github.com/victorgz/) ) - -## Version 1.15 - -- Add "PSet" button to access user permission set assignment from User tab [feature 49](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/49) -- Add shortcut tab to access setup quick links [feature 42](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/42) - -## 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) ) - -![image](https://user-images.githubusercontent.com/96471586/226161542-cbedec0a-8988-4559-9152-d067ea6f9cb6.png) - -- Fix links (object fields and object list) for custom metadata objects [issue 39](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/39) -- Add shortcut link to object list from popup (idea by [Samuel Krissi](https://github.com/samuelkrissi) ) -- Add shortcuts links to (list of record types, current SObject RecordType and objet details, show all data from user tab) from popup [feature 34](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/34) -- Update manifest ## Version from [v2](https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/) to v3 -- Auto detect SObject on import page when posting data which contain SObject header [feature 30](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/30) -- Update to Salesforce API v 57.0 (Spring '23) -- [Switch background color on import page to alert users that it's a production environnement](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/20) -- Implement Auth2 flow to generate access token for connected App - -## Version 1.13 - -- [Automatically remove spaces from column name in import](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/23) -- Update to Salesforce API v 56.0 (Winter '23) -- Add "Skip all unknown fields" to import page -- Add User Id to pop-up - -Add user - -- Support Enhanced Domain [issue #222](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector/issues/222) from [PR223](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector/pull/223) -- [Add inactive users to search result](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/21) - -Inspector menu - -- Update to Salesforce API v 55.0 (Summer '22) -- Update to Salesforce API v 54.0 (Spring '22) -- [Sticked table header to the top on export](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/10) -- Update to Salesforce API v 53.0 (Winter '22) -- Add label to saved query and sort list. -- Remove extra comma when autocomplete query in data export, or select a field from suggested fields just before 'FROM' keyword. - -Inspector menu - -- Add "Copy Id" option when clicking on a SObject field or Id in data export page. - -Inspector menu - -- Integrate UI updates from [Garywoo's fork](https://github.com/Garywoo/Chrome-Salesforce-inspector) +--8<-- "CHANGES.md" diff --git a/mkdocs.yml b/mkdocs.yml index 07a11034..9e5331ed 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,7 +39,10 @@ markdown_extensions: - pymdownx.highlight: anchor_linenums: true - pymdownx.inlinehilite - - pymdownx.snippets + - pymdownx.snippets: + base_path: + - CHANGES.md + check_paths: true - admonition - pymdownx.arithmatex: generic: true From 8e4778d7bd861349c10b1ee98be824957c0d38b0 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 27 Sep 2023 14:05:30 +0200 Subject: [PATCH 30/54] Add new functionalities to how-to section --- docs/how-to.md | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/how-to.md b/docs/how-to.md index ec14d774..b81e7666 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,10 +55,45 @@ 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`) ## Define a CSV separator -Add a new property "csvSeparator" containing the needed separator for CSV files +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) From 326ac73feb55d8a7fb36fb87b405f371fc591977 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 27 Sep 2023 14:27:08 +0200 Subject: [PATCH 31/54] Update site info --- mkdocs.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 From ad8fd02db32666572b32cda962a1f9fda60b2d9a Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:06:08 +0200 Subject: [PATCH 32/54] [popup] feature : Display record name in popup (#166) #165 --- CHANGES.md | 1 + addon/popup.js | 63 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a28ecbf2..cb47bee9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## Version 1.20 +- 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)) diff --git a/addon/popup.js b/addon/popup.js index a7ae0e96..0dae2998 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -661,34 +661,41 @@ class AllDataBoxSObject extends React.PureComponent { 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}); } } + 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; // Find the best match based on the record id or object name from the page URL. @@ -1268,15 +1275,21 @@ class AllDataRecordDetails extends React.PureComponent { 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("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", {}, From 6d42e4a60bba8dcfcc501ae99a08a4c135ef2615 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Fri, 29 Sep 2023 11:49:31 +0200 Subject: [PATCH 33/54] Add missing function for display name in popup --- addon/popup.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addon/popup.js b/addon/popup.js index 0dae2998..4144d96f 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -1271,6 +1271,9 @@ 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"; } From ac1bef303bbd406dcec93f7a37980053f3e18c49 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Fri, 29 Sep 2023 11:51:09 +0200 Subject: [PATCH 34/54] Fix target --- addon/popup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/popup.js b/addon/popup.js index 4144d96f..765bdb00 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -1245,7 +1245,7 @@ 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}), selectedValue.recordId && selectedValue.recordId.startsWith("0Af") From 0e45ed1f70d2f096fc2e01feaa67a452d7acc9aa Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Fri, 29 Sep 2023 16:19:07 +0200 Subject: [PATCH 35/54] Add how to add custom links to shortcut tab --- docs/how-to.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/how-to.md b/docs/how-to.md index b81e7666..5ced31c6 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -97,3 +97,40 @@ By default when you enter keyword in the Shortcut tab, the search is performed o 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 From f6623374c51b7168afb49bfbdb396cdeb4c5c0bc Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Mon, 2 Oct 2023 11:12:26 +0200 Subject: [PATCH 36/54] Add documentation link, remove extra lines --- README.md | 88 ++++++++++++++++++++++++++----------------------------- 1 file changed, 41 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index f220fc51..a6bb523e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # Salesforce inspector reloaded @@ -32,16 +31,22 @@ We all know and love Salesforce Inspector: As the great Søren Krabbe did not ha - [About](#about) - [License](#license) +## Documentation + +> 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 ------ + [List of changes](CHANGES.md) -* Allow users to update API Version [feature 58](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/58) -* Add new "Shortcuts" tab to accelerate setup navigation [feature 42](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/42) -* Add shortcuts links to (list of record types, current SObject RecordType and objet details, show all data from user tab) from popup [feature 34](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/34) -* Control access to Salesforce Inspector reloaded with profiles / permissions (Implement Auth2 flow to generate access token for connected App) [how to](https://github.com/tprouvot/Salesforce-Inspector-reloaded/wiki/How-to#use-sf-inspector-with-a-connected-app) -* Update manifest version from [v2](https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/) to v3 (extensions using manifest v2 will be removed from the store) -* New UI for Export / Import +- Allow users to update API Version [feature 58](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/58) +- Add new "Shortcuts" tab to accelerate setup navigation [feature 42](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/42) +- Add shortcuts links to (list of record types, current SObject RecordType and objet details, show all data from user tab) from popup [feature 34](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/34) +- Control access to Salesforce Inspector reloaded with profiles / permissions (Implement Auth2 flow to generate access token for connected App) [how to](https://github.com/tprouvot/Salesforce-Inspector-reloaded/wiki/How-to#use-sf-inspector-with-a-connected-app) +- Update manifest version from [v2](https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/) to v3 (extensions using manifest v2 will be removed from the store) +- New UI for Export / Import ## Security and Privacy @@ -54,11 +59,16 @@ 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 ------ -From the Chrome Web Store : [Salesforce Inspector reloaded](https://chrome.google.com/webstore/detail/salesforce-inspector-relo/hpijlohoihegkfehhibggnkbjhoemldh) + +### Browser Stores + +- [Chrome Web Store](https://chrome.google.com/webstore/detail/salesforce-inspector-relo/hpijlohoihegkfehhibggnkbjhoemldh) +- [Firefox Browser Add-ons](https://addons.mozilla.org/en-US/firefox/addon/salesforce-inspector-reloaded/) + +### Local Installation If you want to try it locally (to test the release candidate): @@ -67,16 +77,14 @@ If you want to try it locally (to test the release candidate): 3. Open `chrome://extensions/`. 4. Enable `Developer mode`. 5. Click `Load unpacked extension...`. -6. Select the `addon` subdirectory of this repository. +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). -* When you enable the My Domain feature in Salesforce, Salesforce Inspector may not work until you have restarted your browser (or until you have deleted the "sid" cookie for the old Salesforce domain by other means). +- 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). +- When you enable the My Domain feature in Salesforce, Salesforce Inspector may not work until you have restarted your browser (or until you have deleted the "sid" cookie for the old Salesforce domain by other means). ## Contributions ------ Contributions are welcomed ! @@ -85,14 +93,13 @@ This branch will be merge into master when the new version is published on web s Linting : to assure indentation, formatting and best practices coherence, please install ESLint extension. - ## Development ------ 1. Install Node.js with npm 2. `npm install` ### Chrome + 1. `npm run chrome-dev-build` 2. Open `chrome://extensions/`. 3. Enable `Developer mode`. @@ -107,12 +114,11 @@ Linting : to assure indentation, formatting and best practices coherence, please 4. Select the file `addon/manifest.json`. ### Unit tests ------ 1. Set up an org (e.g. a Developer Edition) and apply the following customizations: - 1. Everything described in metadata in `test/`. Push to org with `sfdx force:source:deploy -p test/ -u [your-test-org-alias]` - 2. Ensure the org has no _namespace prefix_ (Setup→Package Manager) - 3. Assign PermissionSet SfInspector + 1. Everything described in metadata in `test/`. Push to org with `sfdx force:source:deploy -p test/ -u [your-test-org-alias]` + 2. Ensure the org has no _namespace prefix_ (Setup→Package Manager) + 3. Assign PermissionSet SfInspector 2. Navigate to one of the extension pages and replace the file name with `test-framework.html`, for example `chrome-extension://example/test-framework.html?host=example.my.salesforce.com`. 3. Wait until "Salesforce Inspector unit test finished successfully" is shown. 4. If the test fails, open your browser's developer tools console to see error messages. @@ -121,36 +127,24 @@ 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 +## Design Principles -### 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) -* Stay completely inactive until the user explicitly interacts with it. The tool has the potential to break Salesforce functionality when used, since we rely on monkey patching and internal APIs. We must ensure that you cannot break Salesforce just by having the tool installed or enabled. For example, we won't fix the setup search placeholder bug. -* For manual ad-hoc tasks only. The tool is designed to help administrators and developers interact with Salesforce in the browser. It is after all a browser add-on. Enabling automation is a non-goal. -* User experience is important. Features should be intuitive and discoverable, but efficiency is more important than discoverability. More advanced features should be hidden, and primary features should be central. Performance is key. -* Automatically provide as much contextual information as possible, without overwhelming the user. Information that is presented automatically when needed is a lot more useful than information you need to explicitly request. For example, provide autocomplete for every input. -* Provide easy access to the raw Salesforce API. Enhance the interaction in a way that does not break the core use case, if our enhancements fails. For example, ensure we can display the result of a data export even if we cannot parse the SOQL query. -* It is fine to implement features that are already available in the core Salesforce UI, if we can make it easier, smarter or faster. -* Ensure that it works for as many users as possible. (for system administrators, for standard users, with person accounts, with multi currency, with large data volumes, with professional edition, on a slow network etc.) -* Be conservative about the number and complexity of Salesforce API requests we make, but don't sacrifice the other principles to do so. -* Focus on system administrators, developers and integrators. + +- Stay completely inactive until the user explicitly interacts with it. The tool has the potential to break Salesforce functionality when used, since we rely on monkey patching and internal APIs. We must ensure that you cannot break Salesforce just by having the tool installed or enabled. For example, we won't fix the setup search placeholder bug. +- For manual ad-hoc tasks only. The tool is designed to help administrators and developers interact with Salesforce in the browser. It is after all a browser add-on. Enabling automation is a non-goal. +- User experience is important. Features should be intuitive and discoverable, but efficiency is more important than discoverability. More advanced features should be hidden, and primary features should be central. Performance is key. +- Automatically provide as much contextual information as possible, without overwhelming the user. Information that is presented automatically when needed is a lot more useful than information you need to explicitly request. For example, provide autocomplete for every input. +- Provide easy access to the raw Salesforce API. Enhance the interaction in a way that does not break the core use case, if our enhancements fails. For example, ensure we can display the result of a data export even if we cannot parse the SOQL query. +- It is fine to implement features that are already available in the core Salesforce UI, if we can make it easier, smarter or faster. +- Ensure that it works for as many users as possible. (for system administrators, for standard users, with person accounts, with multi currency, with large data volumes, with professional edition, on a slow network etc.) +- Be conservative about the number and complexity of Salesforce API requests we make, but don't sacrifice the other principles to do so. +- Focus on system administrators, developers and integrators. ## About ------ + By Thomas Prouvot and forked from [Søren Krabbe and Jesper Kristensen](https://github.com/sorenkrabbe/Chrome-Salesforce-inspector) ## License ------ + [MIT](./LICENSE) From e37c6b247c3da12440cffd9ae112077f718a1fed Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Wed, 11 Oct 2023 12:14:48 +0200 Subject: [PATCH 37/54] Add custom links "Create new XXX" --- addon/links.js | 675 +++++++++++++++++++++++++------------------------ 1 file changed, 339 insertions(+), 336 deletions(-) diff --git a/addon/links.js b/addon/links.js index 57c37790..e05bf473 100644 --- a/addon/links.js +++ b/addon/links.js @@ -1,354 +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 }, + {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 > Process Automation", prod: false }, - { label: "Recycle Bin", link: "/lightning/o/DeleteEvent/home", section: "App Launcher > Custom Link", prod: false } -] \ No newline at end of file + //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} +]; From 69eef688af320558511a37824101f9f1a38b82aa Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Fri, 13 Oct 2023 09:51:06 +0200 Subject: [PATCH 38/54] Fix link for perm set groups --- addon/popup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addon/popup.js b/addon/popup.js index 765bdb00..b2efe1ae 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -862,7 +862,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": [ { @@ -900,7 +900,7 @@ class AllDataBoxShortcut extends React.PureComponent { rec.detail = rec.attributes.type + " • " + rec.UserLicense.Name; break; case "PermissionSet": - rec.link = "/lightning/setup/PermSets/page?address=%2F" + rec.Id; + rec.link = rec.Type === "Group" ? "/lightning/setup/PermSetGroups/page?address=%2F" + rec.PermissionSetGroupId : "/lightning/setup/PermSets/page?address=%2F" + rec.Id; rec.label = rec.Label; rec.name = rec.Name; rec.detail = rec.attributes.type + " • " + rec.Type; From 811245c1e1bb950099408fdff6ff47c65ca7a84a Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Fri, 13 Oct 2023 10:34:35 +0200 Subject: [PATCH 39/54] Feature/enable permission set summary in shortcut (#176) #175 --- CHANGES.md | 3 ++- addon/popup.js | 49 +++++++++++++++++++++++++++++-------------------- docs/how-to.md | 12 ++++++++++++ 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cb47bee9..ece87f3c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## Version 1.20 +- 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)) @@ -14,7 +15,7 @@ - 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" and "Recycle Bin" shortcuts +- 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 listview 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) diff --git a/addon/popup.js b/addon/popup.js index b2efe1ae..03534050 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -884,28 +884,37 @@ class AllDataBoxShortcut extends React.PureComponent { 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 = rec.Type === "Group" ? "/lightning/setup/PermSetGroups/page?address=%2F" + rec.PermissionSetGroupId : "/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); }); diff --git a/docs/how-to.md b/docs/how-to.md index 5ced31c6..0218aeb0 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -134,3 +134,15 @@ Now you can enter the custom links following this convention: 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. From 35ce6b3d39397009e2f7b4b8fcbf3e3e896d9c81 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Mon, 16 Oct 2023 09:52:33 +0200 Subject: [PATCH 40/54] Increase api version for Winter 24 --- CHANGES.md | 1 + addon/inspector.js | 14 +++++++------- sfdx-project.json | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ece87f3c..fa478e35 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## Version 1.20 +- 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 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/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" +} From 172cee2cd71534263629362723233e45a1dd9990 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:13:45 +0200 Subject: [PATCH 41/54] Support .mil domains (#164) --- addon/background.js | 16 +++++++++++++++- addon/button.js | 5 ++--- addon/manifest-template.json | 16 ++++++++++++++-- addon/manifest.json | 16 ++++++++++++++-- addon/popup.js | 4 ++-- 5 files changed, 47 insertions(+), 10 deletions(-) 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..5a9a9d82 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); } @@ -120,7 +119,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/manifest-template.json b/addon/manifest-template.json index 10808124..e201f6f0 100644 --- a/addon/manifest-template.json +++ b/addon/manifest-template.json @@ -17,7 +17,12 @@ "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": [ { @@ -27,7 +32,14 @@ "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"], diff --git a/addon/manifest.json b/addon/manifest.json index c6a949b6..1737080c 100644 --- a/addon/manifest.json +++ b/addon/manifest.json @@ -11,7 +11,12 @@ "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": [ { @@ -21,7 +26,14 @@ "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"], diff --git a/addon/popup.js b/addon/popup.js index 03534050..a14ebec8 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -1571,7 +1571,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]; @@ -1582,7 +1582,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") { From ed8cf2ba6d222eac693dcd5a5078ab7addf8544a Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:20:02 +0200 Subject: [PATCH 42/54] Fix edit layout link (#182) #181 --- CHANGES.md | 1 + addon/inspect.js | 434 ++++++++++++++++++++++++----------------------- 2 files changed, 221 insertions(+), 214 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fa478e35..05e0a089 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## Version 1.20 +- Fix "Edit page layout link" from show all data and use "openLinksInNewTab" property for those links - 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) diff --git a/addon/inspect.js b/addon/inspect.js index 6ace027b..dde954a0 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; } @@ -720,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)"; @@ -791,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, target: linkTarget, 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) { @@ -853,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; } @@ -976,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", @@ -1142,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"), @@ -1188,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 ) ); } @@ -1212,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 @@ -1240,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), @@ -1275,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}) ) )) ); @@ -1319,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, @@ -1342,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", @@ -1361,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)}) ); } } @@ -1378,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(); } @@ -1408,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)}) ); } } @@ -1431,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()) ); } } @@ -1463,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 { @@ -1477,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)") ) ) ); @@ -1513,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)") ) ) ); @@ -1554,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})) ) )) ) @@ -1613,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); }); From fe8c7adcb992313c39c181c6bddd983270df8be1 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Thu, 19 Oct 2023 12:21:20 +0200 Subject: [PATCH 43/54] Add issue link to last change --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 05e0a089..21fa0766 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ ## Version 1.20 -- Fix "Edit page layout link" from show all data and use "openLinksInNewTab" property for those links +- 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) From 40560269e528661dc0511ae0b71a7e503055a333 Mon Sep 17 00:00:00 2001 From: Sarath Addanki <1752644+asknet@users.noreply.github.com> Date: Fri, 20 Oct 2023 02:49:53 -0400 Subject: [PATCH 44/54] [data-export] Input focus on SOQL query field (#153) Problem statement: Whenever I open data export, the query text area isn't focused. It requires another click to set focus and then start typing. Solution: This PR allows to set query textarea as focus on load. Also new to reactjs development. Please review and suggest. --------- Co-authored-by: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> --- CHANGES.md | 1 + addon/data-export.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 21fa0766..f9107438 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ - Update pop-up release note link to github pages - Detect SObject on listview 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 SQOL 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 diff --git a/addon/data-export.js b/addon/data-export.js index 2fbfd0bd..9f0f68fd 100644 --- a/addon/data-export.js +++ b/addon/data-export.js @@ -1082,6 +1082,10 @@ class App extends React.Component { 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(); From 904db2e51519a069c2a4bd9e936f635c52107e36 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Fri, 20 Oct 2023 14:20:37 +0200 Subject: [PATCH 45/54] Remove targetLink variable from inspect --- addon/inspect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/inspect.js b/addon/inspect.js index dde954a0..72d18490 100644 --- a/addon/inspect.js +++ b/addon/inspect.js @@ -795,7 +795,7 @@ class FieldRow extends TableRow { args.set("useToolingApi", "1"); } args.set("recordId", recordId); - return {href: "inspect.html?" + args, target: linkTarget, text: "Show all data (" + sobject.name + ")"}; + return {href: "inspect.html?" + args, text: "Show all data (" + sobject.name + ")"}; }); } else { links = []; From 64a0fbaac53dc0bb058ce36ad882f18713f770b9 Mon Sep 17 00:00:00 2001 From: Antoine Leleu <116869631+AntoineLeleu-Salesforce@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:09:20 +0200 Subject: [PATCH 46/54] [data-export] Add Query Record link (#184) Co-authored-by: Thomas Prouvot --- CHANGES.md | 1 + addon/data-load.css | 8 ++++++++ addon/data-load.js | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f9107438..9ef7e25f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## Version 1.20 +- 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) 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"; From 889aeafbb95a1260de80ae8833beaae8bc0e9f52 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Fri, 27 Oct 2023 12:06:04 +0200 Subject: [PATCH 47/54] Update popup title --- addon/popup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/popup.js b/addon/popup.js index a14ebec8..372b6a2a 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -175,7 +175,7 @@ class App extends React.PureComponent { `}) ) ), - "Salesforce Inspector" + "Salesforce Inspector Reloaded" ) ), h("div", {className: "main"}, From a147aa26ac58f2b8983cf823b0857c4ffc7a1d0e Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Fri, 27 Oct 2023 12:10:33 +0200 Subject: [PATCH 48/54] Update release note with popup title --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 9ef7e25f..84149ebc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## Version 1.20 +- 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) From 4d870dbfcd122114d8ba69a4f1ebbd4a63afc35b Mon Sep 17 00:00:00 2001 From: Oscar <58916926+ogomezba@users.noreply.github.com> Date: Tue, 31 Oct 2023 14:55:21 +0000 Subject: [PATCH 49/54] Add feature "[data-export] Delete button" (#174) Hi! I've created this PR to implement the feature described in #134. Test evidence after opening the test-framework page: Screenshot 2023-10-08 a las 22 27 08 Thank you! --- addon/data-export-test.js | 24 +++++++++++++++ addon/data-export.css | 26 ++++++++++++++++ addon/data-export.js | 62 +++++++++++++++++++++++++++++++++++---- addon/data-import-test.js | 23 +++++++++++++++ addon/data-import.js | 13 ++++++-- addon/test-framework.js | 3 +- 6 files changed, 142 insertions(+), 9 deletions(-) diff --git a/addon/data-export-test.js b/addon/data-export-test.js index 85dd85bd..65c56322 100644 --- a/addon/data-export-test.js +++ b/addon/data-export-test.js @@ -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 f395cfe9..11988edb 100644 --- a/addon/data-export.css +++ b/addon/data-export.css @@ -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 9f0f68fd..e0dc097f 100644 --- a/addon/data-export.js +++ b/addon/data-export.js @@ -221,19 +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() { - let separator = ","; - if (localStorage.getItem("csvSeparator")) { - separator = localStorage.getItem("csvSeparator"); - } + 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. @@ -883,6 +897,7 @@ function RecordTable(vm) { table: [], rowVisibilities: [], colVisibilities: [true], + countOfVisibleRecords: null, isTooling: false, totalSize: -1, addToTable(expRecords) { @@ -900,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; @@ -934,6 +963,7 @@ 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); @@ -1057,6 +1087,11 @@ class App extends React.Component { model.copyAsJson(); model.didUpdate(); } + onDeleteRecords(e) { + let {model} = this.props; + model.deleteRecords(e); + model.didUpdate(); + } onResultsFilterInput(e) { let {model} = this.props; model.setResultsFilter(e.target.value); @@ -1248,6 +1283,7 @@ class App extends React.Component { 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"}, @@ -1285,3 +1321,19 @@ class App extends React.Component { }); } + +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 f2071124..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); + } } /** @@ -1135,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/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; }); From bdc6d6fd7aee81da2866c77760443ce0d2540cc8 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Tue, 31 Oct 2023 16:00:30 +0100 Subject: [PATCH 50/54] Add delete button info --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 84149ebc..9511cad7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## Version 1.20 +- 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) From dfa979aef0823bef4de34d80cb0c5a87728af901 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Mon, 6 Nov 2023 09:51:59 +0100 Subject: [PATCH 51/54] Fix typo --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9511cad7..72a85795 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,9 +22,9 @@ - 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 listview page [feature 121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/121) (idea by [Mehdi Cherfaoui](https://github.com/mehdisfdc)) +- 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 SQOL query text area. [feature 183](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/183) (contribution by [Sarath Addanki](https://github.com/asknet)) +- 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 From 4597c6cf3ae5c4f36de997f8490c1be2d8e70a4c Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:35:55 +0100 Subject: [PATCH 52/54] Feature/log in as experience (#204) #190 --- CHANGES.md | 1 + addon/popup.js | 30 +++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 72a85795..538c2031 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## Version 1.20 +- 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)) diff --git a/addon/popup.js b/addon/popup.js index 372b6a2a..9b4f11e0 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -551,9 +551,9 @@ class AllDataBoxUsers extends React.PureComponent { 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": [ { @@ -573,6 +573,12 @@ class AllDataBoxUsers extends React.PureComponent { //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}); let userDetail = userResult.compositeResponse.find((elm) => elm.httpStatusCode == 200).body.records[0]; + //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 => { + 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); @@ -983,6 +989,10 @@ class UserDetails extends React.PureComponent { return true; } + canLoginAsPortal(user){ + return user.IsActive && user.IsPortalEnabled && user.ContactId; + } + getLoginAsLink(userId) { let {sfHost, contextOrgId, contextPath} = this.props; const retUrl = contextPath || "/"; @@ -990,6 +1000,12 @@ class UserDetails extends React.PureComponent { 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; return "https://" + sfHost + "/lightning/setup/ManageUsers/page?address=%2F" + userId + "%3Fnoredirect%3D1"; @@ -1069,11 +1085,15 @@ class UserDetails extends React.PureComponent { ) )), 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 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, + ) + ) ); } } From 9f3af900b021e88321cb5847cf033d5541bff548 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot Date: Mon, 6 Nov 2023 16:18:44 +0100 Subject: [PATCH 53/54] Handle when user does not have the rights on NetworkMember --- addon/popup.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addon/popup.js b/addon/popup.js index 9b4f11e0..535ffead 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -576,7 +576,9 @@ class AllDataBoxUsers extends React.PureComponent { //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 => { - userDetail.NetworkId = res.records[0].NetworkId; + if (res.records && res.records.length > 0){ + userDetail.NetworkId = res.records[0].NetworkId; + } }); } await this.setState({selectedUser: userDetail}); @@ -990,7 +992,7 @@ class UserDetails extends React.PureComponent { } canLoginAsPortal(user){ - return user.IsActive && user.IsPortalEnabled && user.ContactId; + return user.IsActive && user.NetworkId; } getLoginAsLink(userId) { From 68ccacdcb0d3be6296bff84a495d77fcd0272ef3 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:29:12 +0100 Subject: [PATCH 54/54] [popup] Move popup icon in flow builder (#206) Since Winter 24, some UI changes in flow builder affect the usability of the popup icon. ![image](https://github.com/tprouvot/Salesforce-Inspector-reloaded/assets/35368290/43fe2dde-8620-4bd1-a85b-9601702efaac) There is an improvement which is in progress to let users decide where they want to place their icon. Since this is not finished yet, this PR adds a margin to the button in flow builder to prevent the overlap image --- CHANGES.md | 1 + addon/button.js | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 538c2031..40b682b6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## 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)) diff --git a/addon/button.js b/addon/button.js index 5a9a9d82..b5918ab5 100644 --- a/addon/button.js +++ b/addon/button.js @@ -15,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"); @@ -33,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");