Moving from localStorage to chrome.storage - google-chrome-extension

I would like to use chrome.storage API to save the settings of my users instead of localStorage in my Chrome extension.
Currently my options.js (with localStorage and JSON) file looks like this:
$(function(){ //jQuery Ready
// INIT
$("#notifysav").hide();
// STORAGE
if(localStorage['options']){
var o = JSON.parse(localStorage['options']);
$("#option1").attr('checked', o.option1);
$("#option2").attr('checked', o.option2);
.... [list of options]
}
// Save Button Click event
$("#save").live('click', function(){
localStorage['options'] = JSON.stringify({
"option1":$("#option1").attr('checked'),
"option2":$("#option2").attr('checked'),
.... [list of options]
});
// notification
$("#notifysav").fadeIn("slow").delay(2000).fadeOut("slow");
// reload to apply changes
chrome.extension.sendRequest({action: 'reload'});
});
});// #jQuery ready
My question is how to convert my current code to use the chrome.storage API. From what I understand, I would apply those changes:
$(function(){
// INIT
var storage = chrome.storage.sync;
$("#notifysav").hide();
// Load Options
loadOptions();
// Save Button Click Event
$("#save").live('click',function(){ saveOptions(); });
function loadOptions() {
storage.get( /* Something */ )
}
function saveOptions() {
var option1 = $("#option1").attr('checked');
var option2 = $("#option2").attr('checked');
storage.set({"option1":option1,"option2":option2}, function() {
// Notification
$("#notifysav").fadeIn("slow").delay(2000).fadeOut("slow");
// Reload Event to apply changes
chrome.extension.sendRequest({action: 'reload'});
});
}
});
Thanks for your help!

If I understand correctly, your main problem is how to retrieve data from the storage. Here is what can be done:
chrome.storage.local.get(null, function(all){console.log(all)});
will return you an object with all keys and values stored in the storage, in your case it will output:
Object {option1: "value1", option2: "value2"}
Also you can get just one of the keys:
chrome.storage.local.get("optionkey", function(all){console.log(all)});
or an array of keys:
chrome.storage.local.get(["opt1", "opt2"], function(all){console.log(all)});
In any case you can access data in the callback just by key names.

Thanks for your reply. I finally managed to get something working by changing my original code as follows:
$(function(){
// INIT
const storage = chrome.storage.sync;
var options = new Array();
$("#notifysav").hide();
loadOptions();
// STORAGE
// Save Button Click event
$("#save").live('click', function(){ saveOptions(); });
function loadOptions() {
storage.get('options', function(o) {
$("#option1").attr('checked', o.options[0]);
$("#option2").attr('checked', o.options[1]);
...
});
}
function saveOptions() {
options[0] = $("#option1").prop('checked');
options[1] = $("#option2").prop('checked');
...
//console.log(options);
storage.set({'options':options},function(){
$("#notifysav").fadeIn("slow").delay(2000).fadeOut("slow");
chrome.extension.sendRequest({action: 'reload'});
});
}
});

Related

Firebase Functions and Express: listen to firestore data live

I have a website that runs its frontend of Firebase Hosting and its server which is written using node.js and Express on Firebase Functions
What I want to have redirect links from my website so I can map for example mywebsite.com/youtube to my youtube channel. the way I am creating these links is from my admin panel, and adding them to my Firestore database.
My data is roughly something like this:
The first way I approached this, is by querying my Firestore database on every request, but that is heavily expensive and slow.
Another way I tried to approach this is by setting some kind of background listener to the Firestore database which will always provide up to date data. but unfortunately that did not work because Firebase Functions suspends the main function when the current request execution ends.
lastly, which is the most convenience way, I configured an api route, which will be called from my Admin Panel when any change happens to the data, and I would save the new data to some json file. I tried this on my local but it did not work on production because appearently Firebase Functions is a Read-only system, so we can't edit any files after they are deployed. after some research I found out that Firebase Functions allows writing to the tmp directory, so I went forward with this, and tried deploying it. but again, Firebase Functions was resetting the tmp folder when some request execution ends.
here is my api request code which updates the utm_data.json file in the tmp directory:
// my firestore provider
const db = require('../db');
const fs = require('fs');
const os = require('os')
const mkdirp = require('mkdirp');
const updateUrlsAPI = (req, res) => {
// we wanna get the utm list from firestore, and update the file
// tmp/utm_data.json
// query data from firestore
db.collection('utmLinks').get().then(async function(querySnapshot) {
try {
// get the path to `tmp` folder depending on
// the os running this program
let tmpFolderName = os.tmpdir()
// create `tmp` directory if not exists
await mkdirp(tmpFolderName)
let docsData = querySnapshot.docs.map(doc => doc.data())
let tmpFilePath = tmpFolderName + '/utm_data.json'
let strData = JSON.stringify(docsData)
fs.writeFileSync(tmpFilePath, strData)
res.send('200')
} catch (error) {
console.log("error while updating utm_data.json: ", error)
res.send(error)
}
});
}
and this is my code for reading the utm_data.json file on an incoming request:
const readUrlsFromJson = (req, res) => {
var url = req.path.split('/');
// the url will be in the format of: 'mywebsite.com/routeName'
var routeName = url[1];
try {
// read the file ../tmp/utm_data.json
// {
// 'createdAt': Date
// 'creatorEmail': string
// 'name': string
// 'url': string
// }
// our [routeName] should match [name] of the doc
let tmpFolderName = os.tmpdir()
let tmpFilePath = tmpFolderName + '/utm_data.json'
// read links list file and assign it to the `utms` variable
let utms = require(tmpFilePath)
if (!utms || !utms.length) {
return undefined;
}
// find the link matching the routeName
let utm = utms.find(utm => utm.name == routeName)
if (!utm) {
return undefined;
}
// if we found the doc,
// then we'll redirect to the url
res.redirect(utm.url)
} catch (error) {
console.error(error)
return undefined;
}
}
Is there something I am doing wrong, and if not, what is an optimal solution for this case?
You can initialize the Firestore listener in global scope. From the documentation,
The global scope in the function file, which is expected to contain the function definition, is executed on every cold start, but not if the instance has already been initialized.
This should keep the listener active even after the function's execution has completed until that specific instance is running (which should be about ~30 minutes). Try refactoring the code as shown below:
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
let listener = false;
// Store all utmLinks in global scope
let utmLinks: any[] = [];
const initListeners = () => {
functions.logger.info("Initializing listeners");
admin
.firestore()
.collection("utmLinks")
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach(async (change) => {
functions.logger.info(change.type, "document received");
switch (change.type) {
case "added":
utmLinks.push({ id: change.doc.id, ...change.doc.data() });
break;
case "modified":
const index = utmLinks.findIndex(
(link) => link.id === change.doc.id
);
utmLinks[index] = { id: change.doc.id, ...change.doc.data() };
break;
case "removed":
utmLinks = utmLinks.filter((link) => link.id !== change.doc.id);
default:
break;
}
});
});
return;
};
// The HTTPs function
export const helloWorld = functions.https.onRequest(
async (request, response) => {
if (!listener) {
// Cold start, no listener active
initListeners();
listener = true;
} else {
functions.logger.info("Listeners already initialized");
}
response.send(JSON.stringify(utmLinks, null, 2));
}
);
This example stores all UTM links in an array in global scope which won't be persisted in new instances but you won't have to query each link for every request. The onSnapshot() listener will keep utmLinks updated.
The output in logs should be:
If you want to persist this data permanently and prevent querying in every cold start, then you can try using Google Cloud Compute that keeps running unlike Cloud functions that timeout eventually.

CRM JS event for field value changed by workflow

I have form and button on that form in CRM 2015. By clicking on button on form, user triggers on-demand workflow. When workflow is done, it updates value of one of the fields on the form. However, this server data change is not reflected on the user UI.
What is the best way to register JS callback which will refresh form if workflow execution is successful?
Reading this: https://msdn.microsoft.com/en-us/library/gg334701.aspx it looks like I can't use OnChange() event, because I change data programatically.
First of all I would suggest to use Sync Workflow. After workflow is executed just execute following code:
Xrm.Page.data.refresh(false);
I had such requirements once. I had to reflect changes on the Form that are changed by the Async Workflow, and for some reason i had to keep the Workflow Async.
Following is a work-around i did for requirements like this.
Add a new field to the entity, on which the workflow is executed.
FieldName: "isworkflowexecutedsuccessfully"
FieldType: "TwoOption"
Default Value: "false"
Then in your code where you have written the workflow code, write this:
function someFunctionOfYours() {
RunWorkflow(); //
WaitForWorkflowToCompleteProcessingAndThenReload();
}
function isWorklowExecutionCompleted(TimerId, updateIsWorkflowExecutedSuccessfully) {
var entityName = Xrm.Page.data.entity.getEntityName();
var entityGuid = Xrm.Page.data.entity.getId();
var retrievedOpportunity = XrmServiceToolkit.Soap.Retrieve(entityName, entityGuid, new Array("isworkflowexecutedsuccessfully")); //synchronous call
if (retrievedOpportunity.attributes["isworkflowexecutedsuccessfully"].value = true) {
clearInterval(TimerId);
setTimeout(function () {
setIsworkFlowExecutedSuccessfullyToFalse(updateIsWorkflowExecutedSuccessfully);
}, 3000);
}
}
function WaitForWorkflowToCompleteProcessingAndThenReload() {
var TimerId = setTimeout(function () {
isWorklowExecutionCompleted(TimerId);
}, 5000);
}
function setIsworkFlowExecutedSuccessfullyToFalse(updateIsWorkflowExecutedSuccessfully) {
var entityName = Xrm.Page.data.entity.getEntityName();
var entityGuid = Xrm.Page.data.entity.getId();
var updateOpportunity = new XrmServiceToolkit.Soap.BusinessEntity(entityName, entityGuid);
updateOpportunity.attributes["isworkflowexecutedsuccessfully"] = false;
if (updateIsWorkflowExecutedSuccessfully == false || updateIsWorkflowExecutedSuccessfully == null) {
XrmServiceToolkit.Soap.Update(updateOpportunity);
}
Xrm.Utility.openEntityForm(entityName, entityGuid) //refresh form
}

Persist data on disk using chrome extension API

I am trying to save some data which should be available even when restart the browser So this data should persist. I am using Chrome Storage Sync API for this. But when I am restarting my browser, I get empty object on using chrome.storage.get.
Here is my sample code:
SW.methods.saveTaskListStore = function() {
chrome.storage.sync.set({
'taskListStore': SW.stores.taskListStore
}, function() {
if (SW.callbacks.watchProcessSuccessCallback) {
SW.callbacks.watchProcessSuccessCallback(SW.messages.INFO_DATA_SAVED);
SW.callbacks.watchProcessSuccessCallback = null;
}
});
};
SW.methods.loadTaskListStore = function() {
SW.stores.loadTaskListStore = [];
chrome.storage.sync.get('taskListStore', function(taskFeed) {
var tasks = taskFeed.tasks;
if (tasks && !tasks.length) {
SW.stores.loadTaskListStore = tasks;
}
});
};
I guess I am using the Wrong API.
If this is not some copy-paste error, you are storing under key taskListStore and trying to get data under key loadTaskListStore.
Besides that, according to the documentation on StorageArea.get(), the result object is an object with items in their key-value mappings. Thus, in your case, you should do:
chrome.storage.sync.get("taskListStore", function(items) {
if (items.taskListStore) {
var tasks = items.taskListStore.tasks;
...

Accessing Marionette Apps from Views (using Require.js)

I'm trying to share a Marionette App with some of its Views. I've read the wiki here, but the example leaves me with a question.
I've got a file with a couple of views in it that will all need to use the request/response system and possibly the commands. I don't want to do var MyApp = require('app'); in all of the Views in the file. I came up with the following, but I think there's probably a better way to do it.
Example:
//Views.js
define( ["marionette"], function (Marionette) {
var App = function(){
return require('app');
};
var ExampleItemView = Marionette.ItemView.extend({
initialize: function(){
App().request("getInfo", "aboutStuff");
}
});
return Marionette.CollectionView.extend({
itemView: ExampleItemView,
initialize: function(){
App().request("getInfo", "aboutStuff");
}
});
Is there a better way to do this?
I'd definitely not inject your app into your views since it can create circular dependencies which are ALWAYS a code smell (regardless of whether they can be solved or not) The simples solution by far is to create a separate (singleton) reqres object which is handled by the app and injected into the views.
//reqres.js
define(['backbone.wreqr'], function( Wreqr ){
return new Wreqr.RequestResponse();
});
//app
define(['reqres'], function(reqres){
reqres.setHandlers({
'getInfo' : function(){
return 'foo';
}
});
});
//Views.js
define( ["marionette", "reqres"], function (Marionette, reqres) {
var ExampleItemView = Marionette.ItemView.extend({
initialize: function(){
reqres.request("getInfo", "aboutStuff");
}
});
return Marionette.CollectionView.extend({
itemView: ExampleItemView,
initialize: function(){
reqres.request("getInfo", "aboutStuff");
}
});
Backbone Wreqr is to be replaced by Backbone Radio in the next major release of Marionette, v3.
To use Backbone Radio you could create a channel and do the following:
/**
* App.js (for example)
*/
var Radio = require('backbone.radio');
// Use the 'data' channel - this could be whatever channel name you want
var dataChannel = Radio.channel('data');
// Handler for a request
dataChannel.reply('getMessage', function(name) {
return 'Hello ' + name + '. Alba gu brath!';
});
/**
* View.js (for example)
*/
var Radio = require('backbone.radio');
var dataChannel = Radio.channel('data');
// Make the request
console.log( dataChannel.request('getMessage', 'Craig') ); // -> Hello Craig. Alba gu brath!

chrome extension connection issues

I have written an extension for google chrome and I have a bug I need a help solving.
what I do is using either a text selection or an input of text search for photos on flickr and then create a results tab.
The extension works most of the times. but sometimes it creates a blank tab with no results and when I repeat the same search it then shows results. I figured that it's something to do with the html files messaging maybe something to do with them communicating. I have to say that I always receive the results from flickr so that the request/responce with flickr works ok. Sometimes the error happens when I play with other tabs or do something on other tabs while waiting for results. can you please help me figure out where's the fault?
the background file:
function searchSelection(info,tab){
var updated;
if(info.selectionText==null){
var value = prompt("Search Flickr", "Type in the value to search");
updated=makeNewString(value);
}
else{
updated=makeNewString(info.selectionText);
}
var resultHtml;
var xhReq = new XMLHttpRequest();
xhReq.open(
"GET",
"http://api.flickr.com/services/rest/?method=flickr.photos.search&text="+updated+
"&api_key=a0a60c4e0ed00af8d70800b0987cae70&content_type=7&sort=relevance&per_page=500",
true);
xhReq.onreadystatechange = function () {
if (xhReq.readyState == 4) {
if (xhReq.status == 200) {
chrome.tabs.executeScript(tab.id, {code:"document.body.style.cursor='auto';"});
var photos = xhReq.responseXML.getElementsByTagName("photo");
if(photos.length==0){
alert("No results found for this selection");
chrome.tabs.executeScript(tab.id, {code:"document.body.style.cursor='auto';"});
return;
}
var myJSPhotos=[];
for(var i=0; i<photos.length; i++){
var data={"id":photos[i].getAttribute("id"),"owner":photos[i].getAttribute("owner"),
"secret":photos[i].getAttribute("secret"),"server":photos[i].getAttribute("server"),
"farm":photos[i].getAttribute("farm"),"title":photos[i].getAttribute("title")};
myJSPhotos[i]=data;
}
chrome.tabs.create({"url":"results.html"},function(thistab){
var port= chrome.tabs.connect(thistab.id);
port.postMessage({photos:myJSPhotos});
});
}
};
};
xhReq.send(null);
chrome.tabs.executeScript(tab.id, {code:"document.body.style.cursor='wait';"});
}
var context="selection";
var id = chrome.contextMenus.create({"title": "search Flickr", "contexts":[context,'page'],"onclick":searchSelection});
results html: has only a reference to the js file res.js
res.js :
chrome.extension.onConnect.addListener(function(port) {
port.onMessage.addListener(function(msg) {
//*****//
var photos=msg.photos;
createPage(photos);
});
});
I have to mention that when the tab is empty if I put alert on the //*****// part it won't
fire.
but when I print out the photos.length at the tab create call back function part it prints out the correct result.
Try to set "run_at":"document_start" option for your res.js in the manifest.
I think callback from chrome.tabs.create is fired right away without waiting for page scripts to be loaded, so you might try something like this instead:
//global vars
var createdTabId = null;
var myJSPhotos = null;
xhReq.onreadystatechange = function () {
//assign myJSPhotos to a global var
chrome.tabs.create({"url":"results.html"},function(thistab){
createdTabId = thistab.id;
});
}
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if(changeInfo.status == "complete" && tab.id == createdTabId) {
createdTabId = null;
//now page is loaded and content scripts injected
var port = chrome.tabs.connect(tab.id);
port.postMessage({photos:myJSPhotos});
}
});

Resources