Nuxeo Add-Ons

Nuxeo for Salesforce

Updated: March 18, 2024

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 with nuxeoctl command line or from the Update Center.

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".

Although the Canvas app can be displayed within this new environment, it will still be unstable ("Lightning Experience" is in a beta state on Salesforce side). You can activate the Lightning Experience or disable it via Setup Home > Lightning Experience. Scroll to the bottom to disable it.

  1. Go in your Salesforce dashboard.
  2. Go on Setup (top right).
  3. Go to Build > Create > Apps.
  4. Click the New button under Connected Apps named Nuxeo (it MUST be named "Nuxeo"):
    1. Enable OAuth settings and set the callback URL:  https://NUXEO_SERVER/nuxeo/picker/callback/callback.html</span> .
    2. Add all available Scopes.
    3. Enable Force.com Canvas and set the App URL  <span class="nolink">https://NUXEO_SERVER/nuxeo/picker</span>/
    4. Select OAuth Webflow for Access Method.
    5. Configure Canvas App locations by adding Layouts and Mobile Cards.
  5. Save the "Nuxeo" Connected App.
  6. Go to BuildCustomize and choose any SFDC Object, e.g. "Opportunities".
  7. Click on Page Layouts to edit the SFDC Object page layout.
  8. 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.
  9. Save the layout. Now when an Opportunity is opened the Nuxeo widget will be enabled. You have now access to the consumer key of the nuxeo app, this will be needed in the Nuxeo Platform Configuration part. 

Nuxeo Platform Configuration

  1. Authorize the framing of your Nuxeo server inside Salesforce. Since Nuxeo 8.3, for clickjacking protection the framing is restricted. To unrestrict it, create a new XML extension containing:

    <require>org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestControllerService.defaultContrib</require>
      <extension target="org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestControllerService"
        point="responseHeaders">
      <header name="X-Frame-Options" enabled="false"/>
      </extension>
    
  2. Set up the HTTPS configuration. Salesforce requires the 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.

  3. Add the following configuration parameter (in Admin > Cloud Services > Service Providers OAuth2 Service Providers > Add):

    Service Name=salesforce
    CliendID=YOUR_SALESFORCE_CONSUMER_KEY
    User Authorization URL=https://NUXEO_SERVER/nuxeo/picker/callback/callback.html
    
  4. 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:

    1. Authorize Popups from Salesforce (to allow OAuth execution).
    2. Go to https://NUXEO_SERVER/nuxeo and allow Chrome to access in HTTPS your Nuxeo server.

Synchronization - Salesforce vs Nuxeo

The default behaviour 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. 

javascript.FetchSFObject

<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.

  1. Nuxeo checks if this object is already bound with a Nuxeo document through sfobject.id.
  2. It returns the Nuxeo document if exists.
  3. 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.
  4. The metadata are synchronized from Salesforce to Nuxeo (checking if metadata have been changed).

This behavior is implemented by this operation.

New Behavior

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 nuxeoDocument(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 nuxeoDocument(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;
}

Studio

Those two operations can be overriden inside a Nuxeo Studio project easily by creating two operations for instance: SFGetChildren and FetchSFObject.

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.

javascript.SFGetChildren

<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).

javascript.SFGetChildren

function run(input, params) {
  Auth.LoginAs(null, {});
  var children =  Document.GetChildren(input, {});
  Auth.Logout(null, {});
  return children;
}

Studio

Those two operations can be overriden inside a Nuxeo Studio project easily by creating two operations for instance: SFGetChildren and FetchSFObject.