diff --git a/.gitignore b/.gitignore index 5104ba2a..a0898479 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ target/ .idea/ yarn.lock .history/ -.sfdx +.sfdx/ venv/ \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index bcfc878b..eccd850b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,20 +1,36 @@ +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/Chrome-Salesforce-inspector/issues/60) (by Bilel Morsli) -* Allow users to update API Version [feature 58](https://github.com/tprouvot/Chrome-Salesforce-inspector/issues/58) -* Add org instance in the popup and a link to Salesforce trust status website [feature 53](https://github.com/tprouvot/Chrome-Salesforce-inspector/issues/53) (by [Camille Guillory](https://github.com/CamilleGuillory) ) -* Fix saved query when it contains ":" [issue 55](https://github.com/tprouvot/Chrome-Salesforce-inspector/issues/55) (by [Victor Garcia](https://github.com/victorgz/) ) +* 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/Chrome-Salesforce-inspector/issues/49) -* Add shortcut tab to access setup quick links [feature 42](https://github.com/tprouvot/Chrome-Salesforce-inspector/issues/42) +* 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 =========== @@ -25,13 +41,13 @@ General ![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/Chrome-Salesforce-inspector/issues/39) +* 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/Chrome-Salesforce-inspector/issues/34) +* 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/Chrome-Salesforce-inspector/issues/30) +* 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/Chrome-Salesforce-inspector/issues/20) +* [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 @@ -39,7 +55,7 @@ Version 1.13 General ------- -* [Automatically remove spaces from column name in import](https://github.com/tprouvot/Chrome-Salesforce-inspector/issues/23) +* [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 @@ -47,13 +63,13 @@ General 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/Chrome-Salesforce-inspector/issues/21) +* [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/Chrome-Salesforce-inspector/issues/10) +* [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. diff --git a/README.md b/README.md index 658eb606..4e63614e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + Salesforce inspector reloaded @@ -50,6 +51,18 @@ 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). + +Contributions +----- + +Contributions are welcomed ! + +To submit a PR, please create a branch from releaseCandidate which is the work in progress next version. +This branch will be merge into master when the new version is published on web store. + +Linting : to assure indentation, formatting and best practices coherence, please install ESLint extension. + + Development ----- diff --git a/addon/data-export.js b/addon/data-export.js index 171ab0da..466a5c97 100644 --- a/addon/data-export.js +++ b/addon/data-export.js @@ -704,7 +704,7 @@ class Model { } }) .concat( - new Enumerable(["FIELDS(ALL)", "FIELDS(STANDARD)", "FIELDS(CUSTOM)", "AVG", "COUNT", "COUNT_DISTINCT", "MIN", "MAX", "SUM", "CALENDAR_MONTH", "CALENDAR_QUARTER", "CALENDAR_YEAR", "DAY_IN_MONTH", "DAY_IN_WEEK", "DAY_IN_YEAR", "DAY_ONLY", "FISCAL_MONTH", "FISCAL_QUARTER", "FISCAL_YEAR", "HOUR_IN_DAY", "WEEK_IN_MONTH", "WEEK_IN_YEAR", "convertTimezone"]) + new Enumerable(["FIELDS(ALL)", "FIELDS(STANDARD)", "FIELDS(CUSTOM)", "AVG", "COUNT", "COUNT_DISTINCT", "MIN", "MAX", "SUM", "CALENDAR_MONTH", "CALENDAR_QUARTER", "CALENDAR_YEAR", "DAY_IN_MONTH", "DAY_IN_WEEK", "DAY_IN_YEAR", "DAY_ONLY", "FISCAL_MONTH", "FISCAL_QUARTER", "FISCAL_YEAR", "HOUR_IN_DAY", "WEEK_IN_MONTH", "WEEK_IN_YEAR", "convertTimezone", "toLabel"]) .filter(fn => fn.toLowerCase().startsWith(searchTerm.toLowerCase())) .map(fn => { if (fn.includes(")")) { //Exception to easily support functions with hardcoded parameter options @@ -1129,7 +1129,7 @@ class App extends React.Component { 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" }, "Removed Saved Query"), + 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" }, @@ -1162,9 +1162,9 @@ class App extends React.Component { h("div", { className: "autocomplete-header" }, h("span", {}, model.autocompleteResults.title), h("div", { className: "flex-right" }, - h("button", { disabled: model.isWorking, onClick: this.onExport, title: "Ctrl+Enter / F5", className: "highlighted" }, "Run Export"), - h("a", { 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", { href: "#", className: model.expandAutocomplete ? "toggle contract" : "toggle expand", onClick: this.onToggleExpand, title: "Show all suggestions or only the first line" }, + h("button", { tabIndex: 1, disabled: model.isWorking, onClick: this.onExport, title: "Ctrl+Enter / F5", className: "highlighted" }, "Run Export"), + h("a", { tabIndex: 2, 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: 3, 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" }) ) @@ -1172,7 +1172,7 @@ class App extends React.Component { ), h("div", { className: "autocomplete-results" }, model.autocompleteResults.results.map(r => - h("div", { className: "autocomplete-result", key: r.value }, h("a", { 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), " ") ) ), ), diff --git a/addon/data-import-test.js b/addon/data-import-test.js index 9640eb57..e74dd9f7 100644 --- a/addon/data-import-test.js +++ b/addon/data-import-test.js @@ -170,6 +170,7 @@ export async function dataImportTest(test) { vm.dataFormat = "csv"; vm.didUpdate(); vm.importAction = "delete"; + vm.importActionSelected = true; vm.didUpdate(); vm.setData("Id,_foo*,__Status\r\n" + records[5].Id + ",foo,Queued\r\n" + records[6].Id + ",foo,Succeeded"); assertEquals({ Queued: 1, Processing: 0, Succeeded: 1, Failed: 0 }, vm.importCounts()); diff --git a/addon/data-import.js b/addon/data-import.js index 68af2877..82805e9f 100644 --- a/addon/data-import.js +++ b/addon/data-import.js @@ -18,6 +18,7 @@ class Model { this.dataError = ""; this.useToolingApi = false; this.dataFormat = "excel"; + this.importActionSelected = false; this.importAction = "create"; this.importActionName = "Insert"; this.importType = "Account"; @@ -85,13 +86,16 @@ class Model { } message() { - return this.dataFormat == "excel" ? "Paste Excel data here" : "Paste CSV data here"; + return "Paste " + this.dataFormat.toUpperCase() + " data here"; } setData(text) { if (this.isWorking()) { return; } + if (this.dataFormat == "json") { + text = this.getDataFromJson(text); + } let separator = this.dataFormat == "excel" ? "\t" : ","; let data; try { @@ -132,9 +136,33 @@ class Model { this.importType = sobj; } //automatically select update if header contains id - if (this.hasIdColumn(header)) { + if (this.hasIdColumn(header) && !this.importActionSelected) { this.importAction = "update"; + this.importActionName = "Update"; + } + } + + getDataFromJson(json) { + json = JSON.parse(json); + let csv; + let fields = ["_"].concat(Object.keys(json[0])); + fields = fields.filter(field => field != "attributes"); + + let sobject = json[0]["attributes"]["type"]; + if (sobject) { + csv = json.map(function (row) { + return fields.map(function (fieldName) { + let value = row[fieldName]; + if (value && typeof value === "string") { + return fieldName == "_" ? '"[' + sobject + ']"' : JSON.stringify(value) + } + }).join(",") + }) + fields = fields.map(str => '"' + str + '"'); + csv.unshift(fields.join(",")); + csv = csv.join("\r\n"); } + return csv; } copyOptions() { @@ -723,6 +751,7 @@ class App extends React.Component { let { model } = this.props; model.importAction = e.target.value; model.importActionName = e.target.options[e.target.selectedIndex].text; + model.importActionSelected = true; model.didUpdate(); } onImportTypeChange(e) { @@ -913,7 +942,9 @@ class App extends React.Component { h("span", { className: "conf-label" }, "Format"), h("label", {}, h("input", { type: "radio", name: "data-input-format", value: "excel", checked: model.dataFormat == "excel", onChange: this.onDataFormatChange, disabled: model.isWorking() }), " ", h("span", {}, "Excel")), " ", - h("label", {}, h("input", { type: "radio", name: "data-input-format", value: "csv", checked: model.dataFormat == "csv", onChange: this.onDataFormatChange, disabled: model.isWorking() }), " ", h("span", {}, "CSV")) + h("label", {}, h("input", { type: "radio", name: "data-input-format", value: "csv", checked: model.dataFormat == "csv", onChange: this.onDataFormatChange, disabled: model.isWorking() }), " ", h("span", {}, "CSV")), + " ", + h("label", {}, h("input", { type: "radio", name: "data-input-format", value: "json", checked: model.dataFormat == "json", onChange: this.onDataFormatChange, disabled: model.isWorking() }), " ", h("span", {}, "JSON")) ), h("div", { className: "conf-line" }, h("label", { className: "conf-input" }, diff --git a/addon/inspect.css b/addon/inspect.css index a29e29e1..d298f38c 100644 --- a/addon/inspect.css +++ b/addon/inspect.css @@ -1,74 +1,97 @@ @font-face { - font-family:'Salesforce Sans'; - src:url(fonts/SalesforceSans-Light.woff2); - font-weight:300 + font-family: 'Salesforce Sans'; + src: url(fonts/SalesforceSans-Light.woff2); + font-weight: 300 } + @font-face { - font-family:'Salesforce Sans'; - src:url(fonts/SalesforceSans-LightItalic.woff2); - font-style:italic; - font-weight:300 + font-family: 'Salesforce Sans'; + src: url(fonts/SalesforceSans-LightItalic.woff2); + font-style: italic; + font-weight: 300 } + @font-face { - font-family:'Salesforce Sans'; - src:url(fonts/SalesforceSans-Regular.woff2); - font-weight:400 + font-family: 'Salesforce Sans'; + src: url(fonts/SalesforceSans-Regular.woff2); + font-weight: 400 } + @font-face { - font-family:'Salesforce Sans'; - src:url(fonts/SalesforceSans-Italic.woff2); - font-style:italic; - font-weight:400 + font-family: 'Salesforce Sans'; + src: url(fonts/SalesforceSans-Italic.woff2); + font-style: italic; + font-weight: 400 } + @font-face { - font-family:'Salesforce Sans'; - src:url(fonts/SalesforceSans-Bold.woff2); - font-weight:700 + font-family: 'Salesforce Sans'; + src: url(fonts/SalesforceSans-Bold.woff2); + font-weight: 700 } + @font-face { - font-family:'Salesforce Sans'; - src:url(fonts/SalesforceSans-BoldItalic.woff2); - font-style:italic; - font-weight:700 + font-family: 'Salesforce Sans'; + src: url(fonts/SalesforceSans-BoldItalic.woff2); + font-style: italic; + font-weight: 700 } + * { box-sizing: border-box; } -html, body, #root, [data-reactroot] { + +html, +body, +#root, +[data-reactroot] { height: 100%; } + [data-reactroot] { display: flex; flex-direction: column; } + html { - font-family: "Salesforce Sans",Arial,sans-serif; + font-family: "Salesforce Sans", Arial, sans-serif; font-size: 100%; line-height: 1.5; background: #fff; color: #16325c; } + body { - font-family: "Salesforce Sans",Arial,sans-serif; + font-family: "Salesforce Sans", Arial, sans-serif; font-size: .8125rem; margin: 0; } + a { color: #0070d2; text-decoration: none; transition: color .1s linear; } + a:active { color: #00396b; } -a:focus, a:hover { + +a:focus, +a:hover { text-decoration: underline; color: #005fb2; } -a:active, a:hover { + +a:active, +a:hover { outline: 0; } -.sf-link, .sf-link:active, .sf-link:focus, .sf-link:hover { + +.sf-link, +.sf-link:active, +.sf-link:focus, +.sf-link:hover { margin-right: 1em; background-color: rgb(6, 28, 63); border-radius: 3px; @@ -80,6 +103,7 @@ a:active, a:hover { padding-right: 1em; margin-right: 20px; } + .sf-link svg { width: 1.8em; height: 1.8em; @@ -91,59 +115,78 @@ a:active, a:hover { border-radius: 2px; fill: white; } + table { width: 100%; border-spacing: 0px; } + .value-text { word-wrap: break-word; white-space: pre-wrap; } + .value-text.value-is-object { color: #990; } + .value-text.value-is-number { color: #909; } + .value-text.value-is-boolean { color: #900; } + .value-text.value-is-boolean-true { color: #099; } + .value-text.value-is-blank { color: #aaa; font-style: italic; } + .value-text.value-is-unknown { color: #aaa; font-style: italic; } + tr.fieldCalculated { font-style: italic; } -tr.fieldHidden, tr.fieldHidden a[href] { + +tr.fieldHidden, +tr.fieldHidden a[href] { color: #777; } + tr:hover { background-color: lightblue; } + th { text-align: left; } + .field-label { white-space: nowrap; } + .field-name { white-space: nowrap; } + .field-column { - max-width: 200px; /* To handle long words. This is not actually 200px but scales with the table. Table layout magic. */ + max-width: 200px; + /* To handle long words. This is not actually 200px but scales with the table. Table layout magic. */ } + .field-column textarea { width: calc(100% - 20px); resize: vertical; } + .undo-button { background-color: #eee; border-radius: 3px; @@ -157,13 +200,16 @@ th { vertical-align: top; margin-top: 5px; } + .undo-button[hidden] { display: none; } + .field-actions { text-align: right; white-space: nowrap; } + .actions-button { width: 1.25rem; height: 1.25rem; @@ -181,40 +227,46 @@ th { white-space: normal; cursor: pointer; } + .actions-button:hover, .actions-button:active, .actions-button:focus { color: #005fb2; } + .actions-button:focus { outline: 0; box-shadow: 0 0 3px #0070d2; } + .actions-icon { width: .75rem; height: .75rem; overflow: hidden; fill: #b0adab; } + th[tabindex] { text-decoration: underline; cursor: pointer; color: darkblue; } + .column-filter-box { width: 100%; } + #fieldDetailsView { position: fixed; top: 0; right: 0; bottom: 0; left: 0; - background: rgba(0,0,0,0.8); + background: rgba(0, 0, 0, 0.8); z-index: 99999; } -#fieldDetailsView > div.container { +#fieldDetailsView>div.container { width: 600px; height: 500px; position: relative; @@ -222,11 +274,13 @@ th[tabindex] { border-radius: 10px; background: #fff; } -#fieldDetailsView > div.container > div.mainContent { + +#fieldDetailsView>div.container>div.mainContent { overflow: auto; height: 470px; padding: 5px 20px 13px 20px; } + .closeLnk { background: #606061; color: #FFFFFF; @@ -241,17 +295,21 @@ th[tabindex] { border-radius: 12px; box-shadow: 1px 1px 3px #000; } + .closeLnk:hover { background: #00d9ff; } + #fieldDetailsView td { white-space: pre; } + .filter-box { white-space: nowrap; margin: 0 1rem; position: relative; } + .filter-input { width: 20em; padding: 0 3rem; @@ -259,26 +317,30 @@ th[tabindex] { color: #16325c; border: 1px solid #d8dde6; border-radius: .25rem; - transition: border .1s linear,background-color .1s linear; + transition: border .1s linear, background-color .1s linear; display: inline-block; line-height: 1.875rem; min-height: calc(1.875rem + (1px * 2)); } -.filter-input:active, .filter-input:focus { + +.filter-input:active, +.filter-input:focus { outline: 0; border-color: #1589ee; background-color: #fff; box-shadow: 0 0 3px #0070D2; } + .filter-icon { - left: 1.25rem; - width: 1rem; - height: 1rem; - position: absolute; - top: 50%; - margin-top: -.5rem; - fill: #9faab5; + left: 1.25rem; + width: 1rem; + height: 1rem; + position: absolute; + top: 50%; + margin-top: -.5rem; + fill: #9faab5; } + .filter-clear { right: 1.25rem; width: 1rem; @@ -295,13 +357,15 @@ th[tabindex] { border-radius: .25rem; font-size: .75rem; text-decoration: none; - transition: color 50ms linear,background-color 50ms linear; + transition: color 50ms linear, background-color 50ms linear; } + .filter-clear-icon { width: .875rem; height: .875rem; fill: currentColor; } + .error-message { font-size: 1.2em; font-weight: bold; @@ -311,11 +375,13 @@ th[tabindex] { border: 1px solid red; border-radius: 7px; } + +/* #spinner { position: absolute; left: -15px; top: -15px; -} +}*/ .object-bar { background: #f7f9fb; width: 100%; @@ -324,10 +390,12 @@ th[tabindex] { padding: 0 1.5rem; flex-wrap: wrap; } + .object-tab { position: relative; align-self: flex-end; } + .object-tab a { color: #54698d; padding: 0 .75rem; @@ -336,19 +404,27 @@ th[tabindex] { line-height: 3rem; display: inline-block; } -.object-tab a , .object-tab a:focus, .object-tab a:active, .object-tab a:hover { + +.object-tab a, +.object-tab a:focus, +.object-tab a:active, +.object-tab a:hover { text-decoration: none; } + .object-tab:hover::after, .object-tab.active-tab::after { background-color: #0070d2; } + .object-tab.active-tab a { color: #16325c; } + .object-tab * { vertical-align: middle; } + .object-tab::after { display: block; content: ''; @@ -359,18 +435,22 @@ th[tabindex] { height: 2px; align-self: stretch; } + .object-tab .tab-with-icon { padding-right: .375rem; } + .object-tab .button-icon-link { padding-left: .375rem; } + .button-icon { width: .875rem; height: .875rem; fill: currentColor; vertical-align: middle; } + .object-name { flex: 1 1 0; margin: 0 1em; @@ -381,6 +461,7 @@ th[tabindex] { overflow: hidden; position: relative; } + .button { background-color: #fff; color: #0070d2; @@ -394,77 +475,101 @@ th[tabindex] { font: unset; font-size: .75rem; line-height: 1.875rem; - transition: color 50ms linear,background-color 50ms linear; + transition: color 50ms linear, background-color 50ms linear; } + .button[disabled] { cursor: default; - color: #dddbda; - background-color: #fff; + background-color: var(--slds-g-color-neutral-base-80, #c9c7c5); + border-color: var(--slds-g-color-neutral-base-80, #c9c7c5); + color: var(--slds-g-color-neutral-base-100, #fff); } + .object-actions { position: relative; } + .object-actions .button:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: none; } + .object-actions .button:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; } + .button:active:not([disabled]) { background-color: #eef1f6; color: #00396b; } + .button:focus { outline: 0; box-shadow: 0 0 3px #0070D2; } -.button:focus:not([disabled]), .button:hover:not([disabled]) { + +.button:focus:not([disabled]), +.button:hover:not([disabled]) { background-color: #f4f6f9; color: #005fb2; } -.button:active, .button:focus, .button:hover, .button:visited { + +.button:active, +.button:focus, +.button:hover, +.button:visited { text-decoration: none; } + .button-brand { background-color: #0070d2; border-color: #0070d2; color: #fff; } -.button-brand:focus:not([disabled]), .button-brand:hover:not([disabled]) { + +.button-brand:focus:not([disabled]), +.button-brand:hover:not([disabled]) { background-color: #005fb2; border-color: #005fb2; color: #fff; } + .button-brand:active:not([disabled]) { background-color: #005fb2; border-color: #005fb2; color: #fff; } + .button-destructive { background-color: #c23934; border-color: #c23934; color: #fff; } -.button-destructive:focus:not([disabled]), .button-destructive:hover:not([disabled]) { + +.button-destructive:focus:not([disabled]), +.button-destructive:hover:not([disabled]) { background-color: #a61a14; color: #fff; } + .button-destructive:active:not([disabled]) { background-color: #870500; border-color: #870500; color: #fff; } + .column-button-outer { position: relative; display: inline-block; } + .column-popup-inner { overflow: auto; max-height: 500px; } + .column-popup { position: absolute; z-index: 7000; @@ -477,9 +582,10 @@ th[tabindex] { padding: .25rem 0; font-size: .75rem; background: #fff; - box-shadow: 0 2px 3px 0 rgba(0,0,0,.16); + box-shadow: 0 2px 3px 0 rgba(0, 0, 0, .16); margin-top: .5rem; } + .column-popup::before { width: 1rem; height: 1rem; @@ -491,8 +597,9 @@ th[tabindex] { top: -.5rem; margin-left: -.5rem; } + .column-popup::after { - box-shadow: -1px -1px 0 0 rgba(0,0,0,.16); + box-shadow: -1px -1px 0 0 rgba(0, 0, 0, .16); z-index: -1; width: 1rem; height: 1rem; @@ -504,10 +611,12 @@ th[tabindex] { top: -.5rem; margin-left: -.5rem; } + .column-popup label.menu-item { display: block; padding: .2rem .75rem; } + .column-popup span.menu-item { display: block; padding: .5rem .75rem; @@ -517,36 +626,45 @@ th[tabindex] { text-transform: uppercase; letter-spacing: .0625rem; } + .column-popup label.menu-item:hover { background-color: #f4f6f9; } + .body { padding: 8px; overflow: auto; flex: 1 1 0; } + @keyframes spin { 100% { transform: rotate(360deg); } } + .body.empty { animation: spin 4s linear infinite; } + .edit-bar { text-align: center; padding: 1rem 0; } + .edit-bar .button { margin: 0 .25rem; } + .child-actions { text-align: right; white-space: nowrap; } + .pop-menu-container { position: relative; } + .pop-menu { z-index: 10; position: absolute; @@ -556,12 +674,14 @@ th[tabindex] { margin-left: 5px; margin-top: -5px; } + .pop-menu a { display: block; } + .object-actions .pop-menu, .field-actions .pop-menu, .child-actions .pop-menu { right: 0; text-align: right; -} +} \ No newline at end of file diff --git a/addon/inspect.html b/addon/inspect.html index 6fc3a6a2..2f89076d 100644 --- a/addon/inspect.html +++ b/addon/inspect.html @@ -1,16 +1,20 @@ - - - ... - - - - -
- - - - - + + + + ... + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/addon/inspect.js b/addon/inspect.js index 6ca0508d..ac6da7c1 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) { @@ -105,13 +105,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() { @@ -142,7 +142,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)); @@ -152,7 +152,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); @@ -169,7 +169,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); @@ -275,7 +275,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) => { @@ -407,8 +407,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) { @@ -462,20 +462,20 @@ 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: col == "name" ? "field-name" - : col == "label" ? "field-label" - : "field-column", + : col == "label" ? "field-label" + : "field-column", reactElement: col == "value" ? FieldValueCell - : col == "type" ? FieldTypeCell - : DefaultCell, + : col == "type" ? FieldTypeCell + : DefaultCell, columnFilter: "" }; } @@ -507,10 +507,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", reactElement: col == "object" ? ChildObjectCell : DefaultCell, columnFilter: "" @@ -559,7 +559,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.", {}); @@ -572,21 +572,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; } @@ -696,7 +696,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)"; @@ -767,12 +767,13 @@ class FieldRow extends TableRow { args.set("useToolingApi", "1"); } args.set("recordId", recordId); - return {href: "inspect.html?" + args, text: "Show all data (" + sobject.name + ")"}; + return { href: "inspect.html?" + args, text: "Show all data (" + sobject.name + ")" }; }); } else { links = []; } - links.push({href: this.idLink(), text: "View in Salesforce"}); + links.push({ href: this.idLink(), text: "View in Salesforce" }); + links.push({ href: "#", text: "Copy Id", className: "copy-id", id: this.dataTypedValue }); this.recordIdPop = links; } showReferenceUrl(type) { @@ -828,7 +829,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; } @@ -949,133 +950,147 @@ 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() { - let {model} = this.props; + onDoUpdate(e) { + e.currentTarget.disabled = true; + let { model } = this.props; model.doUpdate(); model.didUpdate(); + e.currentTarget.disabled = false; } - onDoDelete() { - let {model} = this.props; + onDoDelete(e) { + e.currentTarget.disabled = true; + let { model } = this.props; model.doDelete(); model.didUpdate(); + e.currentTarget.disabled = false; } - onDoCreate() { - let {model} = this.props; + onDoCreate(e) { + e.currentTarget.disabled = true; + let { model } = this.props; model.doCreate(); model.didUpdate(); + e.currentTarget.disabled = false; } - onDoSave() { - let {model} = this.props; + onDoSave(e) { + e.currentTarget.disabled = true; + 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(); } render() { - let {model} = this.props; + let { model } = this.props; document.title = model.title(); return ( h("div", {}, - h("div", {className: "object-bar"}, - h("img", {id: "spinner", src: "data:image/gif;base64,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", hidden: model.spinnerCount == 0}), - 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", { 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" }) ), " 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 })) ] }) ), - 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("h1", {className: "object-name"}, - h("span", {className: "quick-select"}, model.objectName()), + h("h1", { className: "object-name" }, + h("span", { className: "quick-select" }, model.objectName()), " ", model.recordHeading() ), - 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", @@ -1094,59 +1109,61 @@ 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(), 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() }, "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)") ) ) ), - h("div", {className: "body " + (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: "body " + (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, { rowList: model.fieldRows, - actionsColumn: {className: "field-actions", reactElement: FieldActionsCell}, + actionsColumn: { className: "field-actions", reactElement: FieldActionsCell }, classNameForRow: row => (row.fieldIsCalculated() ? "fieldCalculated " : "") + (row.fieldIsHidden() ? "fieldHidden " : "") }) : null, model.useTab == "all" ? h("hr", {}) : null, model.useTab == "all" || model.useTab == "childs" ? h(RowTable, { rowList: model.childRows, - actionsColumn: {className: "child-actions", reactElement: ChildActionsCell}, + actionsColumn: { className: "child-actions", reactElement: ChildActionsCell }, classNameForRow: () => "" }) : null ), - model.editMode != null && (model.useTab == "all" || model.useTab == "fields") ? h("span", {className: "edit-bar"}, + model.editMode != null && (model.useTab == "all" || model.useTab == "fields") ? 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"), h("button", { + 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 ) ); } @@ -1159,21 +1176,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 @@ -1187,13 +1204,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), @@ -1207,29 +1224,29 @@ class ColumnVisibiltyToggle extends React.Component { class RowTable extends React.Component { 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}, "") + h("th", { className: actionsColumn.className }, "") ), 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}) + h("th", { className: actionsColumn.className }) ) : 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, {row}) + h(actionsColumn.reactElement, { row }) ) )) ); @@ -1242,12 +1259,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, @@ -1265,13 +1282,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}, + let { col } = this.props; + return h("th", { className: col.className }, h("input", { placeholder: "Filter", className: "column-filter-box", @@ -1284,9 +1301,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) }) ); } } @@ -1298,48 +1315,55 @@ class FieldValueCell extends React.Component { this.onDataEditValueInput = this.onDataEditValueInput.bind(this); this.onCancelEdit = this.onCancelEdit.bind(this); this.onRecordIdClick = this.onRecordIdClick.bind(this); + 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(); } + onLinkClick(e) { + if (e.target.className?.includes("copy-id")) { + navigator.clipboard.writeText(e.target.id); + this.onRecordIdClick(e); + } + } 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}, 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) }) ); } } @@ -1347,21 +1371,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()) ); } } @@ -1379,10 +1403,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 { @@ -1393,28 +1417,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} = this.props; - return h("td", {className: "field-actions"}, - 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 } = this.props; + return h("td", { className: "field-actions" }, + 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)") ) ) ); @@ -1429,29 +1453,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} = this.props; - return h("td", {className: "child-actions"}, - 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 } = this.props; + return h("td", { className: "child-actions" }, + 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)") ) ) ); @@ -1470,41 +1494,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 })) ) )) ) @@ -1529,9 +1553,9 @@ class DetailsBox extends React.Component { model.recordId = args.get("recordId"); model.startLoading(); model.reactCallback = cb => { - ReactDOM.render(h(App, {model}), root, cb); + ReactDOM.render(h(App, { model }), root, cb); }; - ReactDOM.render(h(App, {model}), root); + ReactDOM.render(h(App, { model }), root); }); diff --git a/addon/limits.js b/addon/limits.js index 2b3bcc62..c0b592fc 100644 --- a/addon/limits.js +++ b/addon/limits.js @@ -1,5 +1,5 @@ /* global React ReactDOM */ -import {sfConn, apiVersion} from "./inspector.js"; +import { sfConn, apiVersion } from "./inspector.js"; /* global initButton */ class Model { @@ -76,19 +76,19 @@ class LimitData extends React.Component { h("div", { className: "gauge" }, - h("div", { - className: "meter", - ref: "meter" - }, - "" - ), - h("div", { - className: "meter-value-container" - }, - h("div", { - className: "meter-value" - }, Math.round((1 - (this.props.remaining / this.props.max)) * 100) + "%") - ) + h("div", { + className: "meter", + ref: "meter" + }, + "" + ), + h("div", { + className: "meter-value-container" + }, + h("div", { + className: "meter-value" + }, Math.round((1 - this.divide(this.props.remaining, this.props.max)) * 100) + "%") + ) ), h("figcaption", {}, this.props.label, h("div", {}, (this.props.max - this.props.remaining).toLocaleString() + " of " + (this.props.max).toLocaleString() + " consumed", @@ -98,9 +98,12 @@ class LimitData extends React.Component { ) ); } + divide(a, b) { + return (a / b) ? (a / b) : 0; + } componentDidMount() { // Animate gauge to relevant value - let targetDegree = (this.props.max == 0) ? "180deg" : ((1 - (this.props.remaining / this.props.max)) * 180) + "deg"; //180deg = 100%, 0deg = 0% + let targetDegree = (this.props.max == 0) ? "180deg" : ((1 - this.divide(this.props.remaining, this.props.max)) * 180) + "deg"; //180deg = 100%, 0deg = 0% this.refs.meter.animate([{ transform: "rotate(0deg)" }, { @@ -121,31 +124,31 @@ class App extends React.Component { h("div", { className: "object-bar" }, - h("img", { - id: "spinner", - src: "data:image/gif;base64,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", - hidden: vm.spinnerCount == 0 - }), - h("a", { - href: vm.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("img", { + id: "spinner", + src: "data:image/gif;base64,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", + hidden: vm.spinnerCount == 0 + }), + h("a", { + href: vm.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("div", { className: "body" }, - vm.allLimitData.map(limitData => - h(LimitData, limitData) - ) + vm.allLimitData.map(limitData => + h(LimitData, limitData) + ) ) ) ); diff --git a/addon/manifest-template.json b/addon/manifest-template.json index 680abe77..fea4aaae 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.16", + "version": "1.17", "icons": { "128": "icon128.png" }, diff --git a/addon/manifest.json b/addon/manifest.json index a0a22b99..1fb7c2e6 100644 --- a/addon/manifest.json +++ b/addon/manifest.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.16", + "version": "1.17", "icons": { "128": "icon128.png" }, diff --git a/addon/popup.js b/addon/popup.js index 227cf8bd..88c48ce4 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -131,7 +131,8 @@ class App extends React.PureComponent { let orgInstance = this.getOrgInstance(sfHost); let hostArg = new URLSearchParams(); hostArg.set("host", sfHost); - let linkTarget = inDevConsole ? "_blank" : "_top"; + let linkInNewTab = localStorage.getItem("openLinksInNewTab"); + let linkTarget = inDevConsole || linkInNewTab ? "_blank" : "_top"; return ( h("div", {}, h("div", { className: "header" }, @@ -167,7 +168,7 @@ class App extends React.PureComponent { h("div", { className: "footer" }, h("div", { className: "meta" }, h("div", { className: "version" }, - h("a", { href: "https://github.com/tprouvot/Chrome-Salesforce-inspector/blob/master/CHANGES.md", title: "Release note" }, "v" + addonVersion), + 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://status.salesforce.com/instances/" + orgInstance, title: "Instance status", target: linkTarget }, orgInstance), " / ", @@ -179,7 +180,7 @@ class App extends React.PureComponent { value: apiVersionInput.split(".0")[0] }), ), - h("div", { className: "tip" }, "[ctrl+alt+i] to open"), + h("div", { className: "tip" }, navigator.userAgentData.platform.indexOf("mac") > -1 ? "[ctrl+option+i]" : "[ctrl+alt+i]" + " to open"), h("a", { className: "about", href: "https://github.com/tprouvot/Chrome-Salesforce-inspector", target: linkTarget }, "About"), h("a", { className: "about", href: "https://github.com/tprouvot/Chrome-Salesforce-inspector/wiki", target: linkTarget }, "Wiki") ), @@ -282,7 +283,7 @@ class AllDataBox extends React.PureComponent { loadSobjects() { let entityMap = new Map(); - function addEntity({ name, label, keyPrefix, durableId }, 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) { @@ -299,6 +300,7 @@ class AllDataBox extends React.PureComponent { label, keyPrefix, durableId, + isCustomSetting, availableKeyPrefix: null, }; entityMap.set(name, entity); @@ -322,14 +324,15 @@ class AllDataBox extends React.PureComponent { } function getEntityDefinitions(bucket) { - let query = "select QualifiedApiName, Label, KeyPrefix, DurableId from EntityDefinition" + 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 + durableId: record.DurableId, + isCustomSetting: record.IsCustomSetting }, null); } }).catch(err => { @@ -986,9 +989,13 @@ class AllDataSelection extends React.PureComponent { /** * Optimistically generate lightning setup uri for the provided object api name. */ - getObjectSetupLink(sobjectName, durableId) { + getObjectSetupLink(sobjectName, durableId, isCustomSetting) { if (sobjectName.endsWith("__mdt")) { return this.getCustomMetadataLink(durableId); + } else if (isCustomSetting) { + return "https://" + this.props.sfHost + "/lightning/setup/CustomSettings/page?address=%2F" + durableId + "?setupid=CustomSettings"; + } else if (sobjectName.endsWith("__c")) { + return "https://" + this.props.sfHost + "/lightning/setup/ObjectManager/" + durableId + "/Details/view"; } else { return "https://" + this.props.sfHost + "/lightning/setup/ObjectManager/" + sobjectName + "/Details/view"; } @@ -996,22 +1003,34 @@ class AllDataSelection extends React.PureComponent { getCustomMetadataLink(durableId) { return "https://" + this.props.sfHost + "/lightning/setup/CustomMetadata/page?address=%2F" + durableId + "%3Fsetupid%3DCustomMetadata"; } - getObjectFieldsSetupLink(sobjectName, durableId) { + getObjectFieldsSetupLink(sobjectName, durableId, isCustomSetting) { if (sobjectName.endsWith("__mdt")) { return this.getCustomMetadataLink(durableId); + } else if (isCustomSetting) { + return "https://" + this.props.sfHost + "/lightning/setup/CustomSettings/page?address=%2F" + durableId + "?setupid=CustomSettings"; + + } else if (sobjectName.endsWith("__c")) { + return "https://" + this.props.sfHost + "/lightning/setup/ObjectManager/" + durableId + "/FieldsAndRelationships/view"; } else { return "https://" + this.props.sfHost + "/lightning/setup/ObjectManager/" + sobjectName + "/FieldsAndRelationships/view"; } } - getObjectListLink(sobjectName, keyPrefix) { + getObjectListLink(sobjectName, keyPrefix, isCustomSetting) { if (sobjectName.endsWith("__mdt")) { return "https://" + this.props.sfHost + "/lightning/setup/CustomMetadata/page?address=%2F" + keyPrefix; + } else if (isCustomSetting) { + return "https://" + this.props.sfHost + "/lightning/setup/CustomSettings/page?address=%2Fsetup%2Fui%2FlistCustomSettingsData.apexp?id=" + keyPrefix; + } else { return "https://" + this.props.sfHost + "/lightning/o/" + sobjectName + "/list"; } } - getRecordTypesLink(sfHost, sobjectName) { - return "https://" + sfHost + "/lightning/setup/ObjectManager/" + sobjectName + "/RecordTypes/view"; + getRecordTypesLink(sfHost, sobjectName, durableId) { + if (sobjectName.endsWith("__c")) { + return "https://" + sfHost + "/lightning/setup/ObjectManager/" + durableId + "/RecordTypes/view"; + } else { + return "https://" + sfHost + "/lightning/setup/ObjectManager/" + sobjectName + "/RecordTypes/view"; + } } render() { let { sfHost, showDetailsSupported, contextRecordId, selectedValue, linkTarget, recordIdDetails } = this.props; @@ -1030,15 +1049,15 @@ class AllDataSelection extends React.PureComponent { h("tr", {}, h("th", {}, "Name:"), h("td", {}, - h("a", { href: this.getObjectSetupLink(selectedValue.sobject.name, selectedValue.sobject.durableId), 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), target: linkTarget }, "Fields / "), - h("a", { href: this.getRecordTypesLink(sfHost, selectedValue.sobject.name), target: linkTarget }, "Record Types / "), - h("a", { href: this.getObjectListLink(selectedValue.sobject.name, selectedValue.sobject.keyPrefix), target: linkTarget }, "Object List") + h("a", { href: this.getObjectFieldsSetupLink(selectedValue.sobject.name, selectedValue.sobject.durableId, selectedValue.sobject.isCustomSetting), target: linkTarget }, "Fields / "), + h("a", { href: this.getRecordTypesLink(sfHost, selectedValue.sobject.name, selectedValue.sobject.durableId), target: linkTarget }, "Record Types / "), + h("a", { href: this.getObjectListLink(selectedValue.sobject.name, selectedValue.sobject.keyPrefix, selectedValue.sobject.isCustomSetting), target: linkTarget }, "Object List") ), ), h("tr", {},