From 5ecc29ec36f7eb146b9394185f3cdfa313d089a0 Mon Sep 17 00:00:00 2001 From: Thomas Prouvot <35368290+tprouvot@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:56:38 +0100 Subject: [PATCH] [popup] Add recently viewed list (#325) ## Describe your changes ## Issue ticket number and link #321 ## Checklist before requesting a review - [x] I have read and understand the [Contributions section](https://github.com/tprouvot/Salesforce-Inspector-reloaded#contributions) - [x] Target branch is releaseCandidate and not master - [x] I have performed a self-review of my code - [x] I ran the [unit tests](https://github.com/tprouvot/Salesforce-Inspector-reloaded#unit-tests) and my PR does not break any tests - [x] I documented the changes I've made on the [CHANGES.md](https://github.com/tprouvot/Salesforce-Inspector-reloaded/blob/master/CHANGES.md) and followed actual conventions - [ ] I added a new section on [how-to.md](https://github.com/tprouvot/Salesforce-Inspector-reloaded/blob/master/docs/how-to.md) (optional) --- CHANGES.md | 1 + addon/popup.js | 70 ++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 20baf2c3..94cfef44 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## Version 1.23 +- Load recently viewed records on popup [feature 321](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/321) - Open documentation when installing the extension [feature 322](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/322) - Add Query Plan to data export [feature 314](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/314) - Align show-all data 'Type' column with Salesforce's 'Data Type' field [issue 312](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/312) by [efcdilascio](https://github.com/efcdilascio) diff --git a/addon/popup.js b/addon/popup.js index 4543f016..e15307cb 100644 --- a/addon/popup.js +++ b/addon/popup.js @@ -967,7 +967,7 @@ class AllDataBoxSObject extends React.PureComponent { 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", sfHost, onDataSelect: this.onDataSelect, sobjectsList, getMatches: this.getMatches, inputSearchDelay: 0, placeholderText: "Record id, id prefix or object name", title: "Click to show recent items", resultRender: this.resultRender}), selectedValue ? h(AllDataSelection, {ref: "allDataSelection", sfHost, showDetailsSupported, selectedValue, linkTarget, recordIdDetails, contextRecordId, isFieldsPresent}) : h("div", {className: "all-data-box-inner empty"}, "No record to display") @@ -1604,9 +1604,9 @@ class AllDataSelection extends React.PureComponent { render() { let {sfHost, showDetailsSupported, contextRecordId, selectedValue, linkTarget, recordIdDetails, isFieldsPresent} = this.props; // Show buttons for the available APIs. - let buttons = Array.from(selectedValue.sobject.availableApis); + let buttons = selectedValue.sobject.availableApis ? Array.from(selectedValue.sobject.availableApis) : []; buttons.sort(); - if (buttons.length == 0) { + if (buttons.length == 0 && !selectedValue.sobject.isRecent) { // If none of the APIs are available, show a button for the regular API, which will partly fail, but still show some useful metadata from the tooling API. buttons.push("noApi"); } @@ -1642,7 +1642,7 @@ class AllDataSelection extends React.PureComponent { h("span", {}, (selectedValue.recordId) ? " / " + selectedValue.recordId : ""), ) ), - selectedValue.sobject.name.indexOf("__") == -1 + selectedValue.sobject.name.indexOf("__") == -1 && selectedValue.sobject.availableApis ? h("tr", {}, h("th", {}, "Doc:"), h("td", {}, @@ -1762,6 +1762,7 @@ class AllDataSearch extends React.PureComponent { this.state = { queryString: "", matchingResults: [], + recentItems: [], queryDelayTimer: null }; this.onAllDataInput = this.onAllDataInput.bind(this); @@ -1815,8 +1816,8 @@ class AllDataSearch extends React.PureComponent { this.setState({queryDelayTimer}); } render() { - let {queryString, matchingResults} = this.state; - let {placeholderText, resultRender} = this.props; + let {queryString, matchingResults, recentItems} = this.state; + let {placeholderText, resultRender, sfHost} = this.props; return ( h("div", {className: "input-with-dropdown"}, h("input", { @@ -1832,7 +1833,10 @@ class AllDataSearch extends React.PureComponent { h(Autocomplete, { ref: "autoComplete", updateInput: this.updateAllDataInput, - matchingResults: resultRender(matchingResults, queryString) + matchingResults: resultRender(matchingResults, queryString), + recentItems: resultRender(recentItems, queryString), + queryString, + sfHost }), 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"}) @@ -1874,7 +1878,32 @@ class Autocomplete extends React.PureComponent { this.setState({showResults: true, selectedIndex: 0, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1}); } handleFocus() { - this.setState({showResults: true, selectedIndex: 0, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1}); + let {recentItems} = this.props; + sfConn.rest("/services/data/v" + apiVersion + "/query/?q=SELECT+Id,Name,Type+FROM+RecentlyViewed+LIMIT+50").then(res => { + res.records.forEach(recentItem => { + recentItems.push({key: recentItem.Id, + value: {recordId: recentItem.Id, sobject: {keyPrefix: recentItem.Id.slice(0, 3), label: recentItem.Type, name: recentItem.Name, isRecent: true}}, + element: [ + h("div", {className: "autocomplete-item-main", key: "main"}, + recentItem.Name, + ), + h("div", {className: "autocomplete-item-sub", key: "sub"}, + h(MarkSubstring, { + text: recentItem.Type, + start: -1, + length: 0 + }), + " • ", + h(MarkSubstring, { + text: recentItem.Id, + start: -1, + length: 0 + }) + ) + ]}); + }); + this.setState({recentItems, showResults: true, selectedIndex: 0, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1}); + }); } handleBlur() { this.setState({showResults: false}); @@ -1930,9 +1959,14 @@ class Autocomplete extends React.PureComponent { onResultsMouseUp() { this.setState({resultsMouseIsDown: false}); } - onResultClick(value) { - this.props.updateInput(value); - this.setState({showResults: false, selectedIndex: 0}); + onResultClick(e, value) { + let {sfHost} = this.props; + if (value.sobject.isRecent){ + window.open("https://" + sfHost + "/" + value.recordId, "_blank"); + } else { + this.props.updateInput(value); + this.setState({showResults: false, selectedIndex: 0}); + } } onResultMouseEnter(index) { this.setState({selectedIndex: index, scrollToSelectedIndex: this.state.scrollToSelectedIndex + 1}); @@ -1965,34 +1999,36 @@ class Autocomplete extends React.PureComponent { } } render() { - let {matchingResults} = this.props; + let {matchingResults, recentItems} = this.props; let { showResults, selectedIndex, scrollTopIndex, itemHeight, - resultsMouseIsDown, + resultsMouseIsDown } = this.state; // For better performance only render the visible autocomplete items + at least one invisible item above and below (if they exist) const RENDERED_ITEMS_COUNT = 11; let firstIndex = 0; - let lastIndex = matchingResults.length - 1; + let autocompleteResults = recentItems.length > 0 ? recentItems : matchingResults; + let lastIndex = autocompleteResults.length - 1; let firstRenderedIndex = Math.max(0, scrollTopIndex - 2); let lastRenderedIndex = Math.min(lastIndex, firstRenderedIndex + RENDERED_ITEMS_COUNT); let topSpace = (firstRenderedIndex - firstIndex) * itemHeight; 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-container", style: {display: (showResults && (autocompleteResults.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) + autocompleteResults.slice(firstRenderedIndex, lastRenderedIndex + 1) .map(({key, value, element}, index) => h("a", { key, className: "autocomplete-item " + (selectedIndex == index + firstRenderedIndex ? "selected" : ""), - onClick: () => this.onResultClick(value), + onClick: (e) => this.onResultClick(e, value), onMouseEnter: () => this.onResultMouseEnter(index + firstRenderedIndex) }, element) ),