diff --git a/internal_packages/composer/lib/composer-view.cjsx b/internal_packages/composer/lib/composer-view.cjsx index c99218350..d473b892c 100644 --- a/internal_packages/composer/lib/composer-view.cjsx +++ b/internal_packages/composer/lib/composer-view.cjsx @@ -24,6 +24,22 @@ class ComposerView extends React.Component @containerRequired: false + @propTypes: + localId: React.PropTypes.string.isRequired + + # Either "inline" or "fullwindow" + mode: React.PropTypes.string + + # If this composer is part of an existing thread (like inline + # composers) the threadId will be handed down + threadId: React.PropTypes.string + + # Sometimes when changes in the composer happens it's desirable to + # have the parent scroll to a certain location. A parent component can + # pass a callback that gets called when this composer wants to be + # scrolled to. + onRequestScrollTo: React.PropTypes.func + constructor: (@props) -> @state = populated: false @@ -211,7 +227,7 @@ class ComposerView extends React.Component + exposedProps={draftLocalId:@props.localId, threadId: @props.threadId}/> _renderActionsRegion: => @@ -219,7 +235,7 @@ class ComposerView extends React.Component + exposedProps={draftLocalId:@props.localId, threadId: @props.threadId}> {@props.children} @@ -70,6 +70,10 @@ TokenizingTextField = React.createClass # change. tokens: React.PropTypes.arrayOf(React.PropTypes.object) + # The maximum number of tokens allowed. When null (the default) and + # unlimited number of tokens may be given + maxTokens: React.PropTypes.number + # A unique ID for each token object # # A function that, given an object used for tokens, returns a unique @@ -118,6 +122,10 @@ TokenizingTextField = React.createClass # updates this component's `tokens` prop. onAdd: React.PropTypes.func.isRequired + # By default, when blurred, whatever is in the field is added. If this + # is true, the field will be cleared instead. + clearOnBlur: React.PropTypes.bool + # Gets called when we remove a token # # It's passed an array of objects (the same ones used to render @@ -186,7 +194,10 @@ TokenizingTextField = React.createClass measure.innerText = @state.inputValue measure.style.top = input.offsetTop + "px" measure.style.left = input.offsetLeft + "px" - input.style.width = "calc(6px + #{measure.offsetWidth}px)" + if @_atMaxTokens() + input.style.width = "4px" + else + input.style.width = "calc(6px + #{measure.offsetWidth}px)" render: -> {Menu} = require 'ui-components' @@ -212,17 +223,7 @@ TokenizingTextField = React.createClass
{@_fieldTokenComponents()} - + {@_inputEl()}
+ _inputEl: -> + if @_atMaxTokens() + @_onInputFocused(noCompletions: true)} + onChange={ -> "noop" } + tabIndex={@props.tabIndex} + value="" /> + else + + + _atMaxTokens: -> + if @props.maxTokens + @props.tokens.length >= @props.maxTokens + else return false + _renderPrompt: -> if @props.menuPrompt
{"#{@props.menuPrompt}:"}
@@ -248,9 +279,9 @@ TokenizingTextField = React.createClass # Maintaining Input State - _onInputFocused: -> + _onInputFocused: ({noCompletions}={}) -> @setState focus: true - @_refreshCompletions() + @_refreshCompletions() unless noCompletions _onInputChanged: (event) -> val = event.target.value.trimLeft() @@ -260,7 +291,11 @@ TokenizingTextField = React.createClass @_refreshCompletions(val) _onInputBlurred: -> - @_addInputValue() + if @props.clearOnBlur + @_clearInput() + else + @_addInputValue() + @_refreshCompletions("", clear: true) @setState selectedTokenKey: null focus: false @@ -275,6 +310,7 @@ TokenizingTextField = React.createClass # Managing Tokens _addInputValue: (input) -> + return if @_atMaxTokens() input ?= @state.inputValue @props.onAdd(input) @_clearInput() diff --git a/src/components/unsafe-component.cjsx b/src/components/unsafe-component.cjsx index 6f236f065..db3571784 100644 --- a/src/components/unsafe-component.cjsx +++ b/src/components/unsafe-component.cjsx @@ -55,16 +55,25 @@ class UnsafeComponent extends React.Component element = @injected = React.render(element, node) catch err - stack = err.stack - stackEnd = stack.indexOf('react/lib/') - if stackEnd > 0 - stackEnd = stack.lastIndexOf('\n', stackEnd) - stack = stack.substr(0,stackEnd) + if atom.inDevMode() + console.error err + stack = err.stack + console.log stack + stackEnd = stack.indexOf('react/lib/') + if stackEnd > 0 + stackEnd = stack.lastIndexOf('\n', stackEnd) + stack = stack.substr(0,stackEnd) - element =
-
{@props.component.displayName} could not be displayed.
-
{stack}
-
+ element =
+
{@props.component.displayName} could not be displayed.
+
{stack}
+
+ else + ## TODO + # Add some sort of notification code here that lets us know when + # production builds are having issues! + # + element =
@injected = React.render(element, node) diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index 97691cdf0..a0651b953 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -125,6 +125,8 @@ windowActions = [ "metadataError", "metadataCreated", "metadataDestroyed" + + "unloading" # Tied to `window.onbeforeunload` ] allActions = [].concat(windowActions).concat(globalActions).concat(mainWindowActions) diff --git a/src/flux/models/message.coffee b/src/flux/models/message.coffee index 703763c91..fc7abba67 100644 --- a/src/flux/models/message.coffee +++ b/src/flux/models/message.coffee @@ -124,7 +124,6 @@ class Message extends Model @naturalSortOrder: -> Message.attributes.date.ascending() - constructor: -> super @subject ||= "" diff --git a/src/flux/models/utils.coffee b/src/flux/models/utils.coffee index adb774a6c..31a452d83 100644 --- a/src/flux/models/utils.coffee +++ b/src/flux/models/utils.coffee @@ -135,22 +135,21 @@ Utils = tableNameForJoin: (primaryKlass, secondaryKlass) -> "#{primaryKlass.name}-#{secondaryKlass.name}" - imageNamed: (fullname) -> + imageNamed: (resourcePath, fullname) -> [name, ext] = fullname.split('.') - if Utils.images is undefined - start = Date.now() - {resourcePath} = atom.getLoadSettings() + Utils.images ?= {} + if not Utils.images[resourcePath]? imagesPath = path.join(resourcePath, 'static', 'images') files = fs.listTreeSync(imagesPath) - Utils.images = {} - Utils.images[path.basename(file)] = file for file in files + Utils.images[resourcePath] ?= {} + Utils.images[resourcePath][path.basename(file)] = file for file in files if window.devicePixelRatio > 1 - return Utils.images["#{name}@2x.#{ext}"] ? Utils.images[fullname] ? Utils.images["#{name}@1x.#{ext}"] + return Utils.images[resourcePath]["#{name}@2x.#{ext}"] ? Utils.images[resourcePath][fullname] ? Utils.images[resourcePath]["#{name}@1x.#{ext}"] else - return Utils.images["#{name}@1x.#{ext}"] ? Utils.images[fullname] ? Utils.images["#{name}@2x.#{ext}"] + return Utils.images[resourcePath]["#{name}@1x.#{ext}"] ? Utils.images[resourcePath][fullname] ? Utils.images[resourcePath]["#{name}@2x.#{ext}"] subjectWithPrefix: (subject, prefix) -> if subject.search(/fwd:/i) is 0 diff --git a/src/flux/stores/analytics-store.coffee b/src/flux/stores/analytics-store.coffee index 920c2980c..430828ca8 100644 --- a/src/flux/stores/analytics-store.coffee +++ b/src/flux/stores/analytics-store.coffee @@ -30,7 +30,7 @@ AnalyticsStore = Reflux.createStore fileAborted: (uploadData={}) -> {fileSize: uploadData.fileSize} fileUploaded: (uploadData={}) -> {fileSize: uploadData.fileSize} sendDraftError: (dId, msg) -> {drafLocalId: dId, error: msg} - sendDraftSuccess: (draftLocalId) -> {draftLocalId: draftLocalId} + sendDraftSuccess: ({draftLocalId}) -> {draftLocalId: draftLocalId} showDeveloperConsole: -> {} composeReply: ({threadId, messageId}) -> {threadId, messageId} composeForward: ({threadId, messageId}) -> {threadId, messageId} diff --git a/src/flux/stores/draft-store.coffee b/src/flux/stores/draft-store.coffee index ba7e70f3b..afba681bc 100644 --- a/src/flux/stores/draft-store.coffee +++ b/src/flux/stores/draft-store.coffee @@ -56,6 +56,7 @@ class DraftStore @listenTo Actions.sendDraftError, @_onSendDraftError @listenTo Actions.sendDraftSuccess, @_onSendDraftSuccess + @listenTo Actions.unloading, @_onBeforeUnload @_draftSessions = {} @_sendingState = {} @@ -65,7 +66,6 @@ class DraftStore # TODO: Doesn't work if we do window.addEventListener, but this is # fragile. Pending an Atom fix perhaps? - window.onbeforeunload = => @_onBeforeUnload() ######### PUBLIC ####################################################### @@ -389,7 +389,7 @@ class DraftStore @_onPopoutDraftLocalId(draftLocalId, {errorMessage}) @trigger() - _onSendDraftSuccess: (draftLocalId) => + _onSendDraftSuccess: ({draftLocalId}) => @_sendingState[draftLocalId] = false @trigger() diff --git a/src/flux/stores/metadata-store.coffee b/src/flux/stores/metadata-store.coffee index 0423102c2..2c18a8a07 100644 --- a/src/flux/stores/metadata-store.coffee +++ b/src/flux/stores/metadata-store.coffee @@ -14,7 +14,7 @@ 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 +# SalesforceObjectStore or the SalesforceThreadAssociationStore) 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. @@ -24,7 +24,6 @@ MAX_API_RATE = 1000 module.exports = MetadataStore = Reflux.createStore init: -> - return unless atom.isMainWindow() @listenTo DatabaseStore, @_onDBChanged @listenTo NamespaceStore, @_onNamespaceChanged @@ -45,9 +44,8 @@ MetadataStore = Reflux.createStore @_namespaceId = NamespaceStore.current()?.id @_metadata = {} - return if atom.inSpecMode() - @_fullRefreshFromAPI() + @_refreshCacheFromDB = _.debounce(_.bind(@_refreshCacheFromDB, @), 16) @_refreshCacheFromDB() @@ -62,10 +60,12 @@ MetadataStore = Reflux.createStore else return null _fullRefreshFromAPI: -> + return if not atom.isMainWindow() or atom.inSpecMode() return unless @_namespaceId @_apiRequest() # The lack of type will request everything! _refreshDBFromAPI: -> + return if not atom.isMainWindow() or atom.inSpecMode() types = Object.keys(@_typesToRefresh) @_typesToRefresh = {} promises = types.map (type) => @_apiRequest(type) diff --git a/src/flux/tasks/send-draft.coffee b/src/flux/tasks/send-draft.coffee index 5e9adec2f..bf9ad9da5 100644 --- a/src/flux/tasks/send-draft.coffee +++ b/src/flux/tasks/send-draft.coffee @@ -48,10 +48,13 @@ class SendDraftTask extends Task method: 'POST' body: body returnsModel: true - success: => + success: (newMessage) => + newMessage = (new Message).fromJSON(newMessage) atom.playSound('mail_sent.ogg') Actions.postNotification({message: "Sent!", type: 'success'}) - Actions.sendDraftSuccess(@draftLocalId) + Actions.sendDraftSuccess + draftLocalId: @draftLocalId + newMessage: newMessage DatabaseStore.unpersistModel(draft).then(resolve) error: reject .catch(reject) diff --git a/src/package.coffee b/src/package.coffee index c5d07f37e..fd1e4d9b0 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -162,7 +162,7 @@ class Package @activateConfig() @activateStylesheets() if @requireMainModule() - @mainModule.activate(atom.packages.getPackageState(@name) ? {}) + @mainModule.activate(atom.packages.getPackageState(@name) ? {}, path.resolve(@path)) @mainActivated = true @activateServices() catch e @@ -385,6 +385,7 @@ class Package return mainModulePath = @getMainModulePath() @mainModule = require(mainModulePath) if fs.isFileSync(mainModulePath) + return @mainModule getMainModulePath: -> return @mainModulePath if @resolvedMainModulePath diff --git a/src/sheet-container.cjsx b/src/sheet-container.cjsx index 535f3bfd6..2b38007c3 100644 --- a/src/sheet-container.cjsx +++ b/src/sheet-container.cjsx @@ -181,7 +181,7 @@ class SheetContainer extends React.Component sheetElements = @_sheetElements() -
+
{toolbarElements[0]}
-
+
-
+
{sheetElements[0]}
-
+
diff --git a/static/components/generated-form.less b/static/components/generated-form.less index e57c38555..02317c590 100644 --- a/static/components/generated-form.less +++ b/static/components/generated-form.less @@ -27,6 +27,10 @@ .input-area { position: relative; flex: 2; + + select { + margin-top: 9px; + } } .form-error { diff --git a/static/components/tokenizing-text-field.less b/static/components/tokenizing-text-field.less index e0b276740..b0dfe5e21 100644 --- a/static/components/tokenizing-text-field.less +++ b/static/components/tokenizing-text-field.less @@ -78,6 +78,10 @@ position: relative; padding-left: 2.1em; + &:hover { + cursor: text; + } + input { display: inline-block; width: initial; @@ -87,7 +91,14 @@ min-width: 5em; background-color:transparent; vertical-align:bottom; + &.noop-input { + position: absolute; + min-width: 0; + padding-left: 0; + margin-right: 0; + } } + input:focus { box-shadow: none; }