The Nuxeo for Salesforce addon allows Salesforce (SFDC) users to attach documents to their Salesforce Objects (such as Opportunities, Contacts, Accounts...) through the Salesforce UI within a Nuxeo server.
See GitHub Readme for the Dev project description.
Functional Overview Video
Installation and Configuration
Installation
This addon requires no specific installation steps. It can be installed like any other package from the Update Center or using nuxeoctl command line.
Nuxeo Platform Configuration
Set up the HTTPS configuration. Salesforce is requiring Nuxeo server to be accessed through HTTPS. Follow this documentation to configure your reverse proxy for production purpose. For a dev or test environment, you can configure your Nuxeo server in HTTPS directly with the following configuration parameters example:
nuxeo.server.https.port=8443 nuxeo.server.https.keystoreFile=/Users/vpasquier/.keystore nuxeo.server.https.keystorePass=******
You can setup the keystore by following the Oracle documentation.
Add the following configuration parameter (in Admin > Cloud Services > OAuth2 Provider > Add):
ID=salesforce CliendID=YOUR_SALESFORCE_CONSUMER_KEY User Authorization URL=https://NUXEO_SERVER/nuxeo/picker/callback/callback.html
Set up your browser to access Nuxeo for Salesforce from within Salesforce. If you're using Firefox browser, you don't need to configure it. However with Chrome, here are the guidelines to allow the access:
- Authorize
Popups
from Salesforce (to allow OAuth execution). - Go to
https://NUXEO_SERVER/nuxeo
and allow Chrome to access in HTTPS your Nuxeo server.
- Authorize
Salesforce Configuration
In your Salesforce account, you can setup the Nuxeo for Salesforce plugin through the Salesforce Marketplace (In progress).
You can also set it up directly from your Salesforce dashboard. Note that these instructions assume you are using "Salesforce Classic", not the "Lightning Experience". You can adapt them for the Lightning Experience, or disable it via Setup Home > Lightning Experience. Scroll to the bottom to disable it.
- Go in your Salesforce dashboard.
- Go on Setup (top right).
- Go to Build > Create > Apps.
Click the New button under Connected Apps named
Nuxeo
(it MUST be named "Nuxeo"):- Enable OAuth settings and set the callback URL:
https://NUXEO_SERVER/nuxeo/picker/callback/callback.html
- Add all available Scopes.
- Enable Force.com Canvas and set the App URL
https://NUXEO_SERVER/nuxeo/picker
. - Select OAuth Webflow for Access Method.
- Configure Canvas App locations and add Layouts and Mobile Cards.
- Enable OAuth settings and set the callback URL:
- Save the "Nuxeo" Connected App.
- Go to Build > Customize and choose any SFDC Object, e.g. "Opportunities".
- Click on Pages Layout to edit the SFDC Object page layout.
Add the Nuxeo Canvas App anywhere in the page.
- Hint: Choose "Canvas Apps" in the list of available objects.
- Tip: if you add a new "Section" you need to save the layout before you can drop the Nuxeo Canvas App.
- Save the layout. Now when an Opportunity is opened the Nuxeo widget will be enabled.
Synchronization - Salesforce vs Nuxeo
The default behavior of Nuxeo Salesforce plugin is to bind the current Salesforce Object to a Workspace
document type and the way the metadata are synchronized. This document type Workspace
has a new facet salesforce
to store the SF object id.
Each time a SF user is displaying a SF object in his SF console, Nuxeo is going to create/retrieve the related workspace, listing all its children.
Default Behavior
The Automation operation script javascript.FetchSFObject
can be overriden in order to bind the current Salesforce object to a specific document in Nuxeo.
<scriptedOperation id="javascript.FetchSFObject">
<inputType>void</inputType>
<outputType>void</outputType>
<category>javascript</category>
<script>
function run(input, params) {
var sfobject = {};
sfobject.id = params.sfobjectId;
sfobject.name = params.sfobjectName;
var docs = Repository.Query(null, {
'query': "SELECT * FROM Document WHERE ecm:currentLifeCycleState != 'deleted' AND sf:objectid = '" + sfobject.id + "' AND ecm:isVersion = 0 AND ecm:mixinType != 'HiddenInNavigation'",
});
if (docs.length>0) {
return Repository.GetDocument(null, {
'value': docs[0].id
});
} else {
var workspaces = Repository.GetDocument(null, {
"value" : "/default-domain/workspaces"
});
var newSFobject = Document.Create(workspaces, {
"type" : "Workspace",
"name" : sfobject.name,
"properties" : {
"dc:title" : sfobject.name,
"sf:objectid" : sfobject.id
}
});
return newSFobject;
}}]]>
</script>
</scriptedOperation>
In the operation, the parameter param
of the function provides three attributes: sfobjectid
, sfobjectname
and sfobjecttype
.
Override Example
Here is an example of overriding the SF object binding: When I enter my SF object, like account or an opportunity, the operation below is executed.
- Nuxeo checks if this object is already bound with a Nuxeo document through
sfobject.id
. - It returns the Nuxeo document if exists.
- If the Nuxeo object doesn't exist, it creates it in the appropriate location:
- If the SF object is an account, place it under the document
/default-domain/workspaces/Custom
or under his parent account. - If the SF object is an opportunity, place it under his parent account.
- If the SF object is an account, place it under the document
- The metadata are synchronized from Salesforce to Nuxeo (checking if metadata have been changed).
This behavior is implemented by this operation.
function run(input, params) {
var sfobject = JSON.parse(params.sfObject);
// We are checking if the document is existing. If not we're going to check the rules to create it accordingly.
var docs = Repository.Query(null, {
'query': "SELECT * FROM Document WHERE ecm:currentLifeCycleState != 'deleted' AND sf:objectId = '" + sfobject.Id + "' AND ecm:isVersion = 0 AND ecm:mixinType != 'HiddenInNavigation'",
});
if (docs.length>0) {
return versarDocument(docs[0],sfobject);
} else {
var newSFobject = {};
// We are checking if an account has a parent account or not and create it beneath the appropriate document.
if(sfobject.attributes.type === "Account") {
if(sfobject.ParentId === null){
var proposals = Repository.GetDocument(null, {
"value" : "/default-domain/workspaces/Custom"
});
newSFobject = Document.Create(proposals, {
"type" : "ParentAccount",
"name" : sfobject.Name,
"properties" : {
"dc:title" : sfobject.Name,
"sf:objectId" : sfobject.Id
}
});
}else{
var parents = Repository.Query(null, {
'query': "SELECT * FROM Document WHERE ecm:currentLifeCycleState != 'deleted' AND sf:objectId = '" + sfobject.ParentId + "' AND ecm:isVersion = 0 AND ecm:mixinType != 'HiddenInNavigation'",
});
newSFobject = Document.Create(parents[0], {
"type" : "AccountName",
"name" : sfobject.Name,
"properties" : {
"dc:title" : sfobject.Name,
"sf:objectId" : sfobject.Id,
"sf:Parent_Account" : parents[0]["dc:title"];
}
});
}
} else {
if(sfobject.Contract_ID === null) {
var accounts = Repository.Query(null, {
'query': "SELECT * FROM Document WHERE ecm:currentLifeCycleState != 'deleted' AND sf:objectId = '" + sfobject.AccountId + "' AND ecm:isVersion = 0 AND ecm:mixinType != 'HiddenInNavigation'",
});
var account = accounts[0];
var properties = getProperties(newSFobject, sfobject);
properties["dc:title"]=sfobject.Name;
properties["sf:objectId"]=sfobject.Id;
newSFobject = Document.Create(account, {
"type" : "OpportunityName",
"name" : sfobject.Name,
"properties" : properties
});
}
}
return newSFobject;
}
}
function versarDocument(doc, sfobject){
if(sfobject.attributes.type === "Account") {
var account = Repository.GetDocument(null, {'value': doc.id});
return account;
}else{
var dirtyProperties = getProperties(doc, sfobject);
var opportunity = Document.Update(
doc, {
'properties': dirtyProperties
});
return opportunity;
}
}
function getProperties(doc, sfobject){
var dirtyProperties = {};
if(sfobject.Contract_ID!==doc["sf:Contract_ID"]){
dirtyProperties["sf:Contract_ID"]=sfobject.Contract_ID;
}
if(sfobject.Task_Order_Number!==doc["sf:Task_Order_Number"]){
dirtyProperties["sf:Task_Order_Number"]=sfobject.Task_Order_Number;
}
if(sfobject.Contract_Ceiling_Value!==doc["sf:Contract_Ceiling_Value"]){
dirtyProperties["sf:Contract_Ceiling_Value"]=sfobject.Contract_Ceiling_Value;
}
if(sfobject.StageName!==doc["sf:Stage"]){
dirtyProperties["sf:Stage"]=sfobject.StageName;
}
return dirtyProperties;
}
Those two operations can be overridden inside a Nuxeo Studio project easily by creating two operations for instance: SFGetChildren
and FetchSFObject
.
In order to find all your metadata in Salesforce, go to Setup > Customize > Your Object > Fields. You will be able to map correctly all the Nuxeo and SF metadata with the appropriate field keys.
Documents Listing
Default Behavior
The Automation operation script javascript.SFGetChildren
provides a way for the developer to customize the listing of the document content bound to the Salesforce object.
<scriptedOperation id="javascript.SFGetChildren">
<inputType>document</inputType>
<outputType>documents</outputType>
<category>javascript</category>
<script>
function run(input, params) {
return Document.GetChildren(input, {});
}}]]>
</script>
</scriptedOperation>
Override Example
For instance, the listing execution could be executed in an unrestricted session to list "unauthorized" documents only for title viewing (Salesforce or Nuxeo rights are not affected).
function run(input, params) {
Auth.LoginAs(null, {});
var children = Document.GetChildren(input, {});
Auth.Logout(null, {});
return children;
}
Those two operations can be overridden inside a Nuxeo Studio project easily by creating two operations for instance: SFGetChildren
and FetchSFObject
.