From 57cb02c76a048f9267b9140f1aa370640637f9e4 Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Thu, 30 Apr 2015 11:35:38 -0700 Subject: [PATCH] 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 --- exports/inbox-exports.coffee | 7 +- .../lib/account-sidebar-store.coffee | 4 +- .../composer/lib/composer-view.cjsx | 3 +- .../lib/message-subject-item.cjsx | 21 ++-- .../lib/message-toolbar-items.cjsx | 20 +-- .../search-bar/lib/search-bar.cjsx | 3 +- internal_packages/tooltip/lib/tooltip.cjsx | 2 + .../spec/main-spec.coffee | 1 - .../tokenizing-text-field-spec.cjsx | 2 +- spec-inbox/stores/metadata-store-spec.coffee | 4 + spec/spec-bootstrap.coffee | 1 + src/components/generated-form.cjsx | 10 +- src/components/tokenizing-text-field.cjsx | 11 +- src/flux/actions.coffee | 6 +- src/flux/edgehill-api.coffee | 36 +++--- src/flux/models/metadata.coffee | 34 +++++ src/flux/models/utils.coffee | 6 +- src/flux/stores/metadata-store.coffee | 116 ++++++++++++++++++ src/flux/tasks/create-metadata-task.coffee | 72 +++++++++++ src/flux/tasks/destroy-metadata-task.coffee | 89 ++++++++++++++ src/package-manager.coffee | 3 +- src/package.coffee | 3 +- src/window-bootstrap.coffee | 1 + src/window-secondary-bootstrap.coffee | 1 + 24 files changed, 394 insertions(+), 62 deletions(-) create mode 100644 spec-inbox/stores/metadata-store-spec.coffee create mode 100644 src/flux/models/metadata.coffee create mode 100644 src/flux/stores/metadata-store.coffee create mode 100644 src/flux/tasks/create-metadata-task.coffee create mode 100644 src/flux/tasks/destroy-metadata-task.coffee diff --git a/exports/inbox-exports.coffee b/exports/inbox-exports.coffee index 7f4a4c222..eb685bfa7 100644 --- a/exports/inbox-exports.coffee +++ b/exports/inbox-exports.coffee @@ -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' diff --git a/internal_packages/account-sidebar/lib/account-sidebar-store.coffee b/internal_packages/account-sidebar/lib/account-sidebar-store.coffee index 81abf7ce6..2614bb334 100644 --- a/internal_packages/account-sidebar/lib/account-sidebar-store.coffee +++ b/internal_packages/account-sidebar/lib/account-sidebar-store.coffee @@ -111,10 +111,10 @@ AccountSidebarStore = Reflux.createStore @trigger(@) _onDataChanged: (change) -> - @populateInboxCountDebounced ?= _.debounce -> + @populateInboxCountDebounced ?= _.debounce => @_populateInboxCount() , 1000 - @populateDraftCountDebounced ?= _.debounce -> + @populateDraftCountDebounced ?= _.debounce => @_populateDraftCount() , 1000 diff --git a/internal_packages/composer/lib/composer-view.cjsx b/internal_packages/composer/lib/composer-view.cjsx index 719f72bd3..fcf48f7af 100644 --- a/internal_packages/composer/lib/composer-view.cjsx +++ b/internal_packages/composer/lib/composer-view.cjsx @@ -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' diff --git a/internal_packages/message-list/lib/message-subject-item.cjsx b/internal_packages/message-list/lib/message-subject-item.cjsx index 9c630c505..8dc03a955 100644 --- a/internal_packages/message-list/lib/message-subject-item.cjsx +++ b/internal_packages/message-list/lib/message-subject-item.cjsx @@ -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: =>
{@state.thread?.subject}
- _onChange: -> _.defer => - return unless @isMounted() + _onChange: => _.defer => @setState(@_getStateFromStores()) - _getStateFromStores: -> + _getStateFromStores: => thread: FocusedContentStore.focused('thread') +module.exports = MessageSubjectItem diff --git a/internal_packages/message-list/lib/message-toolbar-items.cjsx b/internal_packages/message-list/lib/message-toolbar-items.cjsx index c03104c8e..e6b5c9c32 100644 --- a/internal_packages/message-list/lib/message-toolbar-items.cjsx +++ b/internal_packages/message-list/lib/message-toolbar-items.cjsx @@ -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 - 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 diff --git a/internal_packages/search-bar/lib/search-bar.cjsx b/internal_packages/search-bar/lib/search-bar.cjsx index 14939ef03..a383f747e 100644 --- a/internal_packages/search-bar/lib/search-bar.cjsx +++ b/internal_packages/search-bar/lib/search-bar.cjsx @@ -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) diff --git a/internal_packages/tooltip/lib/tooltip.cjsx b/internal_packages/tooltip/lib/tooltip.cjsx index a6ae540b5..3455e883b 100644 --- a/internal_packages/tooltip/lib/tooltip.cjsx +++ b/internal_packages/tooltip/lib/tooltip.cjsx @@ -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 ### diff --git a/internal_packages/unread-notifications/spec/main-spec.coffee b/internal_packages/unread-notifications/spec/main-spec.coffee index 259e4fde6..6f912de7d 100644 --- a/internal_packages/unread-notifications/spec/main-spec.coffee +++ b/internal_packages/unread-notifications/spec/main-spec.coffee @@ -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' diff --git a/spec-inbox/components/tokenizing-text-field-spec.cjsx b/spec-inbox/components/tokenizing-text-field-spec.cjsx index 1552bd089..e4604609e 100644 --- a/spec-inbox/components/tokenizing-text-field-spec.cjsx +++ b/spec-inbox/components/tokenizing-text-field-spec.cjsx @@ -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) diff --git a/spec-inbox/stores/metadata-store-spec.coffee b/spec-inbox/stores/metadata-store-spec.coffee new file mode 100644 index 000000000..9cee22939 --- /dev/null +++ b/spec-inbox/stores/metadata-store-spec.coffee @@ -0,0 +1,4 @@ +MetadataStore = require '../../src/flux/stores/metadata-store' +describe "MetadataStore", -> + beforeEach: -> + spyOn(atom, "isMainWindow").andReturn(true) diff --git a/spec/spec-bootstrap.coffee b/spec/spec-bootstrap.coffee index a75f274e6..8a0923a0f 100644 --- a/spec/spec-bootstrap.coffee +++ b/spec/spec-bootstrap.coffee @@ -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. diff --git a/src/components/generated-form.cjsx b/src/components/generated-form.cjsx index 20267ef25..a8dd7db92 100644 --- a/src/components/generated-form.cjsx +++ b/src/components/generated-form.cjsx @@ -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) -
+
{_.map itemsWithSpacers, (formItemData, i) => if formItemData.spacer
@@ -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 diff --git a/src/components/tokenizing-text-field.cjsx b/src/components/tokenizing-text-field.cjsx index 39d6874e0..63782cb14 100644 --- a/src/components/tokenizing-text-field.cjsx +++ b/src/components/tokenizing-text-field.cjsx @@ -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() diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index 8e44dc710..97691cdf0 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -120,7 +120,11 @@ windowActions = [ "fileDownloaded", "popSheet", - "pushSheet" + "pushSheet", + + "metadataError", + "metadataCreated", + "metadataDestroyed" ] allActions = [].concat(windowActions).concat(globalActions).concat(mainWindowActions) diff --git a/src/flux/edgehill-api.coffee b/src/flux/edgehill-api.coffee index 8dedcc416..1f730480a 100644 --- a/src/flux/edgehill-api.coffee +++ b/src/flux/edgehill-api.coffee @@ -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() diff --git a/src/flux/models/metadata.coffee b/src/flux/models/metadata.coffee new file mode 100644 index 000000000..69df90fb1 --- /dev/null +++ b/src/flux/models/metadata.coffee @@ -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() diff --git a/src/flux/models/utils.coffee b/src/flux/models/utils.coffee index 9cc33bb1c..adb774a6c 100644 --- a/src/flux/models/utils.coffee +++ b/src/flux/models/utils.coffee @@ -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 diff --git a/src/flux/stores/metadata-store.coffee b/src/flux/stores/metadata-store.coffee new file mode 100644 index 000000000..0423102c2 --- /dev/null +++ b/src/flux/stores/metadata-store.coffee @@ -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) diff --git a/src/flux/tasks/create-metadata-task.coffee b/src/flux/tasks/create-metadata-task.coffee new file mode 100644 index 000000000..ebe2c3839 --- /dev/null +++ b/src/flux/tasks/create-metadata-task.coffee @@ -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() diff --git a/src/flux/tasks/destroy-metadata-task.coffee b/src/flux/tasks/destroy-metadata-task.coffee new file mode 100644 index 000000000..a63a515a3 --- /dev/null +++ b/src/flux/tasks/destroy-metadata-task.coffee @@ -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() diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 04b15d416..d26b46238 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -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 diff --git a/src/package.coffee b/src/package.coffee index 895d469a7..f620e5979 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -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 diff --git a/src/window-bootstrap.coffee b/src/window-bootstrap.coffee index 9c7446473..e51cea98a 100644 --- a/src/window-bootstrap.coffee +++ b/src/window-bootstrap.coffee @@ -7,6 +7,7 @@ require './window' Atom = require './atom' window.atom = Atom.loadOrCreate('editor') +global.Promise.longStackTraces() if atom.inDevMode() atom.initialize() atom.startRootWindow() diff --git a/src/window-secondary-bootstrap.coffee b/src/window-secondary-bootstrap.coffee index f08cf7549..17ce6ed70 100644 --- a/src/window-secondary-bootstrap.coffee +++ b/src/window-secondary-bootstrap.coffee @@ -11,6 +11,7 @@ require './window' Atom = require './atom' window.atom = Atom.loadOrCreate() +global.Promise.longStackTraces() if atom.inDevMode() atom.initialize() atom.startSecondaryWindow()