feat(salesforce): add Lead and Contact syncing

Summary:
associating salesforce contacts and leads with Nylas contacts

adding fetcher form salesforce

shows conencted leads and contacts

auto associates acount

fixing salesforce forms

Salesforce composers are styled

fix opportunity association with account on object creation

fix specs

Test Plan: edgehill --test

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://review.inboxapp.com/D1507
This commit is contained in:
Evan Morikawa 2015-05-14 14:58:42 -07:00
parent e8f7f76080
commit 8941eb791e
8 changed files with 87 additions and 32 deletions

View file

@ -1,37 +1,41 @@
_ = require 'underscore-plus'
Reflux = require 'reflux'
request = require 'request'
{FocusedContactsStore} = require 'inbox-exports'
{Contact, ContactStore, DatabaseStore, FocusedContactsStore} = require 'inbox-exports'
module.exports =
FullContactStore = Reflux.createStore
init: ->
@_fetchAPIData = _.debounce(_.bind(@__fetchAPIData, @), 50)
@_cachedContactData = {}
@listenTo FocusedContactsStore, @_onFocusedContacts
@_loadContactDataFromAPI = _.debounce(_.bind(@__loadContactDataFromAPI, @), 50)
# @_cachedContactData = {}
@_resolvedFocusedContact = null
@_loadFocusedContact = _.debounce(_.bind(@_loadFocusedContact, @), 20)
@_loadFocusedContact()
sortedContacts: -> FocusedContactsStore.sortedContacts()
focusedContact: -> FocusedContactsStore.focusedContact()
@listenTo ContactStore, @_loadFocusedContact
@listenTo FocusedContactsStore, @_loadFocusedContact
fullContactCache: ->
emails = {}
contacts = FocusedContactsStore.sortedContacts()
emails[contact.email] = contact for contact in contacts
fullContactCache = {}
_.each @_cachedContactData, (fullContactData, email) ->
if email of emails then fullContactCache[email] = fullContactData
return fullContactCache
focusedContact: -> @_resolvedFocusedContact
_onFocusedContacts: ->
contact = FocusedContactsStore.focusedContact() ? {}
if not @_cachedContactData[contact.email]
@_fetchAPIData(contact.email)
@trigger()
# We need to pull fresh from the database so when we update data in the
# for the contact, we get it anew.
_loadFocusedContact: ->
contact = FocusedContactsStore.focusedContact()
if contact
@_resolvedFocusedContact = contact
DatabaseStore.findBy(Contact, email: contact.email).then (contact) =>
@_resolvedFocusedContact = contact
if contact and not contact.thirdPartyData?["FullContact"]?
@_loadContactDataFromAPI(contact)
@trigger()
else
@_resolvedFocusedContact = null
@trigger()
__fetchAPIData: (email="") ->
__loadContactDataFromAPI: (contact) ->
# Swap the url's to see real data
email = email.toLowerCase().trim()
email = contact.email.toLowerCase().trim()
return if email.length is 0
url = "https://api.fullcontact.com/v2/person.json?email=#{email}&apiKey=eadcbaf0286562a"
request url, (err, resp, data) =>
@ -39,5 +43,12 @@ FullContactStore = Reflux.createStore
return {} if resp.statusCode != 200
try
data = JSON.parse data
@_cachedContactData[email] = data
@trigger(@)
contact = @_mergeDataIntoContact(contact, data)
DatabaseStore.persistModel(contact).then => @trigger(@)
_mergeDataIntoContact: (contact, data) ->
contact.title = data.organizations?[0]?["title"]
contact.company = data.organizations?[0]?["name"]
contact.thirdPartyData ?= {}
contact.thirdPartyData["FullContact"] = data
return contact

View file

@ -16,6 +16,7 @@ class SidebarFullContactDetails extends React.Component
<div className="header">
{@_profilePhoto()}
<h1 className="name">{@_name()}</h1>
<div className="email">{@_email()}</div>
</div>
<div className="subheader"
style={display: if @_showSubheader() then "block" else "none"}>
@ -81,7 +82,13 @@ class SidebarFullContactDetails extends React.Component
@_title().length > 0 or @_company().length > 0
_name: =>
(@props.fullContact.contactInfo?.fullName) ? @props.contact?.name
(@props.fullContact.contactInfo?.fullName) ? @props.contact?.name ? ""
_email: =>
email = @props.contact.email ? ""
if @_name().toLowerCase().trim() isnt email.toLowerCase().trim()
return email
else return ""
_title: =>
org = @_primaryOrg()

View file

@ -2,6 +2,8 @@ _ = require 'underscore-plus'
React = require "react"
FullContactStore = require "./fullcontact-store"
{InjectedComponentSet} = require 'ui-components'
SidebarFullContactDetails = require "./sidebar-fullcontact-details"
class SidebarFullContact extends React.Component
@ -10,6 +12,7 @@ class SidebarFullContact extends React.Component
order: 1
maxWidth: 300
minWidth: 200
flexShrink: 0
constructor: (@props) ->
@state = @_getStateFromStores()
@ -24,11 +27,13 @@ class SidebarFullContact extends React.Component
<div className="full-contact-sidebar">
<SidebarFullContactDetails contact={@state.focusedContact ? {}}
fullContact={@_fullContact()}/>
<InjectedComponentSet matching={role: "sidebar:focusedContactInfo"}
exposedProps={focusedContact: @state.focusedContact}/>
</div>
_fullContact: =>
if @state.focusedContact?.email
return @state.fullContactCache[@state.focusedContact.email] ? {}
if @state.focusedContact?.thirdPartyData
return @state.focusedContact?.thirdPartyData["FullContact"] ? {}
else
return {}
@ -36,7 +41,6 @@ class SidebarFullContact extends React.Component
@setState(@_getStateFromStores())
_getStateFromStores: =>
fullContactCache: FullContactStore.fullContactCache()
focusedContact: FullContactStore.focusedContact()

View file

@ -10,7 +10,7 @@ fixtureModule = 'internal_packages/salesforce'
Adapter = require path.join('../../', fixtureModule, 'lib/salesforce-schema-adapter.coffee')
fpath = path.join(fixtureModule, 'spec/fixtures/opportunity-layouts.json')
rawData = JSON.parse(fs.readFileSync(fpath, 'utf-8'))
testData = Adapter.convertLayout("opportunity", rawData)
testData = Adapter.convertFullEditLayout("opportunity", rawData)
describe "Form Builder", ->
beforeEach ->

View file

@ -232,7 +232,7 @@ class GeneratedFieldset extends React.Component
for item, i in items
itemsWithSpacers.push(item)
if i isnt items.length - 1 or items.length is 1
if i isnt items.length - 1
itemsWithSpacers.push(spacer: true)
<div className="row"

View file

@ -17,6 +17,14 @@ For more information about Contacts on the Nylas Platform, read the
`email`: {AttributeString} The email address of the contact. Queryable.
`thirdPartyData`: {AttributeObject} Extra data that we find out about a
contact. The data is keyed by the service that dumped the data there e.g.
"salesforce" or "fullcontact". The value is an object of raw data in the
form that the service provides
We also have "normalized" optional data for each contact. This list may
grow as the needs of a contact become more complex.
This class also inherits attributes from {Model}
Section: Models
@ -32,6 +40,19 @@ class Contact extends Model
queryable: true
modelKey: 'email'
# Contains the raw thirdPartyData (keyed by the vendor name) about
# this contact.
'thirdPartyData': Attributes.Object
modelKey: 'thirdPartyData'
# The following are "normalized" fields that we can use to consolidate
# various thirdPartyData source. These list of attributes should
# always be optional and may change as the needs of a Nylas contact
# change over time.
'title': Attributes.String(modelKey: 'title')
'phone': Attributes.String(modelKey: 'phone')
'company': Attributes.String(modelKey: 'company')
# Used to uniquely identify a contact
nameEmail: ->
"#{(@name ? "").toLowerCase().trim()} #{@email.toLowerCase().trim()}"

View file

@ -81,7 +81,7 @@ class ContactStore
matches
_refreshCache: =>
__refreshCache: =>
new Promise (resolve, reject) =>
DatabaseStore.findAll(Contact)
.then (contacts=[]) =>
@ -89,6 +89,7 @@ class ContactStore
@trigger()
resolve()
.catch(reject)
_refreshCache: _.debounce(ContactStore::__refreshCache, 20)
_onDatabaseChanged: (change) =>
return unless change?.objectClass is Contact.name

View file

@ -2,7 +2,18 @@
@import "ui-mixins";
input[type="text"],
input[type="email"] {
input[type="email"],
input[type="date"],
input[type="datetime"],
input[type="datetime-local"],
input[type="month"],
input[type="number"],
input[type="password"],
input[type="range"],
input[type="search"],
input[type="tel"],
input[type="time"],
input[type="url"] {
width: 100%;
padding-left: @padding-xs-horizontal;
padding-right: @padding-xs-horizontal;
@ -20,4 +31,4 @@ input[type="email"]:focus, {
&.input-bordered {
box-shadow: 0 0 3px @accent-primary;
}
}
}