Modify the QuickBooks Online Interface with a Chrome Extension
Hacking the QuickBooks Online UI
On a recent client project I had been regularly monitoring the QBO sandbox audit log to make sure we were getting the proper results before going live. To validate these results I thought of providing a CSV file of the audit log table to my client's accounting staff.
At first I was just cutting and pasting some JavaScript functions into the Chrome developer tools but that got old and time consuming so instead I just built a full-fledged extension, remarkably called "QBO Audit Log to CSV".
I did not officially release the extension on the Chrome Web Store so if you want to use it you'll have to follow the instructions in the next section.
Getting started
- You are going to need the latest Node version.
- In a separate terminal run
gulp watch
. - Open up Chrome and go to
chrome://extensions
. - Check "Developer mode:" ✔︎
- Click "Load Unpacked Extensions"
- And navigate to the
/app
directory e.g.~/chrome-ext/qbo-audit-log-to-csv/app
- Now you can open
qbo.intuit.com/app/auditlog
orsandbox.qbo.intuit.com/app/auditlog
and you'll see the 2 links from Fig. 2.
$ mkdir ~/chrome-ext $ git clone git@github.com:minimul/qbo-audit-log-to-csv.git $ cd qbo-audit-log-to-csv $ npm install
Modifying the UI
The 3 main scripts that do all the work are: app/scripts.babel/background.js
; app/scripts.babel/contentscript.js
; and /manifest.json
. I'll briefly discuss these files and some of things to take note of.
The gulp watch
command will automatically compile the files in the /app/scripts.babel
directory to the /app
directory whenever there is a file modification.
The manifest.json file
This is your config and setup file. Here is the manifest.json
file for the QBO Audit Log To CSV extension.
{ "name": "QboAuditLog to CSV", "version": "1.0.0", "manifest_version": 2, "description": "QboAuditLog to CSV", "icons": { "128": "images/128-icon.png" }, "default_locale": "en", "background": { "scripts": [ "scripts/chromereload.js", "scripts/background.js" ] }, "permissions": [ "tabs", "webNavigation", "https://*.intuit.com/*/*" ], "content_scripts": [ { "matches": [ "https://*.intuit.com/*/*" ], "js": [ "scripts/contentscript.js" ], "run_at": "document_end", "all_frames": false } ] }
In this file you determine which scripts are "background" and "content_scripts". You set permissions and determine what URL pattern the extension will run against. As you can see only permissions for "tabs" and "webNavigation" were needed.
The background.js file
The background.js
file is a bit hard to explain so here is its definition according to the docs
❝
A common need for extensions is to have a single long-running script to manage some task or state.
Background pages to the rescue.
As the architecture overview explains, the background page is an HTML page that runs in the extension process.
It exists for the lifetime of your extension, and only one instance of it at a time is active. (Exception: if your extension uses incognito "split" mode, a second instance is created for incognito windows.)
❞
Let's take a look at the extension's background.js
.
'use strict';
console.log('QboAudit Background Script');
chrome.runtime.onInstalled.addListener(details => {
console.log('QboAudit previousVersion', details.previousVersion);
})
chrome.webNavigation.onHistoryStateUpdated.addListener( (details) => {
console.log('QboAudit Page uses History API and we heard a pushSate/replaceState.')
if(typeof chrome._LAST_RUN === 'undefined' || notRunWithinTheLastSecond(details.timeStamp)){
chrome._LAST_RUN = details.timeStamp
chrome.tabs.getSelected(null, function (tab) {
if(tab.url.match(/.*\/app\/auditlog1/)){
chrome.tabs.sendRequest(tab.id, 'runQboAuditLog')
}
})
}
})
const notRunWithinTheLastSecond = (dt) => {
const diff = dt - chrome._LAST_RUN
if (diff < 1000){
return false
} else {
return true
}
}
Of note is the highlighted line above. Naturally, you only want to run the extension within the QBO audit log, which has an ending URL like this: *.intuit.com/app/auditlog
. To accomplish this I have a basic wild card set in the manifest.json
but I couldn't dial in the permissions any further then what is presently set (see manifest.json
above) without getting errors, therefore, I had to put the rest of the solution within the background.js
file.
What's with all of the chrome._LAST_RUN
stuff?
Since QuickBooks Online is a single page app you'll need to monitor the push or history state to determine if the user is navigating around. There is a "bug" that duplicate push state events will get kicked off almost simultaneously. Therefore, the rest of the code in background.js
deals with not running that duplicate event.
The contentscript.js file
The contentscript.js
is the one script that will actually get run inside the page in the traditional sense, like as if you are loading it in via the page's <script></script>
tag.
This file contains all of the extension's logic and is called by the background script when the conditions are right.
// background.js excerpt that calls the contentscript.js when the page's URL // suffix of /app/auditlog is navigated to. if(tab.url.match(/.*\/app\/auditlog/)){ chrome.tabs.sendRequest(tab.id, 'runQboAuditLog') } // contentscript.js chrome.extension.onRequest.addListener((request, sender, sendResponse) => { if (request == 'runQboAuditLog') new QboAuditLog() });
Here is the full contentscript.js
file:
'use strict'; class QboAuditLog { constructor(){ [this.date, this.time] = new Date().toLocaleString('en-US').split(', '); this.init() } init() { const domCheck = setInterval( () => { console.log('QboAudit loop running'); if(document.querySelector('.filterBarContainer')){ clearInterval(domCheck) this.createLinks() } else { console.log('QboAudit miss'); } }, 2000) } createLinks(){ this.csvLink() this.removeDupsLink() } csvLink(){ const link = document.createElement('a') link.innerHTML = 'CSV' link.addEventListener('click', () => { this.triggerDownload() return false }) return document.querySelector('.filterBarContainer').appendChild(link) } removeDupsLink(){ const link = document.createElement('a') link.innerHTML = 'Remove Dups' link.style.paddingLeft = '10px' link.addEventListener('click', () => { this.removeDups() return false }) return document.querySelector('.filterBarContainer').appendChild(link) } download(csv, filename) { const csvFile = new Blob([csv], {type: 'text/csv'}) const downloadLink = document.createElement('a') downloadLink.download = filename downloadLink.href = window.URL.createObjectURL(csvFile) downloadLink.style.display = 'none' document.body.appendChild(downloadLink) console.dir(downloadLink); downloadLink.dispatchEvent(new MouseEvent('click')) document.body.removeChild(downloadLink) } triggerDownload() { let csv = [] const rows = this.getRows() for (let i = 0; i < rows.length; i++) { let row = [], cols = rows[i].querySelectorAll('td') for (let j = 0; j < cols.length; j++){ row.push(`"${cols[j].innerText}"`) } csv.push(row.join(',')) } return this.download(csv.join('\n'), `daily-report-${this.date}.csv`) } getRows(){ return document.querySelectorAll('.dgrid-content table tr') } removeDups() { const sel = '.dgrid-column-4'; const els = document.querySelectorAll(sel) els.forEach((item) => { const name = item.innerText let seen = {} this.getRows().forEach((tr) => { const txt = tr.querySelector(sel).innerText //console.log(`QboAudit ${txt}`); if(txt === ''){ tr.closest('div').remove() return } if (name === txt){ if(seen[txt]){ tr.closest('div').remove() } else { seen[txt] = true } } }) }) } } chrome.extension.onRequest.addListener((request, sender, sendResponse) => { if (request == 'runQboAuditLog') new QboAuditLog() });
Pay special attention to the init()
method. I had to create a loop to check when the DOM was ready to properly insert the links. I tried various things but only constructing this manual loop worked.
Also, take a look at the download()
method. It uses the Blob
object but I needed to dispatch a mouseclick event as seen in this line downloadLink.dispatchEvent(new MouseEvent('click'))
to actually get the download to work.
Conclusion
Be sure to check out the screencast as I have much more commentary including some useful information on debugging and on the automatic chromereload.js
code that comes with the Yeoman Chrome Generator, which is what I used to generate the Chrome extension scaffolding. Lastly, don't forget all the code is in its own Github repo.
- Pushed on 10/25/2017 by Christian
- QuickBooks Integration Consulting