I'm trying to catch an error when creating a new record to a data source. (The data source is set to only accept unique values on one of the fields.)
When attempting to force an error the client side script runs and displays the final popup stating that a new record was created successfully. Instead of displaying the popup to state an error creating the record has occurred.
I spoke with one of our senior devs and he explained the issue is due to saveChanges running asynchronously. (The call to save changes runs without any error and only after it completes does it return an error.)
So my question is how do I catch an error after saveChanges completes and display a popup. (If record created successfully or failed.)
My Code:
//Creates New Stores
function createNewStores() {
var newCert = '{"ShopCertificates":[{"Certificate_ID":"","Certificate_Name":"","Valid_From":"","Valid_To":"","Renewal_Date":"","Date_Applied_For_Renewal":"","Date_Compliance_Received":"","Date_Compliance_Issues_Resolved":"","Compliance_Notice_Date":"","Certificate_URL":""}]}';
//Get the Datasource, set it to create mode and create a new blank item.
var createDatasource = app.datasources.Stores.modes.create;
var draft = createDatasource.item;
//Get the selected values from the page.
var brand = app.pages.A_Add_Store.descendants.Dropdown_Brand_Selector.value;
var division = app.pages.A_Add_Store.descendants.Dropdown_Division_Selector.value;
var storeName = app.pages.A_Add_Store.descendants.Dropdown_Stores_Selector.value;
//Set the values of the draft record to be the values entered in the form.
draft.StoreId = parseInt(storeName);
draft.Brand = brand;
draft.Division = division;
draft.Store_Name = storeName;
draft.Cert_JSON = newCert;
//Create the new record in the datasource and save changes to the datasource.
try{
createDatasource.createItem();
app.datasources.Stores.saveChanges();
}
catch(err){
app.popups.Error_Store_Already_Exists.visible = true;
}
//After record is created set values in form to null.
app.pages.A_Add_Store.descendants.Dropdown_Brand_Selector.value = null;
app.pages.A_Add_Store.descendants.Dropdown_Division_Selector.value = null;
app.pages.A_Add_Store.descendants.Dropdown_Stores_Selector.value = null;
//Display Popup stating store has been added.
app.popups.New_Store_Added.visible = true;
}
Assuming that your Stores datasource is set to 'Manual Save' mode the following should work:
Replace this section of code:
try{
createDatasource.createItem();
app.datasources.Stores.saveChanges();
}
catch(err){
app.popups.Error_Store_Already_Exists.visible = true;
}
With this:
createDatasource.createItem();
app.datasources.Stores.saveChanges({
success: function() {
app.popups.New_Store_Added.visible = true;
},
failure: function() {
app.popups.Error_Store_Already_Exists.visible = true;
}
});
If your datasource is set to auto save then the saveChanges() function will get ignored and you will not be able to pass a call back in that function. Please reference the asynchronous operations section in the documentation here https://developers.google.com/appmaker/scripting/client#asynchronous_operations.
If this doesn't work for you or you are unable to use this to figure out your code please let me know.
Related
I am working on Microsoft Excel Add-in in react, I want to assign colors to the specific cells based on the value in it i.e, by color code (See Board Color column in image below). So what I did is, get column of the table, and iterate through that column cells and load "value" for each cell. Then iterate on array of those values and try to fill a respective color to each cell.
on iteration, Values are printed in console correctly, but color are not being filled, now it has become headache for me. It is only happening if I assign color in/after "await this.context.sync()". Before that, Color are filled (checked by dummy cells).
Below is the code:
// ExcelAPI.js class
async setBoardColor() {
console.log("setBoardColor()");
try {
let workItemIDColumn = this.workitemTable.columns.getItem("BOARD COLOR").getDataBodyRange().load("values"); // .load(["values", "address"]);
// let workItemIDColumn = await range.load(["values", "address"]);
console.log("Workitem IDs 00 : ", workItemIDColumn);
// console.log("Workitem IDs count : ", workItemIDColumn.values.length);
await this.context.sync().then(() => {
let accountHeaderRange = this.currentWorksheet.getRange("A2:B2");
accountHeaderRange.format.fill.color = "red";
for (var row = 0; row < workItemIDColumn.values.length; row++) {
console.log("in loop : ", row);
console.log("values : " + workItemIDColumn.values[row][0]);
// workItemIDColumn.getRow(row).getRange("A2:B2").format.fill.color = "red";
if (workItemIDColumn.values[row][0] != "") {
console.log("I am in");
workItemIDColumn.getRow(row).getResizedRange(0, -2).format.fill.color = "red"; // workItemIDColumn.values[row][0];
// workItemIDColumn.getRow(row).format.protection.locked = false;
}
}
});
// console.log("Workitem IDs check : ");
} catch (error) {
console.log("setBoardColor() ", error);
}
}
Calling of above method is in main class component.
renderExcelContent = async (workItems) => {
try {
await Excel.run(async (context) => {
const currentWorksheet = context.workbook.worksheets.getActiveWorksheet();
let excelAPI = new ExcelAPI(context, currentWorksheet);
excelAPI.setBoardColor();
currentWorksheet.getRange("A2:B2").format.protection.locked = false;
currentWorksheet.protection.protect();
// eslint-disable-next-line no-undef
if (Office.context.requirements.isSetSupported("ExcelApi", "1.2")) {
currentWorksheet.getUsedRange().format.autofitColumns();
currentWorksheet.getUsedRange().format.autofitRows();
}
currentWorksheet.activate();
context.runtime.enableEvents = true;
});
} catch (error) {
console.error(error);
}
};
That's just how the Excel JavaScript API works. The things you're doing before calling sync are just interacting with JavaScript objects that aren't actually in the spreadsheet. It's the sync call that writes the changes you've made to the actual worksheet. So if you want to see changes, you have to use sync.
From the documentation (my emphasis):
Excel.RequestContext class
The RequestContext object facilitates requests to the Excel application. Since the Office add-in and the Excel application run in two different processes, the request context is required to get access to the Excel object model from the add-in.
and from sync:
Synchronizes the state between JavaScript proxy objects and the Office document, by executing instructions queued on the request context and retrieving properties of loaded Office objects for use in your code.
(Note that an implicit sync happens after your Excel.run callback returns.)
I have fixed this by "await" before calling setBoardColor() in main class.
await excelAPI.setBoardColor();
I am trying to modify the cats quickstart tutorial to add a notification at the bottom to notify the user when any errors are encountered (e.g. a toast notification or something similar).
I have:
function createCatCard(text, isHomepage) {
if (!isHomepage) {
isHomepage = false;
}
// Use the "Cat as a service" API to get the cat image. Add a "time" URL
// parameter to act as a cache buster.
var now = new Date();
// Replace formward slashes in the text, as they break the CataaS API.
var caption = text.replace(/\//g, ' ');
var imageUrl = Utilities.formatString('https://cataas.com/cat/says/%s?time=%s', encodeURIComponent(caption), now.getTime());
var image = CardService.newImage().setImageUrl(imageUrl).setAltText('Meow')
// Create a button that changes the cat image when pressed.
// Note: Action parameter keys and values must be strings.
var action = CardService.newAction().setFunctionName('onChangeCat').setParameters({text: text, isHomepage: isHomepage.toString()});
var button = CardService.newTextButton().setText('Change cat').setOnClickAction(action).setTextButtonStyle(CardService.TextButtonStyle.FILLED);
var buttonSet = CardService.newButtonSet().addButton(button);
// Create a footer to be shown at the bottom.
var footer = CardService.newFixedFooter().setPrimaryButton(CardService.newTextButton().setText('Powered by cataas.com').setOpenLink(CardService.newOpenLink().setUrl('https://cataas.com')));
// Assemble the widgets and return the card.
var section = CardService.newCardSection().addWidget(image).addWidget(buttonSet);
var card = CardService.newCardBuilder().addSection(section).setFixedFooter(footer);
if (!isHomepage) {
// Create the header shown when the card is minimized,
// but only when this card is a contextual card. Peek headers
// are never used by non-contexual cards like homepages.
var peekHeader = CardService.newCardHeader().setTitle('Contextual Cat').setImageUrl('https://www.gstatic.com/images/icons/material/system/1x/pets_black_48dp.png').setSubtitle(text);
card.setPeekCardHeader(peekHeader)
}
var act = CardService.newActionResponseBuilder().setNotification(CardService.newNotification().setText('bob here').setType(CardService.NotificationType.INFO));
card.addCardAction(act);
return card.build();
}
(The only part I've added is the second to last line). However, when I run the code, I get:
Cannot find method addCardAction(CardService.ActionResponseBuilder). [line: 62, function: createCatCard, file: Common]
EDIT:
I've also tried card.setNotification(CardService.newNotification().setText('bob here').setType(CardService.NotificationType.INFO)); but I get TypeError: Cannot find function setNotification in object CardBuilder. [line: 62, function: createCatCard, file: Common]
EDIT2:
I've also tried
card.newNotification().setText('bob here').setType(CardService.NotificationType.INFO);
but I get TypeError: Cannot find function newNotification in object CardBuilder. [line: 63, function: createCatCard, file: Common]
The card object is of the type CardBuilder, and according to the docs, it does not have an addCardAction, setNotification, or newNotification method, which is why what you are trying to do won't work.
There is a Notification class, that you use when a user interacts with a UI element." Here is an example for the cats quickstart code:
function onChangeCat(e) {
... // <-- omitted for brevity
var navigation = CardService.newNavigation()
.updateCard(card);
var actionResponse = CardService.newActionResponseBuilder()
.setNavigation(navigation);
const failed = someOperationThatMightFail();
if (failed) {
actionResponse
.setNotification(CardService.newNotification()
.setText("PANIC"));
}
return actionResponse.build();
}
Edit:
If you want to indicate a failure using your existing card I believe it has to be done with the actual Card object itself. The Notification class can only be used as a response to interactions with UI elements. An advantage of indicating a failure with the Card object is that the information will be available to the user until the problem has been resolved, unlike with the Notification which will eventually disappear from the UI.
Here is an example of how you could indicate failure using your existing card:
function createCatCard(text, isHomepage) {
...
// Create a footer to be shown at the bottom.
var footer = CardService.newFixedFooter().setPrimaryButton(CardService.newTextButton().setText('Powered by cataas.com').setOpenLink(CardService.newOpenLink().setUrl('https://cataas.com')));
// Assemble the widgets and return the card.
var section = CardService.newCardSection();
const failed = someOperationThatMightFail();
if (failed) {
var textParagraph = CardService.newTextParagraph();
textParagraph.setText('Something went wrong, please try again later');
section
.addWidget(buttontextParagraphSet);
} else {
section
.addWidget(image)
.addWidget(buttonSet);
}
var card = CardService.newCardBuilder().addSection(section).setFixedFooter(footer);
if (!isHomepage) {
// Create the header shown when the card is minimized,
// but only when this card is a contextual card. Peek headers
// are never used by non-contexual cards like homepages.
var peekHeader = CardService.newCardHeader().setTitle('Contextual Cat').setImageUrl('https://www.gstatic.com/images/icons/material/system/1x/pets_black_48dp.png').setSubtitle(text);
card.setPeekCardHeader(peekHeader)
}
...
}
I've created Multi-select option-set field (category) in Dynamics CRM on-premise for Contact and Projects. Now using button click I'm trying to set the value of multi-select field on Project. But each time I am coming across with Error:
Error converting value 920650008 to type System.Collections.Generic.List 1[System.Int32].
Since the multi-select optionset field is global so there is no chance of specified values available or not.
Here is what I am trying previously:
var name = formContext.getAttribute(new.account_metada.CompanyName).getValue();
var entityFormOptions["entityName"] = "new_projects";
entityFormOptions["openInNewWindow"] = true;
var formParameters["new_company"] = id;
formParameters["new_companyname"] = name;
formParameters["new_category"] = formContext.getAttribute("new_category").getValue()
Xrm.Navigation.openForm(entityFormOptions, formParameters).then(
function (success) {
console.log(success);
},
function (error) {
console.log(error);
});
Please let me know how to can I set value of multi-select optionset using Xrm.Navigation.openForm
I got fix the issue by replacing below source code line:
Existing
formParameters["new_multiselectpicklist"] = formContext.getAttribute("new_multiselectpicklist").getValue();
Updated
formParameters["new_multiselectpicklist"] = "["+formContext.getAttribute("new_multiselectpicklist").getValue()+"]";
I tested this personally and getting the same error result with the below snippet. Though the syntax is right - Something could be wrong with the Xrm.Navigation.openForm() method or this could be expected behavior because of unsupported Array datatype.
var entityFormOptions = new Array();
entityFormOptions["entityName"] = "my_entity";
entityFormOptions["openInNewWindow"] = false;
var formParameters = new Array();
formParameters["new_multiselectpicklist"] = formContext.getAttribute("new_multiselectpicklist").getValue();
Xrm.Navigation.openForm(entityFormOptions, formParameters).then(
function (success) {
console.log(success);
},
function (error) {
console.log(error);
});
Even a hard code assignment getting the same error:
formParameters["new_multiselectpicklist"] = [962080001, 962080002];
Edit:
The above line should be like this to make it work.
formParameters["new_multiselectpicklist"] = "[962080001, 962080002]";
I tried this alternate option using extraqs and it worked.
https://mycrmdev.crm.dynamics.com/main.aspx?etn=my_entity&pagetype=entityrecord&extraqs=new_multiselectpicklist=[962080001, 962080002]
I've implemented a call center in Salesforce with Twilio client JavaScript SDK. I'm trying to save the call record information in Salesforce and I'm using the connection.parameters.CallSid to identify the correct record when the recording call back fires. However my CallSid in client side is getting changed automatically sometimes to a different format and hence the recording call back function can't find the Salesforce end record to update it with the RecordingUrl. Have anyone experienced this before or appreciate any guidance.
Below is my JavaScript code. To be more specific, in the startCall function console.log print the CallSid correctly but when goes to saveLog function it's having a different value with a different format and hence the saved record having an incorrect value.
<script type="text/javascript">
Twilio.Device.setup("{! token }");
var callerSid; // hold the Twilio generated CallSid unique to this call
var parentId; // hold the parent record being called in order to associate as the parent of task being logged for the call
var newTaskId; // hold the id of newly created task
// function to fire when click 2 dial executes
var startCall = function (payload) {
sforce.opencti.setSoftphonePanelVisibility({visible: true}); //pop up CTI softphone
parentId = payload.recordId; // the record that the phone number being called belongs to
var cleanednumber = cleanFormatting(payload.number);
params = {"PhoneNumber": cleanednumber};
var connection = Twilio.Device.connect(params);
callerSid = connection.parameters; // track the unique Twilio CallSid
console.log('clk2dial : ', callerSid.CallSid); **// here it logs correcly as CAc57d05994cd69498e0353a5f4b07f2dc**
setTimeout(function(){
saveLog(); // save the call information in a Task record
}, 2000
);
};
//OpenCTI!!
sforce.opencti.enableClickToDial();
sforce.opencti.onClickToDial({listener : startCall}); // click 2 dial
function cleanFormatting(number) {
//changes a SFDC formatted US number, which would be 415-555-1212 into a twilio understanble number 4155551212
return number.replace(' ','').replace('-','').replace('(','').replace(')','').replace('+','');
}
// save the call information in a Task record
function saveLog() {
var keyPrefix;
var taskToSave;
console.log('callerSid.CallSid : ', callerSid.CallSid); **// surprisingly here it logs as TJSce253eb4-c2a0-47f3-957f-8178e95162aa**
if(parentId != null) {
keyPrefix = parentId.slice(0,3);
}
if(keyPrefix != null && keyPrefix == '003') {
taskToSave = {WhoId:parentId, Type: "Call", CallObject: callerSid.CallSid, entityApiName: "Task", Subject: "Call log"};
} else {
taskToSave = {WhatId:parentId, Type: "Call", CallObject: callerSid.CallSid, entityApiName: "Task", Subject: "Call log"};
}
sforce.opencti.saveLog({value:taskToSave, callback: saveLogCallBack});
}
// call back function for saving the call information in a Task record
var saveLogCallBack = function(response) {
if(response.success) {
newTaskId = response.returnValue.recordId;
console.log('save success! : ', newTaskId);
} else {
console.error('Error saving : ', response.errors);
}
}
</script>
Answering my own question as I got through this. I registered a function for Twilio.Device.connect and in the call back function retrieved the CallSid. Along with that I've updated my click 2 dial function as well accordigly as below. However I was unable to find this approach in Twilio documentation and any comments are welcome.
// function to fire when click 2 dial executes
var startCall = function (payload) {
sforce.opencti.setSoftphonePanelVisibility({visible: true}); //pop up CTI softphone
parentId = payload.recordId; // the record that the phone number being called belongs to
var cleanednumber = cleanFormatting(payload.number);
params = {"PhoneNumber": cleanednumber};
Twilio.Device.connect(params);
};
//OpenCTI!!
sforce.opencti.enableClickToDial();
sforce.opencti.onClickToDial({listener : startCall}); // click 2 dial
// registered a function for Twilio Device connect
Twilio.Device.connect(function(response) {
callSid = response.parameters; // track the unique Twilio CallSid
// nothing change in save function so not posting again
saveLog(); // save the call information in a Task record
});
I want to update a div with a list of anchors that I generate from a local database in chrome. It's pretty simple stuff, but as soon as I try to add the data to the main.js file via a callback everything suddenly becomes undefined. Or the array length is set to 0. ( When it's really 18. )
Initially, I tried to install it into a new array and pass it back that way.
Is there a setting that I need to specify in the chrome manifest.json in order to allow for communication with the database API? I've checked, but all I've been able to find was 'unlimited storage'
The code is as follows:
window.main = {};
window.main.classes = {};
(function(awe){
awe.Data = function(opts){
opts = opts || new Object();
return this.init(opts);
};
awe.Data.prototype = {
init:function(opts){
var self = this;
self.modified = true;
var db = self.db = openDatabase("buddy","1.0","LocalDatabase",200000);
db.transaction(function(tx){
tx.executeSql("CREATE TABLE IF NOT EXISTS listing ( name TEXT UNIQUE, url TEXT UNIQUE)",[],function(tx,rs){
$.each(window.rr,function(index,item){
var i = "INSERT INTO listing (name,url)VALUES('"+item.name+"','"+item.url+"')";
tx.executeSql(i,[],null,null);
});
},function(tx,error){
});
});
self._load()
return this;
},
add:function(item){
var self = this;
self.modified = true;
self.db.transaction(function(tx){
tx.executeSql("INSERT INTO listing (name,url)VALUES(?,?)",[item.name,item.url],function(tx,rs){
//console.log('success',tx,rs)
},function(tx,error){
//console.log('error',error)
})
});
self._load()
},
remove:function(item){
var self = this;
self.modified = true;
self.db.transaction(function(tx){
tx.executeSql("DELETE FROM listing where name='"+item.name+"'",[],function(tx,rs){
//console.log('success',tx,rs)
},function(tx,error){
//console.log('error',tx,error);
});
});
self._load()
},
_load:function(callback){
var self = this;
if(!self.modified)
return;
self.data = new Array();
self.db.transaction(function(tx){
tx.executeSql('SELECT name,url FROM listing',[],function(tx,rs){
console.log(callback)
for(var i = 0; i<rs.rows.length;i++)
{
callback(rs.rows.item(i).name,rs.rows.item(i).url)
// var row = rs.rows.item(i)
// var n = new Object()
// n['name'] = row['name'];
// n['url'] = row['url'];
}
},function(tx,error){
//console.log('error',tx,error)
})
})
self.modified = false
},
all:function(cb){
this._load(cb)
},
toString:function(){
return 'main.Database'
}
}
})(window.main.classes);
And the code to update the list.
this.database.all(function(name,url){
console.log('name','url')
console.log(name,url)
var data = []
$.each(data,function(index,item){
try{
var node = $('<div > '+item.name + '</div>');
self.content.append(node);
node.unbind();
node.bind('click',function(evt){
var t = $(evt.target).attr('href');
chrome.tabs.create({
"url":t
},function(evt){
self._tab_index = evt.index
});
});
}catch(e){
console.log(e)
}
})
});
From looking at your code above, I notice you are executing "self._load()" at the end of each function in your API. The HTML5 SQL Database is asynchronous, you can never guarantee the result. In this case, I would assume the result will always be 0 or random because it will be a race condition.
I have done something similar in my fb-exporter extension, feel free to see how I have done it https://github.com/mohamedmansour/fb-exporter/blob/master/js/database.js
To solve a problem like this, did you check the Web Inspector and see if any errors occurs in the background page. I assume this is all in a background page eh? Try to see if any error occurs, if not, I believe your encountering a race condition. Just move the load within the callback and it should properly call the load.
Regarding your first question with the unlimited storage manifest attribute, you don't need it for this case, that shouldn't be the issue. The limit of web databases is 5MB (last I recall, it might have changed), if your using a lot of data manipulation, then you use that attribute.
Just make sure you can guarantee the this.database.all is running after the database has been initialized.