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