mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-03-01 18:44:01 +08:00
feat(salesforce): associate threads with SF objects
Summary: #### WIP! #### This is making it all work with the association endpoint, putting together the Salesforce Sidebar interfaces, and getting the nested creators/updaters working. I still need to do a bunch of UI work and actually debug the whole workflow still --- rename SalesforceContactStore to SalesforceSearchStore rename SalesforceContact to SalesforceSearchResult salesforce sidebar changes salesforce association picker object form store fixes figuring out newFormItem instigators Make SalesforceObjectFormStore declarative off SalesforceObjectStore Make action basd handlers for SalesforceObjectStore sidebar store create and associate salesforce sidebar and picker fixes association works and displays on sidebar salesforce object form fixes object form fixes fix salesforce updating Test Plan: TODO Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://review.inboxapp.com/D1440
This commit is contained in:
parent
01938c1c78
commit
57cb02c76a
24 changed files with 394 additions and 62 deletions
|
@ -12,6 +12,10 @@ Exports =
|
|||
Task: require '../src/flux/tasks/task'
|
||||
TaskQueue: require '../src/flux/stores/task-queue'
|
||||
|
||||
# Tasks
|
||||
CreateMetadataTask: require '../src/flux/tasks/create-metadata-task'
|
||||
DestroyMetadataTask: require '../src/flux/tasks/destroy-metadata-task'
|
||||
|
||||
# The Database
|
||||
DatabaseStore: require '../src/flux/stores/database-store'
|
||||
ModelView: require '../src/flux/stores/model-view'
|
||||
|
@ -44,6 +48,7 @@ Exports =
|
|||
DraftStoreExtension: require '../src/flux/stores/draft-store-extension'
|
||||
MessageStore: require '../src/flux/stores/message-store'
|
||||
ContactStore: require '../src/flux/stores/contact-store'
|
||||
MetadataStore: require '../src/flux/stores/metadata-store'
|
||||
NamespaceStore: require '../src/flux/stores/namespace-store'
|
||||
AnalyticsStore: require '../src/flux/stores/analytics-store'
|
||||
WorkspaceStore: require '../src/flux/stores/workspace-store'
|
||||
|
@ -60,7 +65,7 @@ Exports =
|
|||
|
||||
## TODO move to inside of individual Salesforce package. See https://trello.com/c/tLAGLyeb/246-move-salesforce-models-into-individual-package-db-models-for-packages-various-refactors
|
||||
SalesforceAssociation: require '../src/flux/models/salesforce-association'
|
||||
SalesforceContact: require '../src/flux/models/salesforce-contact'
|
||||
SalesforceSearchResult: require '../src/flux/models/salesforce-search-result'
|
||||
SalesforceObject: require '../src/flux/models/salesforce-object'
|
||||
SalesforceSchema: require '../src/flux/models/salesforce-schema'
|
||||
|
||||
|
|
|
@ -111,10 +111,10 @@ AccountSidebarStore = Reflux.createStore
|
|||
@trigger(@)
|
||||
|
||||
_onDataChanged: (change) ->
|
||||
@populateInboxCountDebounced ?= _.debounce ->
|
||||
@populateInboxCountDebounced ?= _.debounce =>
|
||||
@_populateInboxCount()
|
||||
, 1000
|
||||
@populateDraftCountDebounced ?= _.debounce ->
|
||||
@populateDraftCountDebounced ?= _.debounce =>
|
||||
@_populateDraftCount()
|
||||
, 1000
|
||||
|
||||
|
|
|
@ -18,8 +18,7 @@ ParticipantsTextField = require './participants-text-field'
|
|||
|
||||
# The ComposerView is a unique React component because it (currently) is a
|
||||
# singleton. Normally, the React way to do things would be to re-render the
|
||||
# Composer with new props. As an alternative, we can call `setProps` to
|
||||
# simulate the effect of the parent re-rendering us
|
||||
# Composer with new props.
|
||||
module.exports =
|
||||
ComposerView = React.createClass
|
||||
displayName: 'ComposerView'
|
||||
|
|
|
@ -2,26 +2,25 @@ _ = require 'underscore-plus'
|
|||
React = require 'react'
|
||||
{FocusedContentStore} = require 'inbox-exports'
|
||||
|
||||
module.exports =
|
||||
MessageSubjectItem = React.createClass
|
||||
displayName: 'MessageSubjectItem'
|
||||
class MessageSubjectItem extends React.Component
|
||||
@displayName: 'MessageSubjectItem'
|
||||
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@_unsubscriber = FocusedContentStore.listen @_onChange
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@_unsubscriber() if @_unsubscriber
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
<div className="message-toolbar-subject">{@state.thread?.subject}</div>
|
||||
|
||||
_onChange: -> _.defer =>
|
||||
return unless @isMounted()
|
||||
_onChange: => _.defer =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: ->
|
||||
_getStateFromStores: =>
|
||||
thread: FocusedContentStore.focused('thread')
|
||||
|
||||
module.exports = MessageSubjectItem
|
||||
|
|
|
@ -59,13 +59,14 @@ ArchiveButton = React.createClass
|
|||
Actions.archive()
|
||||
e.stopPropagation()
|
||||
|
||||
class MessageToolbarItems extends React.Component
|
||||
@displayName: "MessageToolbarItems"
|
||||
|
||||
module.exports =
|
||||
MessageToolbarItems = React.createClass
|
||||
getInitialState: ->
|
||||
threadIsSelected: FocusedContentStore.focusedId('thread')?
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
threadIsSelected: FocusedContentStore.focusedId('thread')?
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
classes = classNames
|
||||
"message-toolbar-items": true
|
||||
"hidden": !@state.threadIsSelected
|
||||
|
@ -74,14 +75,15 @@ MessageToolbarItems = React.createClass
|
|||
<ArchiveButton ref="archiveButton" />
|
||||
</div>
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@_unsubscribers = []
|
||||
@_unsubscribers.push FocusedContentStore.listen @_onChange
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
unsubscribe() for unsubscribe in @_unsubscribers
|
||||
|
||||
_onChange: -> _.defer =>
|
||||
return unless @isMounted()
|
||||
_onChange: => _.defer =>
|
||||
@setState
|
||||
threadIsSelected: FocusedContentStore.focusedId('thread')?
|
||||
|
||||
module.exports = MessageToolbarItems
|
||||
|
|
|
@ -21,7 +21,7 @@ class SearchBar extends React.Component
|
|||
@body_unsubscriber = atom.commands.add 'body', {
|
||||
'application:focus-search': @_onFocusSearch
|
||||
}
|
||||
@body_unsubscriber = atom.commands.add '.search-bar', {
|
||||
@search_unsubscriber = atom.commands.add '.search-bar', {
|
||||
'search-bar:escape-search': @_clearAndBlur
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ class SearchBar extends React.Component
|
|||
componentWillUnmount: =>
|
||||
@unsubscribe()
|
||||
@body_unsubscriber.dispose()
|
||||
@search_unsubscriber.dispose()
|
||||
|
||||
render: =>
|
||||
inputValue = @_queryToString(@state.query)
|
||||
|
|
|
@ -6,6 +6,8 @@ React = require 'react/addons'
|
|||
The Tooltip component displays a consistent hovering tooltip for use when
|
||||
extra context information is required.
|
||||
|
||||
Activate by adding a `data-tooltip="Label"` to any element
|
||||
|
||||
It's a global-level singleton
|
||||
###
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
_ = require 'underscore-plus'
|
||||
Promise = require 'bluebird'
|
||||
Contact = require '../../../src/flux/models/contact'
|
||||
Message = require '../../../src/flux/models/message'
|
||||
Thread = require '../../../src/flux/models/thread'
|
||||
|
|
|
@ -72,7 +72,7 @@ describe 'TokenizingTextField', ->
|
|||
tabIndex={@tabIndex}
|
||||
/>
|
||||
)
|
||||
@renderedInput = ReactTestUtils.findRenderedDOMComponentWithTag(@renderedField, 'input').getDOMNode()
|
||||
@renderedInput = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(@renderedField, 'input'))
|
||||
|
||||
it 'renders into the document', ->
|
||||
expect(ReactTestUtils.isCompositeComponentWithType @renderedField, TokenizingTextField).toBe(true)
|
||||
|
|
4
spec-inbox/stores/metadata-store-spec.coffee
Normal file
4
spec-inbox/stores/metadata-store-spec.coffee
Normal file
|
@ -0,0 +1,4 @@
|
|||
MetadataStore = require '../../src/flux/stores/metadata-store'
|
||||
describe "MetadataStore", ->
|
||||
beforeEach: ->
|
||||
spyOn(atom, "isMainWindow").andReturn(true)
|
|
@ -13,6 +13,7 @@ try
|
|||
Atom = require '../src/atom'
|
||||
Atom.configDirPath = fs.absolute('~/.inbox-spec')
|
||||
window.atom = Atom.loadOrCreate()
|
||||
global.Promise.longStackTraces() if atom.inDevMode()
|
||||
|
||||
# Show window synchronously so a focusout doesn't fire on input elements
|
||||
# that are focused in the very first spec run.
|
||||
|
|
|
@ -133,7 +133,8 @@ FormItem = React.createClass
|
|||
|
||||
refreshValidityState: -> _.defer =>
|
||||
return unless @isMounted()
|
||||
el = @refs.input.getDOMNode()
|
||||
return unless @refs.input
|
||||
el = React.findDOMNode(@refs.input)
|
||||
|
||||
customMsg = @props.formItemError?.message
|
||||
if el.setCustomValidity?
|
||||
|
@ -236,7 +237,10 @@ GeneratedFieldset = React.createClass
|
|||
if i isnt items.length - 1 or items.length is 1
|
||||
itemsWithSpacers.push(spacer: true)
|
||||
|
||||
<div className="row" data-row-num={rowNum} key={rowNum}>
|
||||
<div className="row"
|
||||
data-row-num={rowNum}
|
||||
style={zIndex: 1000-rowNum}
|
||||
key={rowNum}>
|
||||
{_.map itemsWithSpacers, (formItemData, i) =>
|
||||
if formItemData.spacer
|
||||
<div className="column-spacer" data-col-num={i} key={i}>
|
||||
|
@ -310,7 +314,7 @@ GeneratedForm = React.createClass
|
|||
not Utils.isEqualReact(nextState, @state)
|
||||
|
||||
_onSubmit: ->
|
||||
valid = @refs.form.getDOMNode().reportValidity()
|
||||
valid = React.findDOMNode(@refs.form).reportValidity()
|
||||
if valid
|
||||
@props.onSubmit()
|
||||
else
|
||||
|
|
|
@ -165,7 +165,7 @@ TokenizingTextField = React.createClass
|
|||
selectedTokenKey: null
|
||||
|
||||
componentDidMount: ->
|
||||
input = @refs.input.getDOMNode()
|
||||
input = React.findDOMNode(@refs.input)
|
||||
check = (fn) -> (event) ->
|
||||
return unless event.target is input
|
||||
# Wrapper to guard against events triggering on the wrong element
|
||||
|
@ -187,12 +187,12 @@ TokenizingTextField = React.createClass
|
|||
componentDidUpdate: ->
|
||||
# Measure the width of the text in the input and
|
||||
# resize the input field to fit.
|
||||
input = @refs.input.getDOMNode()
|
||||
measure = @refs.measure.getDOMNode()
|
||||
input = React.findDOMNode(@refs.input)
|
||||
measure = React.findDOMNode(@refs.measure)
|
||||
measure.innerText = @state.inputValue
|
||||
measure.style.top = input.offsetTop + "px"
|
||||
measure.style.left = input.offsetLeft + "px"
|
||||
input.style.width = "calc(4px + #{measure.offsetWidth}px)"
|
||||
input.style.width = "calc(6px + #{measure.offsetWidth}px)"
|
||||
|
||||
render: ->
|
||||
{Menu} = require 'ui-components'
|
||||
|
@ -278,7 +278,7 @@ TokenizingTextField = React.createClass
|
|||
inputValue: ""
|
||||
|
||||
focus: ->
|
||||
@refs.input.getDOMNode().focus()
|
||||
React.findDOMNode(@refs.input).focus()
|
||||
|
||||
# Managing Tokens
|
||||
|
||||
|
@ -296,6 +296,7 @@ TokenizingTextField = React.createClass
|
|||
@props.tokenKey(t) is @state.selectedTokenKey
|
||||
|
||||
_addToken: (token) ->
|
||||
return unless @isMounted()
|
||||
return unless token
|
||||
@props.onAdd([token])
|
||||
@_clearInput()
|
||||
|
|
|
@ -120,7 +120,11 @@ windowActions = [
|
|||
"fileDownloaded",
|
||||
|
||||
"popSheet",
|
||||
"pushSheet"
|
||||
"pushSheet",
|
||||
|
||||
"metadataError",
|
||||
"metadataCreated",
|
||||
"metadataDestroyed"
|
||||
]
|
||||
|
||||
allActions = [].concat(windowActions).concat(globalActions).concat(mainWindowActions)
|
||||
|
|
|
@ -5,7 +5,6 @@ DatabaseStore = require './stores/database-store'
|
|||
PriorityUICoordinator = require '../priority-ui-coordinator'
|
||||
{modelFromJSON} = require './models/utils'
|
||||
async = require 'async'
|
||||
SALESFORCE_PROXY_ROOT = "/proxy/salesforce/services/data/v33.0"
|
||||
|
||||
class EdgehillAPI
|
||||
|
||||
|
@ -17,23 +16,21 @@ class EdgehillAPI
|
|||
_onConfigChanged: =>
|
||||
env = atom.config.get('env')
|
||||
if env is 'development'
|
||||
@APIRoot = "http://localhost:5009"
|
||||
# @APIRoot = "http://localhost:5009"
|
||||
@APIRoot = "https://edgehill-dev.nylas.com"
|
||||
else if env is 'staging'
|
||||
@APIRoot = "https://edgehill-staging.nilas.com"
|
||||
@APIRoot = "https://edgehill-staging.nylas.com"
|
||||
else
|
||||
@APIRoot = "https://edgehill.nilas.com"
|
||||
@APIRoot = "https://edgehill.nylas.com"
|
||||
|
||||
request: (options={}) ->
|
||||
return if atom.getLoadSettings().isSpec
|
||||
options.method ?= 'GET'
|
||||
if options.proxyPath
|
||||
options.url ?= "#{@APIRoot}#{SALESFORCE_PROXY_ROOT}/#{options.proxyPath}"
|
||||
else
|
||||
options.url ?= "#{@APIRoot}#{options.path}" if options.path
|
||||
options.url ?= "#{@APIRoot}#{options.path}" if options.path
|
||||
options.body ?= {} unless options.formData
|
||||
options.json = true
|
||||
|
||||
auth = @getCredentials()
|
||||
auth = @_getCredentials()
|
||||
if auth
|
||||
options.auth =
|
||||
user: auth.username
|
||||
|
@ -51,30 +48,27 @@ class EdgehillAPI
|
|||
options.success(body) if options.success
|
||||
|
||||
urlForConnecting: (provider, email_address = '') ->
|
||||
auth = @getCredentials()
|
||||
auth = @_getCredentials()
|
||||
root = @APIRoot
|
||||
token = auth?.username
|
||||
"#{root}/connect/#{provider}?login_hint=#{email_address}&token=#{token}"
|
||||
|
||||
getCredentials: ->
|
||||
atom.config.get('edgehill.credentials')
|
||||
|
||||
setCredentials: (credentials) ->
|
||||
atom.config.set('edgehill.credentials', credentials)
|
||||
|
||||
addTokens: (tokens) ->
|
||||
for token in tokens
|
||||
if token.provider is 'inbox'
|
||||
atom.config.set('inbox.token', token.access_token)
|
||||
if token.provider is 'salesforce'
|
||||
atom.config.set('salesforce.token', token.access_token)
|
||||
atom.config.set("#{token.provider}.token", token.access_token)
|
||||
|
||||
if token.user_identifier?
|
||||
@setCredentials({username: token.user_identifier, password: ''})
|
||||
@_setCredentials({username: token.user_identifier, password: ''})
|
||||
|
||||
tokenForProvider: (provider) ->
|
||||
atom.config.get("#{provider}.token")
|
||||
|
||||
_getCredentials: ->
|
||||
atom.config.get('edgehill.credentials')
|
||||
|
||||
_setCredentials: (credentials) ->
|
||||
atom.config.set('edgehill.credentials', credentials)
|
||||
|
||||
_defaultErrorCallback: (apiError) ->
|
||||
apiError.notifyConsole()
|
||||
|
||||
|
|
34
src/flux/models/metadata.coffee
Normal file
34
src/flux/models/metadata.coffee
Normal file
|
@ -0,0 +1,34 @@
|
|||
Model = require './model'
|
||||
Attributes = require '../attributes'
|
||||
{generateTempId} = require './utils'
|
||||
|
||||
Function::getter = (prop, get) ->
|
||||
Object.defineProperty @prototype, prop, {get, configurable: yes}
|
||||
|
||||
module.exports =
|
||||
class Metadata extends Model
|
||||
@attributes:
|
||||
'type': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'type'
|
||||
jsonKey: 'type'
|
||||
|
||||
'publicId': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'publicId'
|
||||
jsonKey: 'publicId'
|
||||
|
||||
'key': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'key'
|
||||
jsonKey: 'key'
|
||||
|
||||
'value': Attributes.Object
|
||||
modelKey: 'value'
|
||||
jsonKey: 'value'
|
||||
|
||||
@getter 'id', ->
|
||||
if @type and @publicId and @key
|
||||
@id = "#{@type}/#{@publicId}/#{@key}"
|
||||
else
|
||||
@id = generateTempId()
|
|
@ -56,12 +56,13 @@ Utils =
|
|||
LocalLink = require './local-link'
|
||||
Event = require './event'
|
||||
Calendar = require './calendar'
|
||||
Metadata = require './metadata'
|
||||
|
||||
## TODO move to inside of individual Salesforce package. See https://trello.com/c/tLAGLyeb/246-move-salesforce-models-into-individual-package-db-models-for-packages-various-refactors
|
||||
SalesforceObject = require './salesforce-object'
|
||||
SalesforceSchema = require './salesforce-schema'
|
||||
SalesforceAssociation = require './salesforce-association'
|
||||
SalesforceContact = require './salesforce-contact'
|
||||
SalesforceSearchResult = require './salesforce-search-result'
|
||||
SalesforceTask = require './salesforce-task'
|
||||
|
||||
SyncbackDraftTask = require '../tasks/syncback-draft'
|
||||
|
@ -83,10 +84,11 @@ Utils =
|
|||
'locallink': LocalLink
|
||||
'calendar': Calendar
|
||||
'event': Event
|
||||
'metadata': Metadata
|
||||
'salesforceschema': SalesforceSchema
|
||||
'salesforceobject': SalesforceObject
|
||||
'salesforceassociation': SalesforceAssociation
|
||||
'salesforcecontact': SalesforceContact
|
||||
'salesforcesearchresult': SalesforceSearchResult
|
||||
'salesforcetask': SalesforceTask
|
||||
|
||||
'MarkThreadReadTask': MarkThreadReadTask
|
||||
|
|
116
src/flux/stores/metadata-store.coffee
Normal file
116
src/flux/stores/metadata-store.coffee
Normal file
|
@ -0,0 +1,116 @@
|
|||
_ = require 'underscore-plus'
|
||||
|
||||
Reflux = require 'reflux'
|
||||
Actions = require '../actions'
|
||||
Metadata = require '../models/metadata'
|
||||
|
||||
EdgehillAPI = require '../edgehill-api'
|
||||
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
NamespaceStore = require '../stores/namespace-store'
|
||||
|
||||
|
||||
CreateMetadataTask = require '../tasks/create-metadata-task'
|
||||
DestroyMetadataTask = require '../tasks/destroy-metadata-task'
|
||||
|
||||
# TODO: This Store is like many other stores (like the
|
||||
# SalesforceObjectStore or the SalesforceAssociationStore) in that it has
|
||||
# to double cache data from the API and the DB with minor variation.
|
||||
# There's a task to refactor these stores into something like an
|
||||
# `APIBackedStore` to abstract some of the complex logic out.
|
||||
|
||||
MAX_API_RATE = 1000
|
||||
|
||||
module.exports =
|
||||
MetadataStore = Reflux.createStore
|
||||
init: ->
|
||||
return unless atom.isMainWindow()
|
||||
@listenTo DatabaseStore, @_onDBChanged
|
||||
@listenTo NamespaceStore, @_onNamespaceChanged
|
||||
|
||||
refreshDBFromAPI = _.debounce(_.bind(@_refreshDBFromAPI, @), MAX_API_RATE)
|
||||
@_typesToRefresh = {}
|
||||
|
||||
@listenTo Actions.metadataError, (errorData) =>
|
||||
return unless errorData.type
|
||||
@_typesToRefresh[errorData.type] = true
|
||||
refreshDBFromAPI()
|
||||
@listenTo Actions.metadataCreated, (type) =>
|
||||
@_typesToRefresh[type] = true
|
||||
refreshDBFromAPI()
|
||||
@listenTo Actions.metadataDestroyed, (type) =>
|
||||
@_typesToRefresh[type] = true
|
||||
refreshDBFromAPI()
|
||||
|
||||
@_namespaceId = NamespaceStore.current()?.id
|
||||
@_metadata = {}
|
||||
|
||||
return if atom.inSpecMode()
|
||||
|
||||
@_fullRefreshFromAPI()
|
||||
@_refreshCacheFromDB = _.debounce(_.bind(@_refreshCacheFromDB, @), 16)
|
||||
@_refreshCacheFromDB()
|
||||
|
||||
# Returns a promise that will eventually return the metadata you want
|
||||
getMetadata: (type, publicId, key) ->
|
||||
if type? and publicId? and key?
|
||||
return @_metadata[type]?[publicId]?[key]
|
||||
else if type? and publicId?
|
||||
return @_metadata[type]?[publicId]
|
||||
else if type?
|
||||
return @_metadata[type]
|
||||
else return null
|
||||
|
||||
_fullRefreshFromAPI: ->
|
||||
return unless @_namespaceId
|
||||
@_apiRequest() # The lack of type will request everything!
|
||||
|
||||
_refreshDBFromAPI: ->
|
||||
types = Object.keys(@_typesToRefresh)
|
||||
@_typesToRefresh = {}
|
||||
promises = types.map (type) => @_apiRequest(type)
|
||||
Promise.settle(promises)
|
||||
|
||||
_apiRequest: (type) ->
|
||||
typePath = if type then "/#{type}/" else ""
|
||||
new Promise (resolve, reject) =>
|
||||
EdgehillAPI.request
|
||||
path: "/metadata/#{@_namespaceId}#{typePath}"
|
||||
success: (metadata) ->
|
||||
metadata = metadata?.results ? []
|
||||
metadata = metadata.map (metadatum) ->
|
||||
metadatum.publicId = metadatum.id
|
||||
return new Metadata(metadatum)
|
||||
if metadata.length is 0 then resolve()
|
||||
else
|
||||
DatabaseStore.persistModels(metadata).then(resolve).catch(reject)
|
||||
error: (apiError) ->
|
||||
apiError.notifyConsole()
|
||||
reject(apiError)
|
||||
|
||||
_onDBChanged: (change) ->
|
||||
return unless change.objectClass is Metadata.name
|
||||
@_refreshCacheFromDB()
|
||||
|
||||
_refreshCacheFromDB: ->
|
||||
new Promise (resolve, reject) =>
|
||||
DatabaseStore.findAll(Metadata)
|
||||
.then (metadata=[]) =>
|
||||
@_metadata = {}
|
||||
for metadatum in metadata
|
||||
@_metadata[metadatum.type] ?= {}
|
||||
@_metadata[metadatum.type][metadatum.publicId] ?= {}
|
||||
@_metadata[metadatum.type][metadatum.publicId][metadatum.key] = metadatum.value
|
||||
@trigger()
|
||||
resolve()
|
||||
.catch(reject)
|
||||
|
||||
_onNamespaceChanged: ->
|
||||
@_namespaceId = NamespaceStore.current()?.id
|
||||
@_fullRefreshFromAPI()
|
||||
|
||||
_deleteAllMetadata: ->
|
||||
DatabaseStore.findAll(Metadata).then (metadata) ->
|
||||
meatdata.forEach (metadatum) ->
|
||||
t = new DestroyMetadataTask(metadatum)
|
||||
Actions.queueTask(t)
|
72
src/flux/tasks/create-metadata-task.coffee
Normal file
72
src/flux/tasks/create-metadata-task.coffee
Normal file
|
@ -0,0 +1,72 @@
|
|||
_ = require 'underscore-plus'
|
||||
Task = require './task'
|
||||
Actions = require '../actions'
|
||||
Metadata = require '../models/metadata'
|
||||
EdgehillAPI = require '../edgehill-api'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
NamespaceStore = require '../stores/namespace-store'
|
||||
|
||||
module.exports =
|
||||
class CreateMetadataTask extends Task
|
||||
constructor: ({@type, @publicId, @key, @value}) ->
|
||||
@name = "CreateMetadataTask"
|
||||
|
||||
shouldDequeueOtherTask: (other) ->
|
||||
@_isSameTask(other) or @_isOldDestroyTask(other)
|
||||
|
||||
_isSameTask: (other) ->
|
||||
other instanceof CreateMetadataTask and
|
||||
@type is other.type and @publicId is other.publicId and
|
||||
@key is other.key and @value is other.value
|
||||
|
||||
_isOldDestroyTask: (other) ->
|
||||
other.name is "DestroyMetadataTask"
|
||||
@type is other.type and @publicId is other.publicId and @key is other.key
|
||||
|
||||
performLocal: ->
|
||||
return Promise.reject(new Error("Must pass a type")) unless @type?
|
||||
@metadatum = new Metadata({@type, @publicId, @key, @value})
|
||||
return DatabaseStore.persistModel(@metadatum)
|
||||
|
||||
performRemote: -> new Promise (resolve, reject) =>
|
||||
EdgehillAPI.request
|
||||
method: "POST"
|
||||
path: "/metadata/#{NamespaceStore.current().id}/#{@type}/#{@publicId}"
|
||||
body:
|
||||
key: @key
|
||||
value: @value
|
||||
success: (args...) =>
|
||||
Actions.metadataCreated @type, @metadatum
|
||||
resolve(args...)
|
||||
error: (apiError) ->
|
||||
apiError.notifyConsole()
|
||||
reject(apiError)
|
||||
|
||||
onAPIError: (apiError) ->
|
||||
Actions.metadataError _.extend @_baseErrorData(),
|
||||
errorType: "APIError"
|
||||
error: apiError
|
||||
Promise.resolve()
|
||||
|
||||
onOtherError: (otherError) ->
|
||||
Actions.metadataError _.extend @_baseErrorData(),
|
||||
errorType: "OtherError"
|
||||
error: otherError
|
||||
Promise.resolve()
|
||||
|
||||
onTimeoutError: (timeoutError) ->
|
||||
Actions.metadataError _.extend @_baseErrorData(),
|
||||
errorType: "TimeoutError"
|
||||
error: timeoutError
|
||||
Promise.resolve()
|
||||
|
||||
_baseErrorData: ->
|
||||
action: "create"
|
||||
className: @constructor.name
|
||||
type: @type
|
||||
publicId: @publicId
|
||||
key: @key
|
||||
value: @value
|
||||
|
||||
onOfflineError: (offlineError) ->
|
||||
Promise.resolve()
|
89
src/flux/tasks/destroy-metadata-task.coffee
Normal file
89
src/flux/tasks/destroy-metadata-task.coffee
Normal file
|
@ -0,0 +1,89 @@
|
|||
_ = require 'underscore-plus'
|
||||
Task = require './task'
|
||||
Actions = require '../actions'
|
||||
Metadata = require '../models/metadata'
|
||||
EdgehillAPI = require '../edgehill-api'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
NamespaceStore = require '../stores/namespace-store'
|
||||
|
||||
module.exports =
|
||||
class DestroyMetadataTask extends Task
|
||||
constructor: ({@type, @publicId, @key}) ->
|
||||
@name = "DestroyMetadataTask"
|
||||
|
||||
shouldDequeueOtherTask: (other) ->
|
||||
@_isSameTask(other) or @_isOldCreateTask(other)
|
||||
|
||||
_isSameTask: (other) ->
|
||||
other instanceof DestroyMetadataTask and
|
||||
@type is other.type and @publicId is other.publicId and @key is other.key
|
||||
|
||||
_isOldCreateTask: (other) ->
|
||||
other.name is "CreateMetadataTask"
|
||||
@type is other.type and @publicId is other.publicId and @key is other.key
|
||||
|
||||
performLocal: ->
|
||||
return Promise.reject(new Error("Must pass a type")) unless @type?
|
||||
return Promise.reject(new Error("Must pass an publicId")) unless @publicId?
|
||||
new Promise (resolve, reject) =>
|
||||
if @key?
|
||||
matcher = {@type, @publicId, @key}
|
||||
else
|
||||
matcher = {@type, @publicId}
|
||||
|
||||
DatabaseStore.findAll(Metadata, matcher)
|
||||
.then (models) ->
|
||||
if (models ? []).length is 0
|
||||
resolve()
|
||||
else
|
||||
Promise.settle(models.map (m) -> DatabaseStore.unpersistModel(m))
|
||||
.then(resolve).catch(reject)
|
||||
.catch (error) ->
|
||||
console.error "Error finding Metadata to destroy", error
|
||||
console.error error.stack
|
||||
reject(error)
|
||||
|
||||
performRemote: -> new Promise (resolve, reject) =>
|
||||
if @key?
|
||||
body = {@key}
|
||||
else
|
||||
body = null
|
||||
|
||||
EdgehillAPI.request
|
||||
method: "DELETE"
|
||||
path: "/metadata/#{NamespaceStore.current().id}/#{@type}/#{@publicId}"
|
||||
body: body
|
||||
success: (args...) =>
|
||||
Actions.metadataDestroyed(@type)
|
||||
resolve(args...)
|
||||
error: (apiError) ->
|
||||
apiError.notifyConsole()
|
||||
reject(apiError)
|
||||
|
||||
onAPIError: (apiError) ->
|
||||
Actions.metadataError _.extend @_baseErrorData(),
|
||||
errorType: "APIError"
|
||||
error: apiError
|
||||
Promise.resolve()
|
||||
|
||||
onOtherError: (otherError) ->
|
||||
Actions.metadataError _.extend @_baseErrorData(),
|
||||
errorType: "OtherError"
|
||||
error: otherError
|
||||
Promise.resolve()
|
||||
|
||||
onTimeoutError: (timeoutError) ->
|
||||
Actions.metadataError _.extend @_baseErrorData(),
|
||||
errorType: "TimeoutError"
|
||||
error: timeoutError
|
||||
Promise.resolve()
|
||||
|
||||
_baseErrorData: ->
|
||||
action: "destroy"
|
||||
className: @constructor.name
|
||||
type: @type
|
||||
publicId: @publicId
|
||||
key: @key
|
||||
|
||||
onOfflineError: (offlineError) ->
|
||||
Promise.resolve()
|
|
@ -370,7 +370,8 @@ class PackageManager
|
|||
@emitter.emit 'did-load-package', pack
|
||||
return pack
|
||||
catch error
|
||||
console.warn "Failed to load package.json '#{path.basename(packagePath)}'", error.stack ? error
|
||||
console.warn "Failed to load package.json '#{path.basename(packagePath)}'"
|
||||
console.warn error.stack ? error
|
||||
else
|
||||
console.warn "Could not resolve '#{nameOrPath}' to a package path"
|
||||
null
|
||||
|
|
|
@ -128,7 +128,8 @@ class Package
|
|||
@requireMainModule() unless @hasActivationCommands()
|
||||
|
||||
catch error
|
||||
console.warn "Failed to load package named '#{@name}'", error.stack ? error
|
||||
console.warn "Failed to load package named '#{@name}'"
|
||||
console.warn error.stack ? error
|
||||
console.error(error.message, error)
|
||||
this
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ require './window'
|
|||
|
||||
Atom = require './atom'
|
||||
window.atom = Atom.loadOrCreate('editor')
|
||||
global.Promise.longStackTraces() if atom.inDevMode()
|
||||
atom.initialize()
|
||||
atom.startRootWindow()
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ require './window'
|
|||
|
||||
Atom = require './atom'
|
||||
window.atom = Atom.loadOrCreate()
|
||||
global.Promise.longStackTraces() if atom.inDevMode()
|
||||
atom.initialize()
|
||||
atom.startSecondaryWindow()
|
||||
|
||||
|
|
Loading…
Reference in a new issue